diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index 78058a7f9..28f67f215 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -7377,3 +7377,65 @@ fn bindings_and_values_the_reader_cant_take_are_left_out() { assert_eq!(back.variables.datasets[1].values.get("picture"), None); assert_eq!(bound_objects(&back).len(), 4); } + +/// Type in a variable font's named instance names the instance's place in the font's design space +/// (`/4`, each axis as 16.16 fixed) and the font's version (`/5`) in its font record, so the app that opens the file doesn't take +/// the font's default instance (#6388); a run's own Variable Font setting is a font of its own, and +/// a static font's record has no design vector. Each comes back as the style it was set in. +#[test] +fn variable_font_instances_name_their_design_space() { + use vectorcraft_doc::{CharStyle, TextObject}; + use vectorcraft_text::test_fonts::{VARIABLE_FAMILY, variable_font}; + vectorcraft_text::FontDb::global().add_font(variable_font().unwrap()); + let set = |doc: &mut Document, y: f64, style: &str, axes: &[(&str, f64)]| { + let st = CharStyle { + font_family: VARIABLE_FAMILY.into(), + font_style: style.into(), + font_axes: axes.iter().map(|(t, v)| (t.to_string(), *v)).collect(), + ..style_of(12.0) + }; + let n = text_node(doc, TextObject::point(vectorcraft_geom::Point::new(10.0, y), style, st)); + let layer = doc.layers[0].id; + doc.insert(Some(layer), usize::MAX, n).unwrap(); + }; + let mut doc = Document::new(300.0, 200.0); + set(&mut doc, 20.0, "Regular", &[]); + set(&mut doc, 40.0, "Bold", &[]); + set(&mut doc, 60.0, "Bold", &[("wght", 650.0)]); + let static_text = TextObject::point(vectorcraft_geom::Point::new(10.0, 80.0), "Static", style_of(12.0)); + let n = text_node(&mut doc, static_text); + let layer = doc.layers[0].id; + doc.insert(Some(layer), usize::MAX, n).unwrap(); + + let td = text_document(&program(&doc)); + // 400 × 65536, 700 × 65536 and 650 × 65536. + assert!(td.contains("VaritestSans-Regular) /2 1 /4 [ 26214400 ]"), "{td}"); + assert!(td.contains("VaritestSans-Bold) /2 1 /4 [ 45875200 ]"), "{td}"); + assert!(td.contains("VaritestSans-Bold) /2 1 /4 [ 42598400 ]"), "a setting of its own is a font of its own: {td}"); + let static_name = vectorcraft_text::FontDb::global().face(&style_of(12.0).font_family, "Regular").unwrap().postscript_name(); + assert!(td.contains(&format!("{static_name}) /2 1 >>")), "a static font has no design vector: {td}"); + // Each variable font's record names the font's version (name ID 5) after its design vector. + // (The string is UTF-16 after a byte order mark, so the match starts after it.) + assert_eq!(td.matches("] /5 (").count(), 3, "{td}"); + assert_eq!(td.matches("Version 1.000) >>").count(), 3, "{td}"); + assert_eq!(td.matches("/CoolTypeFont").count(), 5, "four fonts and the hidden characters' font"); + + let back = read_back(&doc); + let styles: Vec<(String, String)> = back.layers[0] + .children() + .unwrap() + .iter() + .filter_map(|n| match &n.kind { + NodeKind::Text(t) => Some((t.runs[0].style.font_family.clone(), t.runs[0].style.font_style.clone())), + _ => None, + }) + .collect(); + assert_eq!( + styles[..3], + [(VARIABLE_FAMILY.into(), "Regular".into()), (VARIABLE_FAMILY.into(), "Bold".into()), (VARIABLE_FAMILY.into(), "Bold".into())] + ); +} + +fn style_of(size: f64) -> vectorcraft_doc::CharStyle { + style(size, Color::BLACK) +} diff --git a/crates/eps/src/ai_editing/writer/texts.rs b/crates/eps/src/ai_editing/writer/texts.rs index a84fc5720..1e02941c3 100644 --- a/crates/eps/src/ai_editing/writer/texts.rs +++ b/crates/eps/src/ai_editing/writer/texts.rs @@ -417,26 +417,61 @@ fn baseline_direction(st: &CharStyle) -> i64 { } } -/// The name the font of style `st` is written under, and whether it's TrueType: an installed -/// face's PostScript name; else its family and style as `family-style` (the family alone for -/// Regular), the form the reader's `family_style` splits. -fn font_name(st: &CharStyle) -> (String, bool) { +/// A font of the font set, as its `CoolTypeFont` record names it. +#[derive(Clone, Debug, PartialEq)] +struct FontRecord { + /// The PostScript name (`/0`). + name: String, + /// Whether its outlines are TrueType (`/2`). + truetype: bool, + /// For a variable font, where in its design space the type is set (`/4`): each `fvar` axis in + /// the font's order, in the axis's own units. Empty for a static font, which has none: without + /// it, an app that opens the file takes a variable font's default instance (#6388). + design: Vec, + /// For a variable font, its version string (`/5`, name ID 5); empty for a static font. + version: String, +} + +/// The font the font set names for style `st`: an installed face's PostScript name, and for a +/// variable font the setting of each of its axes (the named instance's, or the style's own +/// Variable Font setting, clamped to the axis as layout clamps it) and its version; else its family +/// and style as `family-style` (the family alone for Regular), the form the reader's +/// `family_style` splits. +fn font_name(st: &CharStyle) -> FontRecord { let db = vectorcraft_text::FontDb::global(); // The face of that family and style (the font database gives a fallback face for others). + let record = |name: String| FontRecord { name, truetype: false, design: vec![], version: String::new() }; match db .face_version(&st.font_family, &st.font_style, st.font_version.as_deref()) .filter(|f| f.family == st.font_family && f.style == st.font_style) { - Some(face) => (face.postscript_name(), face.is_truetype()), - None if st.font_style.is_empty() || st.font_style == "Regular" => (st.font_family.clone(), false), - None => (format!("{}-{}", st.font_family, st.font_style.replace(' ', "")), false), + Some(face) => { + let axes = face.axes(); + let design: Vec = axes + .iter() + .map(|a| { + let own = face.axis_value(a.tag).unwrap_or(a.default); + let set = st.font_axes.get(String::from_utf8_lossy(&a.tag).as_ref()).copied().filter(|v| v.is_finite()); + set.map_or(own, |v| (v as f32).clamp(a.min.min(a.max), a.max.max(a.min))) + }) + .collect(); + let version = if design.is_empty() { String::new() } else { face.version.clone() }; + FontRecord { name: face.postscript_name(), truetype: face.is_truetype(), design, version } + } + None if st.font_style.is_empty() || st.font_style == "Regular" => record(st.font_family.clone()), + None => record(format!("{}-{}", st.font_family, st.font_style.replace(' ', ""))), } } +/// `v` as the 16.16 fixed-point number a design vector holds (`wght` 700 is 45875200). +fn fixed(v: f32) -> i64 { + if v.is_finite() { (f64::from(v) * 65536.0).round() as i64 } else { 0 } +} + /// Does the font of style `st` come back as itself: the family and style the reader makes of the /// name it is written under (as its `char_style` does)? fn font_kept(st: &CharStyle) -> bool { - let (family, style) = crate::family_style(&font_name(st).0); + let (family, style) = crate::family_style(&font_name(st).name); let family = vectorcraft_text::FontDb::global().find_family(&family).unwrap_or(family); family == st.font_family && style == st.font_style } @@ -507,8 +542,8 @@ fn place_on(segments: &[[f64; 8]], f: f64) -> f64 { /// The text document being written. #[derive(Default)] pub(in crate::ai_editing) struct TextDoc { - /// Each font's PostScript name and whether it's TrueType. - fonts: Vec<(String, bool)>, + /// The fonts the runs name, each once: two settings of one variable font are two fonts. + fonts: Vec, frames: Vec, stories: Vec, /// The story and frame each text object of a thread is written as. @@ -533,11 +568,15 @@ impl TextDoc { /// The index of the font of `st` in the font set. fn font(&mut self, st: &CharStyle) -> i64 { - let (name, truetype) = font_name(st); - let i = match self.fonts.iter().position(|(n, _)| *n == name) { + let font = font_name(st); + // Two installed versions of one font are two fonts, so each keeps its own `/5`. + let same = |f: &FontRecord| { + f.name == font.name && f.version == font.version && f.design.iter().map(|v| fixed(*v)).eq(font.design.iter().map(|v| fixed(*v))) + }; + let i = match self.fonts.iter().position(same) { Some(i) => i, None => { - self.fonts.push((name, truetype)); + self.fonts.push(font); self.fonts.len() - 1 } }; @@ -806,7 +845,9 @@ impl TextDoc { let total = bytes.saturating_add(added); let total_values = self.doc_values.saturating_add(values); // The font set: each name in UTF-16 with its dictionary round it. - let fonts: usize = self.fonts.iter().map(|(n, _)| n.encode_utf16().count() * 2 + 64).sum(); + // A design vector's numbers and a version string add to it. + let fonts: usize = + self.fonts.iter().map(|f| f.name.encode_utf16().count() * 2 + 64 + f.design.len() * 12 + f.version.encode_utf16().count() * 2).sum(); // ASCII85 writes five characters for every four bytes. if total.saturating_add(RESERVE).saturating_add(fonts).saturating_mul(5) / 4 <= crate::import::MAX_TEXT_DOCUMENT / 4 && total_values.saturating_add(RESERVE_VALUES) <= crate::import::MAX_TEXT_VALUES @@ -924,8 +965,17 @@ impl TextDoc { let mut fonts: Vec = self .fonts .iter() - .map(|(name, truetype)| { - dict(vec![(99, V::Name("CoolTypeFont")), (0, dict(vec![(0, V::Str(name.clone())), (2, V::Int(i64::from(*truetype)))]))]) + .map(|f| { + let mut font = vec![(0, V::Str(f.name.clone())), (2, V::Int(i64::from(f.truetype)))]; + // A variable font's place in its design space and its version (#6388); a static + // font has neither. + if !f.design.is_empty() { + font.push((4, V::List(f.design.iter().map(|v| V::Int(fixed(*v))).collect()))); + if !f.version.is_empty() { + font.push((5, V::Str(f.version.clone()))); + } + } + dict(vec![(99, V::Name("CoolTypeFont")), (0, dict(font))]) }) .collect(); fonts.push(dict(vec![(99, V::Name("CoolTypeFont")), (0, dict(vec![(0, V::Str(HIDDEN_GLYPH_FONT.into())), (2, V::Int(0))]))])); diff --git a/crates/text/src/test_fonts.rs b/crates/text/src/test_fonts.rs index cbb2c20d2..aab7f0e1f 100644 --- a/crates/text/src/test_fonts.rs +++ b/crates/text/src/test_fonts.rs @@ -59,7 +59,8 @@ pub const VARIABLE_WIDEN: f32 = 0.25; /// axis (400–700, default 400, the face itself being Regular) whose maximum makes the glyphs of /// [`VARIABLE_CHARS`] [`VARIABLE_WIDEN`] wider (outlines and advances, through `gvar`), with the /// named instances Normal (the default, the face Regular), SemiBold (550) and Bold (700), plus an instance without -/// a name and one named like another (`BOLD`), which style lists leave out. `None` when it can't +/// a name and one named like another (`BOLD`), which style lists leave out. Its version (name ID 5) is +/// `Version 1.000`. `None` when it can't /// be built. pub fn variable_font() -> Option> { const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf"); @@ -106,6 +107,7 @@ pub fn variable_font() -> Option> { (1, VARIABLE_FAMILY), (2, "Regular"), (4, "Varitest Sans Regular"), + (5, "Version 1.000"), (6, "VaritestSans-Regular"), (256, "Weight"), (257, "Normal"), diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index fe06c7d2d..810d1568b 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (`.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-10 · **Change:** minor (`.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 @@ -113,7 +113,10 @@ private editing data. VectorCraft: gutters and first baseline, threaded frames included) and type on a path are written as text: a text object for each, naming its story in the text document, with the characters, fonts, sizes, leading, tracking, baseline shift, scales, solid fills and strokes, capitals and paragraph alignment, indents, spacing and direction; other character - and paragraph attributes are left out with a warning. Each run's baseline direction is written in the form the reader + and paragraph attributes are left out with a warning. A font of the text document's font set that is a variable font + also names where in its design space the type is set (each `fvar` axis in the font's order, as 16.16 fixed: the named + instance's setting, or the run's own Variable Font setting) and its version string, so the type doesn't open at the + font's default instance (#6388); two settings of one font are two fonts of the set, and a static font names neither. Each run's baseline direction is written in the form the reader below reads it: 1 when Standard Vertical Roman Alignment is on, 2 when it is off and 3 for Tate-chu-yoko; the document's defaults and the normal character style carry 1. A run with both Tate-chu-yoko and Standard Vertical Roman Alignment is written as Tate-chu-yoko, with the type attributes warning. Type in opacity masks, pattern tiles, symbols and art cut to a @@ -337,6 +340,7 @@ Stop opacity on translated artboards remains outside this regression coverage. | Date | Change | Summary | |---|---|---| | 2026-11-10 | minor | `.ai` and EPS import: an envelope of type (a warp, a mesh or a top object around type) kept its drawn art, its type hidden, because the type is laid out from the text document only after the layers are read. It now waits for that: its text slots get their type, and where VectorCraft's own envelope of that type draws as the drawn art the file saves (the same check and budget as other envelopes, seen on white and on black), it opens live in the drawn art's place, its type editable in Edit Contents, and the hidden copy goes. As with other live envelopes, the layers with one are then compared with the page, which weighs the envelope as the art it draws (not as type laid out again, which the comparison allows more for), and when they don't draw as it the drawn art and the hidden type are put back. Layers used though they don't draw as the page (the editing data trusted, or the objects that differ coming in as the page's art) leave envelopes of type their drawn art and hidden type too. A `.ai` whose layers would open alone, without its PDF part, imports the PDF part to compare them with, and should they still not draw as it, or should no envelope of type open live, they open alone as before, with the envelopes' drawn art. Type in a font that isn't available, slots without type of their own (threaded, bound to a text or a visibility variable, a wrap object, type a look draws), envelopes in a symbol's or a pattern's art, and envelopes that draw differently keep their drawn art, with the note (#3179). Tests `tests_native::an_envelope_of_type_that_draws_as_its_drawn_art_opens_live`, `an_envelope_of_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_white_type_that_draws_apart_from_its_drawn_art_keeps_it`, `an_envelope_of_type_waits_on_the_page_of_layers_that_would_open_alone`, `an_envelope_of_type_that_draws_apart_from_the_page_does_not_open_live`, `trusted_layers_that_draw_apart_from_the_page_leave_an_envelope_of_type_its_drawn_art`, `layers_with_objects_that_come_in_as_the_page_art_leave_an_envelope_of_type_its_drawn_art`, `an_envelope_of_type_bound_to_a_visibility_variable_keeps_its_drawn_art` | +| 2026-11-10 | minor | `.ai` save: a variable font's record in the text document's font set names the type's place in the font's design space (`/4`, each `fvar` axis as 16.16 fixed: the named instance's setting, or the run's own Variable Font setting) and the font's version (`/5`). Without them, type in a named instance such as Bold opened at the font's default instance in the app that opens the file. Two settings of one font are now two fonts of the set; a static font's record is unchanged. The record's form comes from the public report in #6388. Test `ai_editing::tests::variable_font_instances_name_their_design_space` (#6388) | | 2026-11-10 | minor | `.ai` and EPS import: an envelope of type, which keeps its drawn art with its type hidden, now says so in the import notes ("type in an envelope comes in as the envelope's drawn art, not as editable type"); nothing said it before, while the other envelopes that keep their drawn art have a note. The type is still not editable in the envelope (part of #3179). Test `tests_native::envelope_options_keep_the_source_text_hidden_under_its_outlined_appearance` | | 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` | diff --git a/docs/gaps.md b/docs/gaps.md index 43fa83db3..ddc02a2d0 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G5: built-in Game Trails brush library; estimate 9–19 → 8–18 h; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: 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; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-10 · **Change:** minor (G9: `.ai` save names a variable font instance's design-space setting, #6388; previously minor: G5: built-in Game Trails brush library; estimate 9–19 → 8–18 h; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: 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; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -189,6 +189,8 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi ## G9 Illustrator `.ai` and EPS files from real users +- **Done (#6388, 2026-11-10):** a saved `.ai`'s text document names, for type in a variable font, where in the font's design space it is set (each axis, the named instance's or the run's own setting) and the font's version, so Montserrat Bold no longer opens as the font's default instance (Thin); two settings of one font are two fonts of the set. Test `ai_editing::tests::variable_font_instances_name_their_design_space`. The PDF part's font naming (#4548) remains with craft-libs. Estimate unchanged. + - **Narrowed (#6165, 2026-11-09):** editable PDF import preserves explicit ActualText once, with empty/nested spans, authored order and glyph advances covered by `crates/pdf/src/tests/actual_text.rs`. One replacement uses the first placed glyph's line/style; exact redistribution across lines/styles and general mixed-direction layout remain. Input is bounded to depth 128 and 64 KiB encoded per accepted replacement. This does not close G9's broader `.ai`/EPS gaps or change its estimate. - **Done (2026-11-09):** the fonts an EPS carries draw their glyphs instead of being set as type in an installed font: composite (Type 0) fonts with the 8/8 mapping, plain Type 42 fonts (characters from their glyph names or character map, widths from the font), CID-keyed fonts over CMaps (Type 0 and Type 2 CIDFonts, CIDFonts in a FontSet's Compact Font Format data, base-font CMaps, named CIDFonts over predefined Unicode CMaps set as type) and embedded Type 1 fonts; the show operators' spacing along the baseline is kept as tracking, `kshow` included. Each step is charged to the interpreter's budgets. Hand-written test files only (no corpus file uses these fonts); scores and estimates unchanged. This narrows G9.