Merge pull request #7425 from dotCooCoo/fix/ai-envelope-budget-warped-type

.ai save: images with a Warp count against the envelope limit
This commit is contained in:
BFlat
2026-11-15 05:30:30 +07:00
committed by GitHub
6 changed files with 162 additions and 15 deletions
+113
View File
@@ -10194,6 +10194,119 @@ fn envelopes_past_the_readers_checks_are_written_as_their_art() {
assert!(!notes.iter().any(|n| n == crate::import::ENVELOPE_DRAWN), "{notes:?}");
}
/// A document of `envelopes` envelopes and `images` embedded images with one Warp, the images first
/// when `images_first`, saved and opened again: the live envelopes, the images with their Warp live
/// and all the objects it opens with, the save's warnings and the open's notes.
fn envelopes_and_warped_images(envelopes: usize, images: usize, images_first: bool) -> (usize, usize, usize, Vec<String>, Vec<String>) {
use vectorcraft_doc::live::{EnvelopeKind, EnvelopeOptions};
let mut doc = Document::new(200.0, 100.0);
let layer = doc.layers[0].id;
let add_images = |doc: &mut Document| {
for _ in 0..images {
let photo = warped_photo(doc, vec![fx("warp.arc", serde_json::json!({"bend": 40.0}))]);
doc.insert(Some(layer), usize::MAX, photo).unwrap();
}
};
if images_first {
add_images(&mut doc);
}
for _ in 0..envelopes {
let points = [(30.0, 10.0), (170.0, 0.0), (30.0, 90.0), (170.0, 90.0)].map(kurbo::Point::from).to_vec();
let kind = EnvelopeKind::Mesh { rows: 1, cols: 1, points, handles: vec![] };
let envelope = envelope_node(&mut doc, kind, 50.0, false, EnvelopeOptions::NEW);
doc.insert(Some(layer), usize::MAX, envelope).unwrap();
}
if !images_first {
add_images(&mut doc);
}
let (p, warnings) = private_data(&doc, &opts()).unwrap();
let (back, notes) = crate::ai_alone(&p.data).unwrap();
let a = back.layers[0].children().unwrap();
let live = a.iter().filter(|n| matches!(n.kind, NodeKind::Envelope { .. })).count();
let warped = a.iter().filter(|n| matches!(n.kind, NodeKind::Image(_)) && n.appearance.effects.iter().any(|e| e.id == "warp.arc")).count();
(live, warped, a.len(), warnings, notes)
}
/// Images with a Warp take the reader's checks as envelopes do: past them, an image is written as
/// its drawn look, with a warning, and the file opens with every object written live still live.
#[test]
fn warped_images_past_the_readers_checks_are_written_as_their_drawn_look() {
// GUARDS: images with a Warp were written live past the checks the envelopes left, and the
// file opened with the last one as its drawn look and no warning on saving.
let limit = crate::import::ENVELOPE_CHECKS;
let (live, warped, all, warnings, notes) = envelopes_and_warped_images(limit - 1, 2, false);
assert_eq!((live, warped, all), (limit - 1, 1, limit + 1), "{warnings:?} {notes:?}");
assert!(warnings.iter().any(|w| w == writer::WARPED_IMAGES), "{warnings:?}");
assert!(!warnings.iter().any(|w| w == writer::ENVELOPES), "{warnings:?}");
assert!(!notes.iter().any(|n| n.contains("drawn look") || n == crate::import::ENVELOPE_DRAWN), "{notes:?}");
}
/// An image with a Warp whose pixels can't be read is written as its gray outline, which the reader
/// doesn't check: it leaves its check to the objects after it.
#[test]
fn a_warped_image_written_as_its_outline_takes_no_check() {
// GUARDS: an image written as its gray outline took a check, and the image after it lost its Warp.
use vectorcraft_doc::live::{EnvelopeKind, EnvelopeOptions};
let limit = crate::import::ENVELOPE_CHECKS;
let mut doc = Document::new(200.0, 100.0);
let layer = doc.layers[0].id;
for _ in 0..limit - 1 {
let points = [(30.0, 10.0), (170.0, 0.0), (30.0, 90.0), (170.0, 90.0)].map(kurbo::Point::from).to_vec();
let kind = EnvelopeKind::Mesh { rows: 1, cols: 1, points, handles: vec![] };
let envelope = envelope_node(&mut doc, kind, 50.0, false, EnvelopeOptions::NEW);
doc.insert(Some(layer), usize::MAX, envelope).unwrap();
}
let arc = || fx("warp.arc", serde_json::json!({"bend": 40.0}));
let mut broken = image_node(&mut doc, ImageBlob::png(b"not a png".to_vec()), (8, 8), Affine::new([10.0, 0.0, 0.0, 10.0, 20.0, 10.0]), None);
broken.appearance = Appearance { effects: vec![arc()], ..Appearance::default() }.into();
doc.insert(Some(layer), usize::MAX, broken).unwrap();
let photo = warped_photo(&mut doc, vec![arc()]);
doc.insert(Some(layer), usize::MAX, photo).unwrap();
let (p, warnings) = private_data(&doc, &opts()).unwrap();
assert!(warnings.iter().any(|w| w == writer::IMAGES), "{warnings:?}");
assert!(!warnings.iter().any(|w| w == writer::WARPED_IMAGES), "{warnings:?}");
let (back, notes) = crate::ai_alone(&p.data).unwrap();
let a = back.layers[0].children().unwrap();
let warped = a.iter().filter(|n| matches!(n.kind, NodeKind::Image(_)) && n.appearance.effects.iter().any(|e| e.id == "warp.arc")).count();
assert_eq!(warped, 1, "{notes:?}");
}
/// Past the reader's checks, an image with a Warp whose pixels can't be read is still written as its
/// gray outline, with the warning for its pixels and not the one for the checks.
#[test]
fn an_unreadable_warped_image_past_the_checks_is_warned_about_its_pixels() {
// GUARDS: an image whose pixels can't be read was warned about as past the checks.
use vectorcraft_doc::live::{EnvelopeKind, EnvelopeOptions};
let limit = crate::import::ENVELOPE_CHECKS;
let mut doc = Document::new(200.0, 100.0);
let layer = doc.layers[0].id;
for _ in 0..limit {
let points = [(30.0, 10.0), (170.0, 0.0), (30.0, 90.0), (170.0, 90.0)].map(kurbo::Point::from).to_vec();
let kind = EnvelopeKind::Mesh { rows: 1, cols: 1, points, handles: vec![] };
let envelope = envelope_node(&mut doc, kind, 50.0, false, EnvelopeOptions::NEW);
doc.insert(Some(layer), usize::MAX, envelope).unwrap();
}
let mut broken = image_node(&mut doc, ImageBlob::png(b"not a png".to_vec()), (8, 8), Affine::new([10.0, 0.0, 0.0, 10.0, 20.0, 10.0]), None);
broken.appearance = Appearance { effects: vec![fx("warp.arc", serde_json::json!({"bend": 40.0}))], ..Appearance::default() }.into();
doc.insert(Some(layer), usize::MAX, broken).unwrap();
let (_, warnings) = private_data(&doc, &opts()).unwrap();
assert!(warnings.iter().any(|w| w == writer::IMAGES), "{warnings:?}");
assert!(!warnings.iter().any(|w| w == writer::WARPED_IMAGES), "{warnings:?}");
}
/// Envelopes after images with a Warp get the checks the images left: past them, an envelope is
/// written as its art, with a warning.
#[test]
fn envelopes_get_the_checks_warped_images_left() {
// GUARDS: envelopes were written live past the checks the images with a Warp before them left.
let limit = crate::import::ENVELOPE_CHECKS;
let (live, warped, all, warnings, notes) = envelopes_and_warped_images(limit - 1, 2, true);
assert_eq!((live, warped, all), (limit - 2, 2, limit + 1), "{warnings:?} {notes:?}");
assert!(warnings.iter().any(|w| w == writer::ENVELOPES), "{warnings:?}");
assert!(!warnings.iter().any(|w| w == writer::WARPED_IMAGES), "{warnings:?}");
assert!(!notes.iter().any(|n| n.contains("drawn look") || n == crate::import::ENVELOPE_DRAWN), "{notes:?}");
}
/// Every Envelope Options switch and the ends of Fidelity come back as saved.
#[test]
fn envelope_options_and_fidelity_are_read_back() {
+6 -5
View File
@@ -30,7 +30,7 @@ pub(super) use brushes::BRUSHES;
pub(super) use envelopes::ENVELOPES;
pub(super) use styles::{LARGE_CANVAS_STYLES, style_links, styled_objects};
#[cfg(test)]
pub(super) use styles::{PANEL_EMPTY, PANEL_STYLES, STYLE_LINKS, same_appearance};
pub(super) use styles::{PANEL_EMPTY, PANEL_STYLES, STYLE_LINKS, WARPED_IMAGES, same_appearance};
pub(super) use images::PAYLOAD;
#[cfg(test)]
@@ -281,9 +281,9 @@ pub(super) struct Writer<'a> {
/// The object just written is an envelope's distorted art, which the plugin's `XP` follows
/// ([`Self::envelope`]).
deform: bool,
/// How many more envelopes may be written live: as many as the reader checks in one file
/// ([`crate::import::ENVELOPE_CHECKS`]).
envelopes_left: usize,
/// How many more envelopes and images with a Warp may be written live: as many as the reader
/// checks in one file ([`crate::import::ENVELOPE_CHECKS`]), which it spends on both.
checks_left: usize,
/// The image read last and its pixels ([`images::Samples::of`], `None` when they can't be
/// read): the pieces of a warped image, which follow one another, read them once.
last_samples: Option<(vectorcraft_doc::ImageBlob, Option<images::Read>)>,
@@ -357,7 +357,7 @@ impl<'a> Writer<'a> {
brush_set: brushes::Brushes::default(),
brushes_live: true,
deform: false,
envelopes_left: crate::import::ENVELOPE_CHECKS,
checks_left: crate::import::ENVELOPE_CHECKS,
objects: 0,
left_out: 0,
definition_objects: 0,
@@ -662,6 +662,7 @@ impl<'a> Writer<'a> {
&& self.clip.is_none()
&& !self.in_symbol
&& let Some(orig) = self.styled.get(&n.id).cloned()
&& self.check_left(&orig)
&& let Some(name) = self.object_style(n, &orig)
{
return self.styled_object(n, &orig, &name);
@@ -26,7 +26,7 @@ use super::{MAX_NEST, Writer, Xy, hide};
use crate::import::{DEFORM_PLUGIN, EnvelopeGridPoint as GridPoint, envelope_outline, envelope_upright, warp_number};
use crate::ps;
pub(in crate::ai_editing) const ENVELOPES: &str = "envelopes of type, envelopes without contents, envelopes in opacity masks, pattern tiles, art cut to a clipping object or other envelopes' contents, envelopes on the layers of a large canvas or in a turned or mirrored frame, warps drawn without their style's net, meshes and top objects that the data would open in another shape, envelopes with a Fidelity that isn't a whole number, envelopes that map from a box other than their contents' bounds with or without strokes or keep bounds margins with Distort Appearance off, envelopes after the first 256, and envelopes nested more than 8 deep are written as their art in the editing data for other apps";
pub(in crate::ai_editing) const ENVELOPES: &str = "envelopes of type, envelopes without contents, envelopes in opacity masks, pattern tiles, art cut to a clipping object or other envelopes' contents, envelopes on the layers of a large canvas or in a turned or mirrored frame, warps drawn without their style's net, meshes and top objects that the data would open in another shape, envelopes with a Fidelity that isn't a whole number, envelopes that map from a box other than their contents' bounds with or without strokes or keep bounds margins with Distort Appearance off, envelopes after the first 256 envelopes and images with a Warp, and envelopes nested more than 8 deep are written as their art in the editing data for other apps";
/// The most rows or columns of an envelope's grid written (Make with Mesh's own limit, and the
/// reader's).
@@ -287,7 +287,7 @@ impl Writer<'_> {
/// another shape, or some of it is left out for the object limit ([`Self::take_objects`]).
pub(super) fn envelope(&mut self, n: &Node, xy: Xy) -> bool {
let NodeKind::Envelope { content, kind, fidelity, editing, options, frame } = &n.kind else { return false };
if self.hidden > 0 || self.clip.is_some() || self.nest >= MAX_NEST || !self.symbols_live || self.envelopes_left == 0 {
if self.hidden > 0 || self.clip.is_some() || self.nest >= MAX_NEST || !self.symbols_live || self.checks_left == 0 {
return false;
}
let Some(record) = record(n, content, kind, *fidelity, *editing, options, **frame) else { return false };
@@ -339,7 +339,7 @@ impl Writer<'_> {
return false;
}
self.deform = true;
self.envelopes_left -= 1;
self.checks_left -= 1;
true
}
+13 -4
View File
@@ -121,29 +121,37 @@ impl Samples {
}
impl Writer<'_> {
/// Whether [`Self::image`] can read `im`'s pixels, rather than write its gray outline.
pub(super) fn image_readable(&mut self, im: &ImageObject) -> bool {
let doc = self.doc;
doc.image_blob(im).and_then(|b| self.samples(b)).is_some()
}
/// Image object `im` of transparency `xy`: embedded, a raster ([`Self::raster`]); linked, a
/// placed file with a preview ([`Self::placed_file`]), except in a part of the data written on
/// `%_` lines (other than a styled object's copy on the layers, [`Self::styled_object`]) and in
/// a symbol's art. One whose pixels can't be read is its outline filled mid gray. Where the art is
/// cut to a clipping path, the image is in a clipping group of that path; a raster of pixels
/// written whole already is then the part of them that the path shows ([`ai_ops::image_window`]).
pub(super) fn image(&mut self, im: &ImageObject, xy: Xy) {
/// True when the image is written as an image.
pub(super) fn image(&mut self, im: &ImageObject, xy: Xy) -> bool {
let doc = self.doc;
let blob = doc.image_blob(im);
let read = blob.and_then(|b| self.samples(b));
let Some((samples, deep)) = read.as_deref() else {
let bounds = PathData::from_bezpath(&(im.xf * Rect::new(0.0, 0.0, f64::from(im.width), f64::from(im.height)).to_path(0.1)));
self.warn(IMAGES);
return self.fill_object(&bounds, FillRule::NonZero, &FillLayer::new(Paint::solid(Color::gray(0.5))), xy);
self.fill_object(&bounds, FillRule::NonZero, &FillLayer::new(Paint::solid(Color::gray(0.5))), xy);
return false;
};
// A region that has nothing left hides the image.
if self.clip.as_ref().is_some_and(|c| c.subpaths.iter().all(|sp| sp.anchors.is_empty())) {
return;
return false;
}
// The image, and where it is cut, the clipping group and its clipping path.
let clipping = self.clip.as_ref().map_or(0, |c| 1 + super::object_count(c.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).count()));
if !self.take_objects(1 + clipping) {
return;
return false;
}
if *deep {
self.warn(IMAGE_DEPTH);
@@ -217,6 +225,7 @@ impl Writer<'_> {
self.end_group("Q", Xy::PLAIN);
}
self.clip = cut;
true
}
/// An embedded image whose pixels `xf` places in the document: `%AI5_File:`,
+25 -2
View File
@@ -23,6 +23,7 @@ pub(in crate::ai_editing) const LARGE_CANVAS_STYLES: &str = "on a large canvas,
pub(in crate::ai_editing) const PANEL_STYLES: &str = "graphic styles with gradient or pattern fills, with fills, strokes or effects the editing data can't hold as a graphic style, with a name another style has, with names over 1,024 bytes, or past 4,096 styles, are left out of the editing data for other apps";
pub(in crate::ai_editing) const PANEL_EMPTY: &str =
"a Graphic Styles panel with no style written opens from the editing data with a new document's styles";
pub(in crate::ai_editing) const WARPED_IMAGES: &str = "images with a Warp after the first 256 envelopes and images with a Warp are written as their drawn look, without their Warp, in the editing data for other apps";
pub(in crate::ai_editing) const STYLE_LINKS: &str = "links to graphic styles are left out of the editing data for other apps on type, images, symbol instances, knockout shapes and other objects that aren't written with their graphic style";
/// How many objects of `doc`'s layers, opacity masks and symbols are linked to a graphic style.
@@ -719,6 +720,24 @@ impl Writer<'_> {
items_keep && m.children().is_none_or(|c| c.iter().all(|c| self.member_keeps_outline(c)))
}
/// Whether `orig` may be written with its art style as far as the reader's checks go: an image
/// with a Warp takes one of them, as an envelope does ([`Self::envelope`]); with none left, it is
/// written as its drawn look, with a warning. One whose pixels can't be read takes none: it is
/// written as its gray outline ([`Self::image`]).
pub(super) fn check_left(&mut self, orig: &Node) -> bool {
if !warped_image(orig) || self.checks_left > 0 {
return true;
}
let doc = self.doc;
if let NodeKind::Image(im) = &orig.kind
&& !self.image_readable(&doc.shown_image(orig, im))
{
return true;
}
self.warn(WARPED_IMAGES);
false
}
/// Object `n` (as baking left it; `orig` before baking) with the style `name`: its lock and
/// hidden state; its drawn look (a group of its vector art as baking left it, without shadows,
/// glows or feathers, the object's transparency
@@ -729,7 +748,8 @@ impl Writer<'_> {
/// `1 (name) XW`.
/// The look's group and the object itself are counted first ([`Self::take_objects`]): without
/// room for them, nothing is written, and the object counts neither as written with its graphic
/// style nor as written with a brush on its stroke.
/// style nor as written with a brush on its stroke. An image written as an image takes one of
/// the reader's checks ([`Self::check_left`]).
pub(super) fn styled_object(&mut self, n: &Node, orig: &Node, name: &str) {
let own = match &orig.kind {
NodeKind::Path { path, .. } => super::object_count(path.subpaths.iter().filter(|sp| !sp.anchors.is_empty()).count()),
@@ -787,9 +807,12 @@ impl Writer<'_> {
match &orig.kind {
NodeKind::Path { path, rule, .. } => self.styled_outline(orig, path, *rule, fit, xy),
NodeKind::Compound { rule, .. } => self.styled_outline(orig, &compound_path(orig), *rule, fit, xy),
// The reader checks the object only when it is an image, not its gray outline.
NodeKind::Image(im) => {
let was = std::mem::replace(&mut self.styled_copy, on_layers);
self.image(&self.doc.shown_image(orig, im), xy);
if self.image(&self.doc.shown_image(orig, im), xy) {
self.checks_left = self.checks_left.saturating_sub(1);
}
self.styled_copy = was;
}
NodeKind::Group { children, .. } => {
+2 -1
View File
@@ -629,6 +629,7 @@ since 2026-10-01.
- **Warp as a Bézier patch per style:** each of the fifteen Warp styles bends the object's bounds as one Bézier patch whose control points the style sets from the bend and the size of the bounds; the horizontal distortion then scales each column of control points about its middle and the vertical distortion scales each row. The Warp effects and warp envelopes share the map, and content with no width or no height takes the middle of the patch along that side. New warps, the built-in graphic styles' Warps and warps opened from `.ai` files carry `version` 1 for it. Warps in `.vectorcraft` documents saved before it read as `version` 0 and keep their earlier look, and edits keep a warp's version; a `version` 0 warp saved to `.ai` opens again with `version` 1, since the file holds only its settings. The patches are VectorCraft's own rules; the styles and ranges are those of the Warp options in [Distort and transform effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html) (doc `live::tests::warp_nets_bend_each_style_as_its_own_patch`, `warp_net_arc_edges_are_concentric_circles`, `warp_net_distortion_scales_the_columns_then_the_rows`, `warp_net_takes_any_numbers`, `warp_net_bends_a_huge_box_without_overflowing`, `warp_envelopes_bend_flat_content_as_the_middle_of_the_patch`, `warp_envelopes_write_their_map_version_only_when_it_has_one`, `style_libs::tests::builtin_warp_styles_take_the_nets_map`; effects `tests::a_warp_draws_with_the_map_its_version_names`, `tests::a_warp_bends_a_flat_box_as_the_middle_of_its_patch`, `tests::new_warps_take_the_nets_version`; engine `effectedit::warps_keep_the_map_their_version_names`, `envelope::new_warp_envelopes_take_the_net_and_saved_ones_keep_their_map`; eps `ai_editing::tests::every_effect_a_graphic_style_holds_is_read_back`, `import::ai::tests::a_warp_envelope_opens_live`, `import::ai::tests::warp_envelopes_read_every_style`, `import::ai::style::tests::a_stack_of_fills_with_effects_of_their_own_is_read_as_itself`; ui-egui `dialogs::tests_warp::warp_options_edit_keeps_each_targets_map_version`). Scores and hours unchanged.
- **Warp effects bend images:** a Warp effect on an image, or on a group that holds one, bends the image's pixels with its frame, as the Warp effect is documented to distort bitmap images in [Reshaping with effects](https://helpx.adobe.com/illustrator/using/reshaping-objects-effects.html): after the last effect, the image is cut into 10 × 10 cells of two triangles, each a clipped piece under its own affine map. Effects after the Warp reshape only the frame, a second Warp bends the same pieces, and warps saved without a version bend the pixels with their earlier map, as they bend the frame. A placed gradient on a group that holds the image follows the group's bounds as the pieces draw them. SVG writes the image once for the pieces; PDF, EPS, `.ai` and PNG write the bent pieces. The images stay unbent when their pieces would make the art pass 2 million objects, and so does an image whose frame keeps its shape past the anchor budget. A grayscale image's fill color now colorizes the pieces of a warp or an envelope, and an image under any geometry effect keeps that color and its smoothing setting. Existing documents with a Warp effect on an image, or on a group that holds one, draw differently: before, the image showed unbent, cut by the bent frame. So do existing documents with a colorized image under an envelope or a geometry effect, or an image drawn without smoothing under a geometry effect: before, those lost the color or the setting. The cell count and the limits are VectorCraft's own (effects `tests::warped_images_stay_within_the_object_budget`, `tests::warped_images_keep_document_keys_and_hidden_images_apart`, `tests::an_image_whose_frame_passes_the_budget_stays_whole`, `tests::effects_after_a_warp_reshape_an_images_frame_and_not_its_pieces`, `tests::an_images_pixels_take_the_map_their_warps_version_names`, `tests::an_image_under_a_geometry_effect_keeps_its_color_and_smoothing`, `tests::a_nested_groups_gradient_follows_its_warped_image`; engine `warp_images::an_images_pixels_bend_with_effect_warp`, `a_warped_image_keeps_its_bend_in_svg`, `a_warped_image_keeps_its_bend_in_pdf`, `a_warped_image_keeps_its_bend_in_ai`, `a_warped_image_keeps_its_bend_in_eps`, `a_warped_image_keeps_its_bend_in_png`, `svg_writes_an_image_clipped_twice_in_a_users_group_twice`, `expand_appearance_of_a_warped_image_draws_as_the_canvas_did`, `a_colorized_image_keeps_its_color_when_warped`, `a_warp_on_a_group_bends_the_image_in_it`, `envelope_distort::a_colorized_image_keeps_its_color_in_an_envelope`). Scores and hours unchanged.
- **`.ai` files keep an image's Warp live:** an embedded image whose appearance is one Warp effect, with no opacity mask, is saved to the `.ai` editing data with an art style of that Warp: its bent pieces are its drawn look, and the image itself follows on `%_` lines, as for other styled objects. Opening an `.ai` file reads such an image as the image with its Warp live, its pixels, placement, name and opacity kept, when the live warp draws as the file's drawn look and the import has a comparison left; otherwise the drawn look stays, with the drawn-look note. The Warp effect applies to bitmap images ([the help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html)), and native files keep live effects ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/supported-file-formats-in-illustrator.html)). Images with an opacity mask, and images with a Warp and another effect or with another geometry effect, are saved and opened as their drawn art. Existing `.ai` files with an embedded image on `%_` lines open differently: before, that image also came in beside its drawn look. Which images keep the Warp and the comparison are VectorCraft's own rules (eps `ai_editing::tests::an_image_with_a_warp_reads_back_with_its_warp_live`, `an_image_whose_warp_draws_otherwise_than_its_look_keeps_its_look`, `an_image_on_hidden_lines_does_not_come_in_beside_its_look`, `other_warped_images_are_written_as_their_drawn_art`, `import::ai::read::ops::tests::an_image_takes_a_style_of_one_warp_alone`). Scores and hours unchanged.
- **`.ai` saves count images with a Warp against the envelope limit:** opening an `.ai` file compares at most 256 envelopes and images with a Warp with their drawn art, and saving the `.ai` editing data now counts both against those 256, in the order they are written. An image with a Warp past them is saved as its drawn look, without its Warp, and an envelope past them as its distorted art, each with a warning on saving. An image with a Warp whose pixels can't be read is saved as its gray outline and is not counted. Before, saving counted only envelopes, so images with a Warp were saved live past the 256 without a warning, and the file opened with the ones past them as their drawn art. The limit of 256 is VectorCraft's own (eps `ai_editing::tests::warped_images_past_the_readers_checks_are_written_as_their_drawn_look`, `envelopes_get_the_checks_warped_images_left`, `a_warped_image_written_as_its_outline_takes_no_check`, `an_unreadable_warped_image_past_the_checks_is_warned_about_its_pixels`). Scores and hours unchanged.
- **`.ai` files keep a linked image's Warp live and its link:** a linked image whose appearance is one Warp effect, with no opacity mask, is saved to the `.ai` editing data with an art style of that Warp, as an embedded one is: its bent pieces are its drawn look, and the image itself follows on `%_` lines as a placed file with its path, its folder relative to the document's and its preview. Before, it was saved as its drawn art and its live Warp was lost. Opening the file reads it as the linked image with its Warp live, its link, pixels, placement, name, opacity, hide and lock kept, under the same drawn-look comparison; a hidden image with a Warp, embedded or linked, is now compared with its look drawn shown, and keeps its Warp too. A linked image with a Warp in an opacity mask's art keeps its Warp and is embedded, with the existing warning for linked images in masks. Linked files stay connected to their source and track it by relative path ([the help page](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/manage-linked-and-embedded-files/links-panel-overview.html)); the placed-file form is the one the `.ai` reader reads (`crates/eps/src/import/ai/read/art.rs` `end_place`). eps `ai_editing::tests::a_linked_image_with_a_warp_reads_back_linked_with_its_warp_live`, `a_large_linked_image_with_a_warp_reads_back_from_its_preview`, `a_hidden_or_locked_image_with_a_warp_keeps_its_state`, `a_linked_image_with_a_warp_in_a_mask_is_embedded_with_its_warp_live`. Scores and hours unchanged.
- **EPS writes a repeated image's samples once:** an image drawn more than once in an EPS file (the pieces of an envelope or a Warp effect, copies of one image, or one image in a symbol's instances) has its compressed samples written once in the setup, and each placement reads them from there: a reusable stream rewound for each placement at Language Level 3 ([PostScript Language Reference, 3rd ed., §3.13](https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf)), and an array of strings of at most 49,152 bytes fed to the image by a procedure at Level 2. An image drawn once is written where it is drawn, as before. The 256 MB limit on the page counts the samples once, so an envelope of a large photo no longer loses pieces to it. A colorized image is reused per ink (eps `tests::an_image_placed_twice_writes_its_samples_once_at_both_levels`, `tests::an_envelopes_image_pieces_draw_one_copy_of_its_samples`, `tests::each_ink_of_a_colorized_image_placed_twice_is_written_once`, `tests::samples_longer_than_a_string_are_reused_at_both_levels`, `tests::the_page_limit_counts_a_repeated_images_samples_once`). Scores and hours unchanged.
- **`.ai` files write a warped image's pieces as the parts they show:** in the `.ai` editing data and in legacy `.ai` files, an embedded image cut to a clipping path (in the editing data, also an image under an opacity mask that clips) is written with all of its pixels the first time, and every later copy of the same pixels is written as the rectangle of them that the path or mask shows, 2 pixels more on each side, placed where it lies in the image. The pieces of a Warp effect, an envelope or their expanded art, 200 copies of the image for 10 × 10 cells before, now hold about 7 times the image's pixels in all for a 64 × 64 image. A copy whose opacity mask is left out, nested too deep or past the object limit, keeps all of its pixels. Each image's pixels are read once for the pieces that follow one another. An image cut to a rectangle of half its pixels or more, and the first copy written, keep all of their pixels. The `XI` operator's arguments give an image's location and size ([AI file-format specification, §8.1](https://web.archive.org/web/20050126105313/https://partners.adobe.com/public/developer/en/illustrator/sdk/AI7FileFormat.pdf)); the 2-pixel margin and the half-the-pixels limit are VectorCraft's own rules (eps `ai_editing::tests::the_pieces_of_a_warped_image_are_written_as_the_parts_they_show`, `masked_pieces_of_a_warped_image_are_written_as_the_parts_they_show`, `an_image_with_a_live_warp_writes_its_pixels_whole_once`, `a_clipped_image_written_first_keeps_all_of_its_pixels`, `a_masked_copy_whose_mask_is_left_out_keeps_all_of_its_pixels`, `a_masked_copy_nested_too_deep_for_its_mask_keeps_all_of_its_pixels`, `legacy_ai::tests::the_pieces_of_a_warped_image_are_written_as_the_parts_they_show`). Scores and hours unchanged.
@@ -1726,7 +1727,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
SVG import reads each `<circle>` and `<ellipse>` as the ellipse VectorCraft draws: four smooth anchors on the axes, starting at the right, with handles of the standard length, so an ellipse exported as `<ellipse>` comes back with the anchors it was written from and draws the same (`circles_and_ellipses_import_as_the_ellipse_drawn` in `crates/svg/tests/it/basic_shapes.rs`, `filled_ellipse_on_the_artboard_edge_survives_visually` in `prop_svg.rs`).
SVG import keeps attributes in other applications' XML namespaces (`plotter:pen="2"`) on the objects and layers they belong to, as it keeps their `data-*` attributes: copies, duplicates and `.vectorcraft` files keep them, and SVG export writes them back with each namespace declared once on the root (the rest of #522; `crates/svg/tests/it/foreign_attributes.rs`, `tests/attributes.rs`).
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps defines every symbol in its editing copy with its art, registration point and place in the Symbols panel's order, and writes its instances as symbol instances, which VectorCraft's `.ai` reader opens as symbols and instances again (`symbols_and_their_instances_are_read_back` in `crates/eps/src/ai_editing/tests.rs`; [file-format-parity.md](file-format-parity.md) lists the instances written as their art).
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps keeps envelopes made with a warp, a mesh or a top object editable: its editing copy holds each envelope's contents, shape, Fidelity, Envelope Options and Edit Contents state in the form VectorCraft's `.ai` envelope reader reads, beside its distorted art, so the file opens with the same live envelope, Edit Contents still switching between the contents and the envelope ([Edit the contents of envelopes](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/edit-the-contents-of-envelopes.html)). Envelopes of type, envelopes in opacity masks, pattern tiles or another envelope's contents, and envelopes the reader would open with another shape or other settings (a Fidelity that isn't a whole number, bounds margins kept with Distort Appearance off), and envelopes after the first 256 in a file are written as their distorted art, with a warning (`envelopes_are_read_back_live` and `envelopes_the_reader_cannot_open_live_are_written_as_their_art` in `crates/eps/src/ai_editing/tests.rs`).
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps keeps envelopes made with a warp, a mesh or a top object editable: its editing copy holds each envelope's contents, shape, Fidelity, Envelope Options and Edit Contents state in the form VectorCraft's `.ai` envelope reader reads, beside its distorted art, so the file opens with the same live envelope, Edit Contents still switching between the contents and the envelope ([Edit the contents of envelopes](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/edit-the-contents-of-envelopes.html)). Envelopes of type, envelopes in opacity masks, pattern tiles or another envelope's contents, and envelopes the reader would open with another shape or other settings (a Fidelity that isn't a whole number, bounds margins kept with Distort Appearance off), and envelopes after the first 256 envelopes and images with a Warp in a file are written as their distorted art, with a warning (`envelopes_are_read_back_live` and `envelopes_the_reader_cannot_open_live_are_written_as_their_art` in `crates/eps/src/ai_editing/tests.rs`).
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps writes eligible paths, compound paths and groups with live effects with an art style when the writer supports their appearance and effects (not a filled path that uses the even-odd rule, and none on a large canvas), which VectorCraft's `.ai` reader opens as the same appearance with its effects live (`objects_with_live_effects_are_read_back` and `every_effect_setting_written_on_an_object_is_read_back` in `crates/eps/src/ai_editing/tests.rs`; [file-format-parity.md](file-format-parity.md) lists the objects written without one and the effects they lose).
The same holds for the objects of an opacity mask's art, a mask inside another mask's art and a hidden art root included, so a mask whose art has Offset Path opens drawing the image it masks as it did, with the effect live; geometry effects that no art style holds on a mask's art, such as Roughen, are written applied to the art, which opens drawing what it drew (`a_masked_image_with_live_offset_path_round_trips`, `hidden_and_nested_mask_art_keep_live_offset_path` and `a_masks_art_with_an_effect_no_style_holds_is_written_as_it_draws` in `crates/eps/src/ai_editing/tests.rs`).
A PDF-compatible `.ai` saved with Include Editing Data for Other Apps writes point type, area type and type on a path as type in its editing copy, and objects next to it keep their own transparency (`crates/eps/src/ai_editing/tests.rs`, `cmd::fileio::tests_pdfedit::type_comes_back_from_the_editing_copy`; [file-format-parity.md](file-format-parity.md) lists what is written as outlines).