.ai import: an envelope in Edit Contents opens on its drawn art when its mesh lies away by a move (#6570)

While an envelope's contents are edited, the editing data keeps its mesh
in the envelope's dictionary. In some users' files that mesh lies away
from the drawn art by a plain move, the contents sitting with the drawn
art, as if the mesh hadn't followed the envelope when it moved. The
envelope then drew its contents elsewhere, failed the drawn-art check
and kept its drawn art. Nothing in the dictionary records the move, and
no envelope saved outside Edit Contents was found displaced.

A mesh or top object saved in Edit Contents (and a warp of type tried
on its saved grid) whose mesh draws the contents as large as the drawn
art, within half a point over width and height together, but further
from it than that, is now tried again after every other way, with one
more of the import's checks, its mesh moved by as far as the drawn
art's box lies from the box of what it draws. The drawn-art check, the
budget and the page comparison decide as before, so an envelope that
opened live before opens the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jmverges
2026-11-11 10:39:39 +00:00
co-authored by Claude Opus 5.5
parent 6244ac54bf
commit 112fe53d88
6 changed files with 195 additions and 10 deletions
+61 -6
View File
@@ -19,10 +19,12 @@
//! (`AIDeformArtWidthKey`, `AIDeformArtHeightKey`), the contents (`X=` … `X+`, then
//! `(AIDeformArtKey)`) and the kind (`AIDeformStyle`: 1 a warp, 2 a mesh, 3 a top object). Then the
//! distorted art on plain lines, and the plugin's `XP`. While the contents are edited, the group
//! holds the contents and the dictionary the mesh. A top object's outline is the edge of its
//! one-patch mesh. A warp's mesh is a grid of its shape: a warp of type whose own warp doesn't come
//! out as drawn opens as a mesh envelope on that grid where that does. An envelope whose parts don't
//! read, or that doesn't come out as drawn, keeps its drawn art.
//! holds the contents and the dictionary the mesh, which in some files lies away from the drawn art
//! by a move, as if it hadn't followed the envelope: such a mesh or top object whose grid draws its
//! contents as large as the drawn art but elsewhere is tried again moved onto that art. A top
//! object's outline is the edge of its one-patch mesh. A warp's mesh is a grid of its shape: a warp
//! of type whose own warp doesn't come out as drawn opens as a mesh envelope on that grid where that
//! does. An envelope whose parts don't read, or that doesn't come out as drawn, keeps its drawn art.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::Arc;
@@ -62,6 +64,10 @@ const MAX_GRID: u32 = 50;
/// How far, in points over width and height together, a saved size may lie from the contents'
/// bounds with their strokes and still count as that box.
const SAVED_SIZE_SLACK: f64 = 0.5;
/// How far, in points over width and height together, what an envelope draws may differ in size
/// from its drawn art and still count as that art moved, and how far it must lie from it to count
/// as moved at all ([`moved_onto`]).
const MOVED_SLACK: f64 = 0.5;
/// The bits of the options word that are read: Distort Appearance, Distort Pattern Fills, Distort
/// Linear Gradients, Anti-Alias (set: off) and Preserve Shape Using (set: Clipping Mask).
const KNOWN_BITS: i64 = 0b111 | 1 << 16 | 1 << 17;
@@ -183,6 +189,40 @@ fn with_strokes(kind: &NodeKind) -> Option<NodeKind> {
Some(kind)
}
/// The kind of `envelope`, a mesh or a top object in Edit Contents, with its grid moved onto
/// `drawn`, the drawn art the file saves for it, where what it draws is as large as that art (within
/// [`MOVED_SLACK`]) but lies elsewhere (further than that): moved by as far as the drawn art's box
/// lies from the box of what it draws. While the contents are edited, the grid is kept in the
/// envelope's dictionary, and in some files it lies away from the drawn art by a move, the contents
/// with the drawn art, as if the grid hadn't followed the envelope when it moved. `None` outside
/// Edit Contents, for a warp (it maps from its contents' box, which lies with them), and where
/// nothing is drawn. Symbol instances and pattern fills in the contents are measured as they are,
/// not drawn out: where that makes the sizes differ, no move is tried.
pub(super) fn moved_onto(envelope: &Node, drawn: &Node) -> Option<NodeKind> {
let NodeKind::Envelope { kind, editing: true, .. } = &envelope.kind else { return None };
if matches!(**kind, EnvelopeKind::Warp { .. }) {
return None;
}
// What it draws, measured as its drawn art is: the pieces in a group with its own appearance.
let mut drew = envelope.clone();
drew.kind = NodeKind::Group { children: vectorcraft_render::expand_live(envelope).into_iter().map(Arc::new).collect(), clip: false };
let (from, to) = (drew.visual_bounds()?, drawn.visual_bounds()?);
let by = to.center() - from.center();
let resized = (from.width() - to.width()).abs() + (from.height() - to.height()).abs();
if !(resized <= MOVED_SLACK && by.x.abs() + by.y.abs() > MOVED_SLACK && by.x.is_finite() && by.y.is_finite()) {
return None;
}
let mut moved = envelope.kind.clone();
if let NodeKind::Envelope { kind, .. } = &mut moved {
match &mut **kind {
EnvelopeKind::Mesh { points, .. } => points.iter_mut().for_each(|p| *p += by),
EnvelopeKind::TopObject { path, .. } => path.transform(Affine::translate(by)),
EnvelopeKind::Warp { .. } => return None,
}
}
Some(moved)
}
/// A warp: its style, bend, distortions and orientation.
fn warp(o: &Obj) -> Option<EnvelopeKind> {
let style = whole(o, "DeformStyle")??;
@@ -552,7 +592,8 @@ impl WarpedType {
/// for it ([`live_on_saved_grid`]), which opens in its place where it does: its type stays
/// editable in that shape, and the warp's style and bend are lost. With Distort Appearance, each
/// is also checked mapping from its type's glyphs grown by half their strokes' weight
/// ([`with_strokes`]). Each
/// ([`with_strokes`]). In Edit Contents, a mesh or top object whose grid draws the type
/// elsewhere than its drawn art is checked last moved onto that art ([`moved_onto`]). Each
/// check uses one of the import's checks and, for an envelope that shows, adds to what the page
/// check allows ([`drawn_alike`]). Elsewhere, and where the type is set in a font that isn't
/// available, the drawn art and the hidden type stay.
@@ -634,7 +675,17 @@ impl WarpedType {
(plain.visible, plain.opacity) = (true, 1.0);
let page = self.page.filter(|_| shown.contains(&drawn));
let mut alike = None;
for (i, kind) in kinds.into_iter().enumerate() {
// In Edit Contents, a grid that draws the type elsewhere than its drawn art is tried again
// moved onto that art ([`moved_onto`]), after every other way.
let (mut tries, mut moved, mut again, mut i) = (kinds.into_iter(), vec![], false, 0);
loop {
let Some(kind) = tries.next() else {
if again || moved.is_empty() {
break;
}
(tries, again) = (std::mem::take(&mut moved).into_iter(), true);
continue;
};
// A second try is only made while more checks are left than there are envelopes of
// type and warped text objects still to come (some of which may need none): one
// envelope's tries don't use up the checks of the others.
@@ -642,6 +693,7 @@ impl WarpedType {
return Err(TypeKept::Budget);
}
self.checks -= 1;
i += 1;
let mut envelope = art.clone();
(envelope.id, envelope.kind) = (id, kind);
let shared = Shared { patterns: &doc.patterns, images: &doc.images };
@@ -651,6 +703,9 @@ impl WarpedType {
alike = Some(envelope);
break;
}
if !again && let Some(kind) = moved_onto(&live_one, &plain) {
moved.push(kind);
}
}
let envelope = alike.ok_or(TypeKept::Differs)?;
if doc.node(drawn).is_none() {
+12 -1
View File
@@ -913,7 +913,18 @@ impl<'a> Reader<'a> {
let mut plain = drawn.clone();
(plain.visible, plain.opacity) = (true, 1.0);
let page = self.page_in_doc();
if !crate::import::ai::envelope::drawn_alike(&shown, &plain, layer, &shared, page, &mut self.envelope_spent) {
let mut alike = crate::import::ai::envelope::drawn_alike(&shown, &plain, layer, &shared, page, &mut self.envelope_spent);
// In Edit Contents, a grid that draws the contents elsewhere than the drawn art is checked
// again moved onto that art.
if !alike
&& self.envelope_checks > 0
&& let Some(kind) = crate::import::ai::envelope::moved_onto(&shown, &plain)
{
self.envelope_checks -= 1;
(envelope.kind, shown.kind) = (kind.clone(), kind);
alike = crate::import::ai::envelope::drawn_alike(&shown, &plain, layer, &shared, page, &mut self.envelope_spent);
}
if !alike {
self.warn(crate::import::ai::envelope::DRAWN);
return;
}
+57
View File
@@ -7880,6 +7880,63 @@ fn an_envelope_whose_contents_are_edited_opens_live_in_edit_contents() {
assert_eq!(fill(&content[0]).color(), Some(Color::rgb(1.0, 0.0, 0.0)));
}
/// While an envelope's contents are edited, its mesh is kept in its dictionary, and in some files it
/// lies away from the drawn art by a move, the contents with the drawn art. Where the mesh draws the
/// contents as large as the drawn art but elsewhere, the envelope opens live with its mesh moved
/// onto that art; where it draws them at another size, it keeps its drawn art.
#[test]
fn an_envelope_in_edit_contents_whose_mesh_stayed_behind_opens_live_on_its_drawn_art() {
use kurbo::Shape as _;
use vectorcraft_doc::live::EnvelopeKind;
// The top object of `an_envelope_whose_contents_are_edited_opens_live_in_edit_contents`.
let at = |(i, j): (usize, usize)| (40.0 + i as f64 * 120.0, 20.0 + j as f64 * 60.0);
let handle = |p: (usize, usize), q: (usize, usize)| match (p, q) {
((0, 1), (1, 1)) => (70.0, 95.0),
((1, 1), (0, 1)) => (130.0, 95.0),
_ => {
let ((x, y), (u, v)) = (at(p), at(q));
(x + (u - x) / 3.0, y + (v - y) / 3.0)
}
};
let mesh = mesh_text(1, 1, &at, &handle, None);
let mut bp = kurbo::BezPath::new();
bp.move_to((40.0, 20.0));
bp.curve_to((70.0, 5.0), (130.0, 5.0), (160.0, 20.0));
bp.curve_to((160.0, 40.0), (160.0, 60.0), (160.0, 80.0));
bp.curve_to((120.0, 80.0), (80.0, 80.0), (40.0, 80.0));
bp.curve_to((40.0, 60.0), (40.0, 40.0), (40.0, 20.0));
bp.close_path();
let shaped = |a: Affine| EnvelopeKind::TopObject { path: vectorcraft_geom::PathData::from_bezpath(&(a * bp.clone())), corners: None };
let keys = ENVELOPE_KEYS.replace("0 /Bool (AIDeformEditModeKey)", "1 /Bool (AIDeformEditModeKey)");
// The contents and the drawn art lie 30 points right and 15 down of where the mesh in the
// dictionary draws them.
let moved = Affine::translate((30.0, 15.0));
let contents = Rect::new(70.0, 35.0, 190.0, 95.0);
let read_with = |drawn: &EnvelopeKind| {
let block = format!("0 g\n{mesh}");
let red = format!("{RED}{}f\n", rect_path(contents.x0, 100.0 - contents.y1, contents.x1, 100.0 - contents.y0));
read_ok(&layer(&envelope_parts(&red, &keys, &block, 3, &drawn_box(drawn, contents, contents))))
};
let r = read_with(&shaped(moved));
let a = art(&r.doc);
assert_eq!(a.len(), 1, "{a:#?}");
let (content, kind, _, editing, _) = live_envelope(&a[0]);
assert!(editing);
let EnvelopeKind::TopObject { path, .. } = kind else { panic!("{kind:?}") };
// Moved as far as the drawn art's box lies from the box of what the mesh draws.
let (got, want) = (path.to_bezpath().bounding_box(), (moved * bp.clone()).bounding_box());
assert!((got.origin() - want.origin()).hypot() < 0.01 && (got.size() - want.size()).to_vec2().hypot() < 1e-6, "{got:?} {want:?}");
assert_eq!(content.len(), 1, "{content:#?}");
assert_eq!(content[0].geometric_bounds(), Some(contents));
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
// Drawn art moved and a tenth larger isn't the mesh's art moved: it stays.
let r = read_with(&shaped(moved * Affine::scale_about(1.1, (100.0, 50.0))));
let a = art(&r.doc);
assert_eq!(a.len(), 1, "{a:#?}");
assert!(matches!(a[0].kind, NodeKind::Group { .. }), "{a:#?}");
assert_eq!(r.warnings, [super::envelope::DRAWN]);
}
/// An envelope that doesn't read (a warp style number past the list, a mesh envelope whose shape
/// isn't a mesh, one whose plugin never closes it) keeps its drawn art, with one note; the art
/// round it is read as ever.
+61
View File
@@ -5846,6 +5846,67 @@ fn a_warp_of_type_without_a_saved_grid_keeps_its_drawn_art() {
assert_eq!(super::tests::program_warnings(&r.warnings), [crate::import::ai::ENVELOPE_TYPE_DRAWN], "{:?}", r.warnings);
}
/// [`type_envelope_on`] while its contents are edited: the text object in the group before its
/// dictionary, and `shape` in its contents' block, where the editing data then saves the mesh.
fn type_envelope_editing(keys: &str, shape: &str, drawn: &str) -> String {
let hidden = |s: &str| -> String { s.lines().map(|l| format!("%_{l}\n")).collect() };
format!(
"{}{}%_X=\n{}%_X+\n%_ (AIDeformArtKey) ,\n%_1 /Int (AIDeformStyle) ,\n%_;\n%_\n%_9 () XW\nu\n{drawn}U\n9 () XW\n(Adobe Deform Plugin) 1 0 0 XP\n",
hidden(&format!("u\n{}U\n", text_object(0))),
hidden(&format!("/ArtDictionary :\n{}", keys.replace("0 /Bool (AIDeformEditModeKey)", "1 /Bool (AIDeformEditModeKey)"))),
hidden(&format!("0 g\n{shape}")),
)
}
/// While the contents of an envelope of type are edited, its grid is kept in its dictionary, and in
/// some files it lies away from the drawn art by a move, the type with the drawn art. A warp of type
/// whose saved grid draws the type as large as its drawn art but elsewhere opens live as a mesh
/// envelope on that grid moved onto the drawn art (#3179).
#[test]
fn a_warp_of_type_in_edit_contents_whose_grid_stayed_behind_opens_on_its_drawn_art() {
use vectorcraft_doc::live::EnvelopeKind;
let Some(family) = installed_family() else { return };
let doc = type_envelope_text(&family, None);
let mut text = made_envelope_type(&doc);
text.visible = true;
// The drawn art is the grid's moved 10 points left and 25 down.
let by = kurbo::Vec2::new(-10.0, 25.0);
let EnvelopeKind::Mesh { rows, cols, points, handles } = rising_kind() else { panic!("a mesh") };
let want: Vec<kurbo::Point> = points.iter().map(|p| *p + by).collect();
let kind = EnvelopeKind::Mesh { rows, cols, points: want.clone(), handles };
let options = vectorcraft_doc::live::EnvelopeOptions::NEW;
let shaped = Node::new(
vectorcraft_doc::NodeId(1_000_000),
NodeKind::Envelope {
content: vec![std::sync::Arc::new(text)],
kind: Box::new(kind),
fidelity: 50.0,
editing: false,
options,
frame: Box::default(),
},
);
let (drawn, page) = drawn_envelope(&shaped);
let art = native_with_text(
&layer("Art", true, &format!("{}\n{}", square(10, 10), type_envelope_editing(&arc_keys(0.3), &rising_grid(), &drawn))),
&doc,
);
let r = import(&eps(&format!("{}\n{page}", page_square(10, 10)), &art)).unwrap();
assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings);
let (envelopes, loose) = envelopes_and_type(&r.document);
assert_eq!(envelopes, [vec!["FLUX".to_string()]], "{:?}", r.warnings);
assert!(loose.is_empty(), "{loose:?}");
assert!(super::tests::program_warnings(&r.warnings).is_empty(), "{:?}", r.warnings);
let mut live = None;
r.document.walk(|n| {
if let NodeKind::Envelope { kind, editing, .. } = &n.kind {
live = Some(((**kind).clone(), *editing));
}
});
let Some((EnvelopeKind::Mesh { points, .. }, true)) = live else { panic!("not a mesh envelope in Edit Contents: {live:?}") };
assert!(points.iter().zip(&want).all(|(p, q)| (*p - *q).hypot() < 0.05), "{points:?}");
}
/// A warp of type tried again on its saved grid leaves a check for each envelope of type still to
/// come: with one check too few for both, the next envelope, whose first try opens it, gets it.
#[test]
+2 -1
View File
@@ -1,6 +1,6 @@
# File format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (`.ai` and EPS import, PDF import: tracking and manual kerning in horizontally scaled type take the Horizontal Scale, as the file's page spaces such type; `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke, #3179; `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid, #3179; `.ai` and EPS import: stroked type keeps its stroke's cap, join and miter limit from the text document, #6472; `.ai` save: type in a variable font's named instance names its design-space setting, #6388; `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-11 · **Change:** minor (`.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art, #6570; `.ai` and EPS import, PDF import: tracking and manual kerning in horizontally scaled type take the Horizontal Scale, as the file's page spaces such type; `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke, #3179; `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid, #3179; `.ai` and EPS import: stroked type keeps its stroke's cap, join and miter limit from the text document, #6472; `.ai` save: type in a variable font's named instance names its design-space setting, #6388; `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page, #3179; `.ai` and EPS import: a note for type in an envelope, #3179; `.ai` and EPS import: the live envelope and warped type check sees white art; PDF import: DeviceN images of one ink or of process plates keep their inks, #6348; `.ai` import: images in a spot ink's Separation space keep their ink, #6350; PDF import: Separation images keep their ink, #6314; PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; PDF import: image masks open colorized by the color they are painted in, #6313; EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **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
@@ -339,6 +339,7 @@ Stop opacity on translated artboards remains outside this regression coverage.
| Date | Change | Summary |
|---|---|---|
| 2026-11-11 | minor | `.ai` and EPS import: while an envelope's contents are edited, the editing data keeps its mesh (a mesh's, a top object's or a warp's grid) 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: the envelope drew its contents elsewhere and kept its drawn art. A mesh or top object saved in Edit Contents (or a warp of type tried on its saved grid) whose mesh draws the contents as large as the drawn art, within half a point over width and height together, but further from it than that, is now tried again, after every other way and with one more of the import's checks, with its mesh moved by as far as the drawn art's box lies from the box of what it draws; it opens live there where it then draws as the drawn art. In users' files, used locally and never committed, only envelopes saved in Edit Contents draw elsewhere than their drawn art, and nothing in their dictionaries says by how much they moved. Tests `ai::tests::an_envelope_in_edit_contents_whose_mesh_stayed_behind_opens_live_on_its_drawn_art`, `tests_native::a_warp_of_type_in_edit_contents_whose_grid_stayed_behind_opens_on_its_drawn_art` (#6570) |
| 2026-11-11 | minor | `.ai` and EPS import, PDF import, SVG export and `.ai` save: type with a Horizontal Scale other than 100% and tracking or manual kerning is spaced as a `.ai`'s own page spaces it. Layout now measures tracking and manual kerning in thousandths of the em as the scale draws it (see type-parity.md, same date); they were added at the full em, so such type drifted from the page letter by letter. A `.ai`'s page writes that type's character spacing as the tracking itself in a text space as wide as the scale, which PDF scales by it (ISO 32000-1 §9.4.4). In 224 users' files (used locally, never committed), 93 render differently. Of the 25 whose render moved by more than 1% of its pixels against the page, 23 came closer. The note on the type differing from the page shows a smaller share in 5 files (11.0% to 4.4% at most) and a larger one in 2 (by 0.2 to 0.3 points). 9 more envelopes of type open live (113 to 122 of 203, none fewer in any file), and no file switched between its layers and its PDF part. PDF import: the tracking of horizontally scaled type is the file's `Tc` in thousandths of its size (it was that times the scale), the glyphs where they were (test `tests::text_placement::character_spacing_in_horizontally_scaled_type_is_the_files_tc`). SVG export writes the letter spacing in thousandths of the font size it writes (the vertical scale), so type scaled uniformly keeps its spacing in a viewer (test `text_layout::spacing_in_uniformly_scaled_type_takes_the_scale`); tracked type scaled horizontally alone still differs a little there, as before. A `.ai` saved here writes the same values as before; VectorCraft now lays them out as the file's own PDF part draws them. Scores and estimates unchanged. |
| 2026-11-11 | minor | `.ai` and EPS import: stroked type in an envelope with Distort Appearance is 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, for a stroke on its appearance or on a group round it as well as its characters'; it was mapped from its bounds with strokes, which reach as far as a miter join may at a corner. Tests `ai::envelope::tests::stroked_type_is_tried_mapped_from_its_glyphs_grown_by_half_its_stroke`, `type_with_a_character_stroke_is_tried_mapped_from_its_bounds_with_strokes`, `a_stroke_on_a_group_of_type_grows_the_box_by_half_its_weight` (part of #3179) |
| 2026-11-11 | minor | `.ai` and EPS import: a warp of type (an envelope made with a warp around type) whose own warp, VectorCraft's, doesn't draw as its drawn art is tried again as a mesh envelope on the warp grid the file saves, with the same drawn-art check, budget and page comparison as #6421: where it passes, the type is editable and keeps the file's shape, and the warp's settings (style, bend) are lost. A second try is only made while more checks are left than there are envelopes of type and warped text objects still to come. Type with strokes in an envelope with Distort Appearance on is also tried mapped from its box with strokes, where the size the file saves for it doesn't already give that box (a rule chosen by measuring real files). An envelope's grid (warp, mesh or top object) now reads its way round from all its rows and columns, not its first row alone: a mesh bent far round (its end columns leaning more than halfway over) reads the way it runs, not turned a quarter round, and a grid symmetric about its middle reads the same wherever it is on the page. Tests `tests_native::a_warp_of_type_opens_as_a_mesh_on_its_saved_grid_and_keeps_its_shape_when_its_text_changes`, `a_warp_of_stroked_type_maps_from_its_bounds_with_strokes_where_its_drawn_art_does`, `a_warp_of_type_without_a_saved_grid_keeps_its_drawn_art`, `a_warp_of_type_tried_again_leaves_the_next_envelope_its_check`, `ai::tests::a_mesh_envelope_bent_far_round_opens_the_way_it_runs`, `a_mesh_envelope_bent_halfway_round_opens_the_same_way_wherever_it_is` (part of #3179) |
File diff suppressed because one or more lines are too long