.ai import: the live envelope and warped type check sees white art (#6412)

The check that keeps a live envelope or warped type only where it draws
as the art saved with it renders each on its own, over nothing, and
compared the pixels as seen on white. White art over nothing looks like
nothing on white, so white contents that drew somewhere else passed and
opened live in the wrong place.

Pixels now also differ when they look different seen on black, so such
art keeps its saved art, as colored art does. Opaque colored art is
compared as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jmverges
2026-11-10 07:59:02 +00:00
co-authored by Claude Opus 5.5
parent 8400af1065
commit 3b1b5c7d47
4 changed files with 63 additions and 6 deletions
+5 -5
View File
@@ -35,7 +35,7 @@ use vectorcraft_doc::{Appearance, Document, Effect, ImageBlob, LayerColor, Node,
use vectorcraft_geom::PathData;
use super::obj::Obj;
use crate::import::native::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, compare_scale, differ, differ_in_colour};
use crate::import::native::compare::{MAX_DIFFERENCE, MAX_SOLID_DIFFERENCE, compare_scale, differ, differ_in_colour, differ_on_black};
/// The plugin that writes envelopes.
pub(super) const PLUGIN: &str = "Adobe Deform Plugin"; // brand-ok: the plugin name the files use
@@ -325,9 +325,9 @@ fn alone(node: &Node, layer: NodeId, shared: &Shared<'_>) -> Document {
}
/// How `live` and `drawn`, drawn over `region` at `scale`, differ: the pixels that differ in
/// lightness or colour, those whose neighbours all differ too (where one draws something the other
/// doesn't, not an edge drawn a little apart), and the pixels either inks. `None` when they can't
/// be drawn.
/// lightness or colour, seen on white or on black (each is drawn on its own: white art shows only on
/// black), those whose neighbours all differ too (where one draws something the other doesn't, not
/// an edge drawn a little apart), and the pixels either inks. `None` when they can't be drawn.
fn differing(live: &Document, drawn: &Document, region: Rect, scale: f64) -> Option<(usize, usize, usize)> {
let x = vectorcraft_render::Renderer::new().render_region(live, region, scale, false);
let y = vectorcraft_render::Renderer::new().render_region(drawn, region, scale, false);
@@ -336,7 +336,7 @@ fn differing(live: &Document, drawn: &Document, region: Rect, scale: f64) -> Opt
}
let (w, h) = (x.width as usize, x.height as usize);
let (xs, ys) = (x.pixels.as_chunks::<4>().0, y.pixels.as_chunks::<4>().0);
let differs: Vec<bool> = xs.iter().zip(ys).map(|(p, q)| differ(p, q) || differ_in_colour(p, q)).collect();
let differs: Vec<bool> = xs.iter().zip(ys).map(|(p, q)| differ(p, q) || differ_in_colour(p, q) || differ_on_black(p, q)).collect();
let inked = xs.iter().zip(ys).filter(|(p, q)| p[3] > 8 || q[3] > 8).count();
let at = |i: usize, j: usize| differs.get(j * w + i).copied().unwrap_or(false);
let any = differs.iter().filter(|d| **d).count();
+47
View File
@@ -7826,6 +7826,23 @@ fn an_envelope_that_doesnt_come_out_as_drawn_keeps_its_drawn_art() {
assert_eq!(r.warnings, [super::envelope::DRAWN]);
}
/// The same, in white: white art seen on white looks like nothing, so the check sees it on black
/// too, and a white envelope that draws elsewhere keeps its drawn art.
#[test]
fn a_white_envelope_that_doesnt_come_out_as_drawn_keeps_its_drawn_art() {
use vectorcraft_doc::live::EnvelopeKind;
const WHITE: &str = "0 0 0 0 1 1 1 Xa\n";
let other = EnvelopeKind::Warp { style: "arc".into(), bend: -50.0, h: 0.0, v: 0.0, horizontal: true };
let content = envelope_content().replace(RED, WHITE);
let drawn = drawn_content(&other).replace(RED, WHITE);
let body = envelope_of(&straight_mesh(1, 1, (120.0, 60.0)), &warp_keys(1, 0.5, 0.0, 0.0, 0), &content, 1, &drawn);
let r = read_ok(&layer(&body));
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]);
}
/// A document of `n` warped text objects, with the art the live warp makes of each, in its look.
fn warped_type_doc(n: usize) -> (Document, Vec<super::envelope::WarpedLook>) {
use vectorcraft_doc::{Appearance, Effect, TextObject};
@@ -7871,6 +7888,36 @@ fn warped_type_past_the_budget_comes_in_as_its_look() {
assert_eq!(kept.extras().wrap, Some(wrap));
}
/// White warped type is checked as other type is: seen on white it looks like nothing, so the check
/// sees it on black too. Type whose live warp draws as its look stays type; type whose look is
/// warped the other way comes in as its look.
#[test]
fn white_warped_type_that_draws_elsewhere_comes_in_as_its_look() {
use vectorcraft_doc::{Appearance, CharStyle, Effect, TextObject};
let warp = |bend: f64| {
let mut appearance = Appearance::default();
appearance.effects.push(Effect { id: "warp.arc".into(), params: serde_json::json!({"bend": bend}), visible: true });
appearance
};
for (look_bend, replaced) in [(40.0, 0), (-40.0, 1)] {
let mut doc = Document::new(400.0, 200.0);
let mut layer = Node::layer(doc.alloc_id(), "Art", vectorcraft_doc::LayerColor::Preset(0));
let white = CharStyle { fill: Paint::solid(Color::WHITE), ..Default::default() };
let t = TextObject::point(kurbo::Point::new(20.0, 60.0), "TILT", white);
let mut text = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t)));
text.appearance = warp(40.0).into();
let mut other = text.clone();
other.appearance = warp(look_bend).into();
let mut look = vectorcraft_effects::expand_outlined(None, &other, None).unwrap();
look.id = doc.alloc_id();
let looks = vec![super::envelope::WarpedLook { slot: text.id, look, left_out: None }];
layer.children_mut().unwrap().push(Arc::new(text));
doc.layers = vec![Arc::new(layer)];
let warped = super::envelope::WarpedType { looks, checks: 1, ..Default::default() };
assert_eq!(warped.settle(&mut doc, Some, &Default::default(), &mut vec![]), replaced, "look bent {look_bend}");
}
}
/// The budget for warped type is what the envelopes left: their checks and the page's share they
/// spent, with the page they are held to.
#[test]
+9
View File
@@ -56,6 +56,15 @@ pub(in crate::import) fn differ(p: &[u8; 4], q: &[u8; 4]) -> bool {
lightness(p).abs_diff(lightness(q)) > SAME_PIXEL || (a.min(b) <= WHITE && a.abs_diff(b) > SAME_PIXEL)
}
/// Do two pixels look different seen on black: in lightness? An object drawn on its own and seen
/// only on white hides what it draws in white (white type over nothing looks like nothing): seen
/// on black too, that shows.
pub(in crate::import) fn differ_on_black(p: &[u8; 4], q: &[u8; 4]) -> bool {
// Premultiplied, a pixel is what it shows on black.
let on_black = |p: &[u8; 4]| [0, 1, 2].map(|i| u32::from(p[i]));
p != q && lightness(on_black(p)).abs_diff(lightness(on_black(q))) > SAME_PIXEL
}
/// How far apart two pixels' red, green or blue may be for them to be the same colour (a colour
/// and its CMYK equivalent stay within it; a red and a dark red don't).
const SAME_COLOUR: u32 = 64;
+2 -1
View File
@@ -1,6 +1,6 @@
# File format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (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-10 · **Change:** minor (`.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
@@ -327,6 +327,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
| Date | Change | Summary |
|---|---|---|
| 2026-11-10 | minor | `.ai` and EPS import: the check that keeps a live envelope or warped type only where it draws as the art saved with it renders each on its own, over nothing, and compared the pixels seen on white, where white art looks like nothing. White contents that drew somewhere else passed it and opened live in the wrong place (white warped type off its banner, in users' files). Pixels now also differ when they look different seen on black, so such art keeps its saved art, as colored art does. Tests `import::ai::tests::a_white_envelope_that_doesnt_come_out_as_drawn_keeps_its_drawn_art`, `white_warped_type_that_draws_elsewhere_comes_in_as_its_look` |
| 2026-11-10 | minor | PDF import: an image in a DeviceN space of one colorant opens as a Separation image does (#6314): a grayscale image of its tints colorized by its ink. One of two or more colorants that are all process plates (Cyan, Magenta, Yellow, Black, and the ink `None`) opens as a CMYK image, each colorant's tint in its plate and its Decode array applied (8 or 16 bits a sample; one that can't be read, such as a JPEG, opens in RGB with the CMYK images' warning). Both used to open as RGB images, losing their inks. A DeviceN image that mixes a spot ink with other colorants, such as a duotone, still opens as RGB: an image holds one ink or the process plates, not both (#6348). Tests `colorize::a_devicen_image_of_one_ink_opens_colorized_by_it`, `colorize::a_devicen_image_of_process_plates_opens_in_cmyk`, `colorize::a_duotone_devicen_image_still_opens_as_rgb` |
| 2026-11-09 | minor | `.ai` import: an image whose color space `XC` sets to one ink (`n [c m y k /DeviceCMYK (name) /Separation] XC`, or an RGB alternate) opened as a gray RGB image: its samples, the ink's tints (255 at 100 %), were read as gray levels, so the tints drew inverted and the ink was lost. It now opens as a grayscale image of the tints, black at 100 %, colorized by the ink at 100 %: a spot swatch, a process plate's CMYK color (no swatch of its name), or Registration; the ink's color counts toward the document's color mode as a fill's does. Its alpha channel becomes its opacity mask, a reduced large image's too: a colorized image reduced to fit the limits (an image mask included) now keeps its alpha as an opacity mask rather than in its pixels, as at its full size. 8-bit and 1-bit samples, binary or in hex comment lines. A device space (`/DeviceGray XC` after a linked image's file name) and the ink `None` change nothing, as before; linked images (`XF`) keep their placeholder. Tests `import::ai::tests::separation_images_are_colorized_with_their_ink`, `separation_images_of_process_plates_and_registration`, `a_separation_image_with_hex_comment_lines_keeps_its_ink`, `a_reduced_separation_image_keeps_its_ink_and_its_alpha_as_a_mask` (#6350) |
| 2026-11-09 | minor | PDF import: an image in a Separation color space (one ink, ISO 32000-1 §8.6.6.4) opens as a grayscale image of its tints (white where it lays no ink, black at 100 %, its Decode array applied; 1, 2, 4, 8 and 16 bits a sample, or one gray channel from its JPEG, CCITT, JBIG2 or JPEG 2000 decoder) colorized by its ink at 100 %: a spot ink's swatch, a process plate's CMYK color, or Registration. It used to open as an RGB image, losing the ink, and Fill and Recolor Artwork didn't reach its color. Its soft mask or mask becomes its opacity mask, as a CMYK image's does. The ink is found by the image's color space (kind, colorant name and tint transform), once per image. As with any colorized image, where it lays no ink the art under it shows, where the RGB image was opaque white. The ink `None`, which paints nothing, an ink whose color can't be told, an image inside an alpha soft mask, inline images and DeviceN images open as before; samples that can't be read (data cut short, a decoder's output of another layout) open in RGB with a warning, and the ink gets no swatch. Tests `colorize::a_separation_image_opens_colorized_by_its_ink`, `colorize::a_separation_image_reads_its_samples_and_decode_array`, `colorize::a_separation_image_of_a_process_plate_or_of_no_ink`, `colorize::a_separation_image_keeps_its_soft_mask`, `colorize::a_separation_image_drawn_twice_is_colorized_twice`, `colorize::a_separation_jpeg_opens_colorized_by_its_ink`, `colorize::a_separation_image_that_cant_be_read_stays_rgb` (#6314) |