From 1301192aebc5231af4daee3810e07a35195bc81a Mon Sep 17 00:00:00 2001 From: HassanKhan Date: Sun, 15 Nov 2026 10:10:41 +0500 Subject: [PATCH 1/3] PDF import: stroked type keeps its cap, join and miter limit (#7741) Glyph strokes reached the type's character style as paint and width only, so stroked PDF text (rendering modes 1, 2, 5, 6, and under ActualText) came in with butt caps, miter joins and a miter limit of 10. The text path now carries the graphics state's line cap, line join and miter limit, taken as paths take them, and glyphs stroked with other ones start a run of their own. --- crates/pdf/src/import.rs | 2 +- crates/pdf/src/import/builder.rs | 6 ++-- crates/pdf/src/import/state.rs | 9 +++++- crates/pdf/src/import_text.rs | 34 +++++++++++++++----- crates/pdf/src/tests/textstroke.rs | 51 ++++++++++++++++++++++++++++++ docs/file-format-parity.md | 5 ++- 6 files changed, 93 insertions(+), 14 deletions(-) diff --git a/crates/pdf/src/import.rs b/crates/pdf/src/import.rs index d978680d67..e5a5e2e382 100644 --- a/crates/pdf/src/import.rs +++ b/crates/pdf/src/import.rs @@ -27,7 +27,7 @@ use crate::import_scan::{FontNames, MAX_NESTING, Ocgs, Scan, TextOverprint, Walk use crate::import_shading::{ FunctionBudget, FunctionPaint, clipped, extend_clip, fold_stop_opacity, function_paint, gradient_shape, mesh_shading, shading_gradient, }; -use crate::import_text::{Families, LineFacts, Look, Placement, TextLine, Upright}; +use crate::import_text::{Families, LineFacts, Look, Placement, TextLine, TypeStroke, Upright}; use crate::{CropTo, ImportOptions, ImportReport, PdfError, TextAs}; mod actual_text; diff --git a/crates/pdf/src/import/builder.rs b/crates/pdf/src/import/builder.rs index 0b32054c7b..0d50c58f31 100644 --- a/crates/pdf/src/import/builder.rs +++ b/crates/pdf/src/import/builder.rs @@ -4,7 +4,7 @@ use super::entry::{ UNNAMED_CHARACTERS_NOTE, max_objects, }; use super::reshaped; -use super::state::{Builder, Frame, FrameKind, GlyphRun, Saved, bounds, grouped, line_width, mean_scale, paints, round3, same_stroke, stroke_layer}; +use super::state::{Builder, Frame, FrameKind, GlyphRun, Saved, bounds, grouped, mean_scale, paints, round3, same_stroke, stroke_layer, type_stroke}; use super::{ Affine, Appearance, AppearanceItem, Arc, BezPath, BfString, BlendMode, CacheKey, Color, Colors, Families, FillLayer, FillRule, FunctionBudget, FunctionPaint, HashMap, HashSet, ImageBlob, ImageObject, Knockout, Look, MaskSpec, MaskType, Native, Node, NodeId, NodeKind, OpacityMask, Paint, @@ -961,7 +961,7 @@ impl<'p> Builder<'p> { } None => (None, 1.0), }; - let stroke = stroke.zip(paint.clone()).map(|(p, paint)| (paint, line_width(p, scale))); + let stroke = stroke.zip(paint.clone()).map(|(p, paint)| type_stroke(paint, p, scale)); let look = Look { font: font_key, family: info.family, @@ -1109,7 +1109,7 @@ impl<'p> Builder<'p> { }; // What the glyph is drawn with as an outline, should its line be kept so. let drawn = paint.clone().map(|p| (p, stroke.cloned())); - let stroke = stroke.zip(paint.clone()).map(|(p, paint)| (paint, line_width(p, scale))); + let stroke = stroke.zip(paint.clone()).map(|(p, paint)| type_stroke(paint, p, scale)); let look = Look { font: key, family: info.family, diff --git a/crates/pdf/src/import/state.rs b/crates/pdf/src/import/state.rs index 12eb4498dc..7deb2bd73b 100644 --- a/crates/pdf/src/import/state.rs +++ b/crates/pdf/src/import/state.rs @@ -1,7 +1,7 @@ use super::cid::{CidText, FontInfo}; use super::{ Affine, Arc, BezPath, BlendMode, Colors, Dash, Families, FillRule, FontNames, FunctionBudget, HashMap, HashSet, ImageBlob, LineCap, LineFacts, - LineJoin, LumaData, MaskSpec, Node, NodeId, Paint, PathEl, PatternDef, Rect, Scan, StrokeLayer, StrokeProps, TextAs, TextLine, + LineJoin, LumaData, MaskSpec, Node, NodeId, Paint, PathEl, PatternDef, Rect, Scan, StrokeLayer, StrokeProps, TextAs, TextLine, TypeStroke, }; /// Decoded RGBA pixels, width, height and hayro's scale factors. @@ -277,6 +277,13 @@ pub(super) fn stroke_layer(paint: Paint, opacity: f32, p: &StrokeProps, scale: f st } +/// The stroke `p` draws type with in `paint` at `scale`: width, cap, join and miter limit as +/// [`stroke_layer`] takes them (#7741). +pub(super) fn type_stroke(paint: Paint, p: &StrokeProps, scale: f64) -> TypeStroke { + let st = stroke_layer(paint, 1.0, p, scale); + TypeStroke { paint: st.paint, width: st.width, cap: st.cap, join: st.join, miter_limit: st.miter_limit } +} + /// The most entries of a dash array kept (an even count keeps it a pattern of dashes and gaps). const MAX_DASH_ENTRIES: usize = 64; /// Most dash periods [`stroke_outline`] cuts a stroke into. diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs index 9b0122326f..7c4d0845d9 100644 --- a/crates/pdf/src/import_text.rs +++ b/crates/pdf/src/import_text.rs @@ -13,7 +13,7 @@ use std::collections::{HashMap, HashSet}; use kurbo::{Affine, BezPath, Point, Rect, Vec2}; use vectorcraft_color::Paint; -use vectorcraft_doc::{CharStyle, Node, ParaDirection, TextKind, TextObject, TextRun}; +use vectorcraft_doc::{CharStyle, LineCap, LineJoin, Node, ParaDirection, TextKind, TextObject, TextRun}; use vectorcraft_text::TextLayout; /// One glyph's placement: its baseline origin, advance direction, size and horizontal scale. @@ -70,8 +70,19 @@ pub(crate) struct Upright { pub top: Point, } +/// A stroke on type: its paint and width, and the cap, join and miter limit the file strokes the +/// glyphs with (#7741). +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct TypeStroke { + pub paint: Paint, + pub width: f64, + pub cap: LineCap, + pub join: LineJoin, + pub miter_limit: f64, +} + /// What a run of type is drawn with: its font (cache key, family and style), size, horizontal -/// scale, fill and stroke (paint and width). +/// scale, fill and stroke. #[derive(Clone, Debug, PartialEq)] pub(crate) struct Look { pub font: u128, @@ -82,7 +93,7 @@ pub(crate) struct Look { pub size: f64, pub h_scale: f64, pub fill: Option, - pub stroke: Option<(Paint, f64)>, + pub stroke: Option, /// The fill, the stroke overprints. pub overprint_fill: bool, pub overprint_stroke: bool, @@ -102,7 +113,11 @@ impl Look { } fn style(&self) -> CharStyle { - let (stroke, stroke_width) = self.stroke.clone().unwrap_or((Paint::None, 0.0)); + let default = CharStyle::default(); + let (stroke, stroke_width, stroke_cap, stroke_join, stroke_miter_limit) = match self.stroke.clone() { + Some(s) => (s.paint, s.width, s.cap, s.join, s.miter_limit), + None => (Paint::None, 0.0, default.stroke_cap, default.stroke_join, default.stroke_miter_limit), + }; CharStyle { font_family: self.family.clone(), font_style: self.style.clone(), @@ -112,9 +127,12 @@ impl Look { fill: self.fill.clone().unwrap_or(Paint::None), stroke, stroke_width, + stroke_cap, + stroke_join, + stroke_miter_limit, overprint_fill: self.overprint_fill, overprint_stroke: self.overprint_stroke, - ..CharStyle::default() + ..default } } } @@ -463,7 +481,7 @@ impl TextLine { /// The reader fills a whole Tj/TJ before replaying its glyphs as strokes. A replay /// must neither consume the replacement nor rewind the pen. Only the first glyph's /// style belongs to the replacement; suppressed glyphs have no text to restyle. - pub fn stroke_actual(&mut self, font: u128, at: Placement, stroke: (Paint, f64), overprint: bool) -> bool { + pub fn stroke_actual(&mut self, font: u128, at: Placement, stroke: TypeStroke, overprint: bool) -> bool { let key = (font, at.origin.x.to_bits(), at.origin.y.to_bits()); let Some(run) = self.actual_fills.remove(&key) else { return false }; if let Some((look, ..)) = run.and_then(|i| self.runs.get_mut(i)) { @@ -501,10 +519,10 @@ impl TextLine { Some(bp) } - /// A stroke (`stroke`: paint and width) over the glyph just drawn at `at` in `font` (fill + /// A stroke (`stroke`: paint, width, cap, join and miter limit) over the glyph just drawn at `at` in `font` (fill /// and stroke rendering), which `overprint`s or not: the run is stroked. `false`: it isn't /// that glyph. - pub fn stroke_last(&mut self, font: u128, at: Placement, stroke: (Paint, f64), overprint: bool) -> bool { + pub fn stroke_last(&mut self, font: u128, at: Placement, stroke: TypeStroke, overprint: bool) -> bool { let Some((look, ..)) = self.runs.last_mut().filter(|(l, ..)| l.font == font && (at.origin - self.last).hypot() < self.at.size * 1e-3) else { return false; }; diff --git a/crates/pdf/src/tests/textstroke.rs b/crates/pdf/src/tests/textstroke.rs index 2669f02844..c5496b53d5 100644 --- a/crates/pdf/src/tests/textstroke.rs +++ b/crates/pdf/src/tests/textstroke.rs @@ -55,3 +55,54 @@ fn an_aligned_character_stroke_is_clipped_to_its_side_of_the_glyphs() { assert!(ops.contains(&if clip { "W*" } else { "W" }), "{align:?}: clipped to its side"); } } + +/// The cap, join and miter limit a PDF strokes its type with, whatever the rendering mode that +/// strokes it (stroke, fill and stroke, and either with a clip), and with replacement text +/// (`ActualText`) too: not butt caps and miter joins (#7741). +#[test] +fn imported_stroked_type_keeps_the_files_cap_join_and_miter_limit() { + use vectorcraft_testkit::pdf::{PdfPage, pdf}; + let page = |content: &str| { + let resources = "/Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >>"; + pdf(&[PdfPage { resources: resources.into(), ..PdfPage::new(300.0, 200.0, content) }], None) + }; + let styles = |content: &str| { + let d = import(&page(content)).unwrap(); + let mut runs = vec![]; + d.walk(|n| { + if let NodeKind::Text(t) = &n.kind { + runs.extend(t.runs.iter().map(|r| r.style.clone())); + } + }); + runs + }; + let cases = [ + ("1 J 1 j", (LineCap::Round, LineJoin::Round, 10.0)), + ("2 J 2 j 3 M", (LineCap::Square, LineJoin::Bevel, 3.0)), + ("0 J 0 j 3 M", (LineCap::Butt, LineJoin::Miter, 3.0)), + ]; + for (settings, want) in cases { + for mode in ["1", "2", "5", "6"] { + // A word shown at once, a glyph at a time (a stroke over the glyph just filled), and + // in a span with replacement text. + for text in ["(WAVE) Tj", "(W) Tj (A) Tj", "/Span << /ActualText (WAVE) >> BDC (WAVE) Tj EMC"] { + let content = format!("0 g 0 0 0 RG 4 w {settings} BT {mode} Tr /F1 60 Tf 40 110 Td {text} 0 Tr ET"); + let runs = styles(&content); + let stroked: Vec<_> = runs.iter().filter(|s| !s.stroke.is_none()).collect(); + assert!(!stroked.is_empty(), "{content}: stroked type comes in: {runs:?}"); + if matches!(mode, "2" | "6") && !text.starts_with("(WAVE)") { + assert!(stroked.iter().any(|s| !s.fill.is_none()), "{content}: the stroke is over the filled glyphs: {runs:?}"); + } + for s in stroked { + assert!((s.stroke_width - 4.0).abs() < 1e-6, "{content}: width {}", s.stroke_width); + let got = (s.stroke_cap, s.stroke_join, s.stroke_miter_limit); + assert_eq!(got, want, "{content}"); + } + } + } + } + // Glyphs stroked with another join are a run of their own. + let runs = styles("0 0 0 RG 4 w BT 1 Tr /F1 60 Tf 40 110 Td 1 j (WA) Tj 0 j (VE) Tj 0 Tr ET"); + let joins: Vec<_> = runs.iter().map(|s| s.stroke_join).collect(); + assert_eq!(joins, [LineJoin::Round, LineJoin::Miter]); +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 06405fa620..34e73b99a1 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-13 · **Change:** minor (`.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; `.ai` and EPS import: a top-object envelope whose mesh saves its sides' curves in pieces draws its contents along each whole curve, as its drawn art does, #7097; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: `.ai` and EPS import: type from the text document, type a PostScript program sets and legacy `.ai` type are set in an installed face of their font's PostScript name, exact name first, in the version a text document's record keeps; the `.ai` save writes a static font's version when its name alone gives another copy, #6983; `.ai` and EPS import: type keeps the manual kerning of its text document, #6899; `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, its strokes' miter tips and strokes a Transform effect moves included, after its glyphs grown by half its strokes' weight, #6824; `.ai` and EPS import: type filled or stroked with a pattern swatch keeps its pattern, from the art style its text document paint names, #6856; #6865: explicitly requested oversized editing artboard; PDF import: Indic type keeps its marks on their line, and a line whose type would not draw as the file does keeps the file's glyphs as outlines, #5235; previously: newly imported SVG/SVGZ titles are filtered and bounded without changing restored titles, #3626; `.ai` and EPS import, and what every export and `.ai` save draw: type's strokes in an envelope paint at their own weight along its bent outlines, Distort Appearance on or off, #6644; `.ai` and EPS import: centred and right-aligned lines of tracked type are aligned without the tracking after their last character, as the file's page aligns them, #6610; `.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` save and open: layer colour presets Yellow, Magenta and Cyan keep their colour, as the `Lb` colour index now follows the file's numbering, #6502; `.ai` save: stroked type keeps its stroke's cap, join and miter limit in the text document, #6472; `.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-15 · **Change:** minor (PDF import: stroked type keeps the line cap, line join and miter limit the file strokes it with, not butt caps and miter joins, #7741; `.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; `.ai` and EPS import: a top-object envelope whose mesh saves its sides' curves in pieces draws its contents along each whole curve, as its drawn art does, #7097; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: `.ai` and EPS import: type from the text document, type a PostScript program sets and legacy `.ai` type are set in an installed face of their font's PostScript name, exact name first, in the version a text document's record keeps; the `.ai` save writes a static font's version when its name alone gives another copy, #6983; `.ai` and EPS import: type keeps the manual kerning of its text document, #6899; `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, its strokes' miter tips and strokes a Transform effect moves included, after its glyphs grown by half its strokes' weight, #6824; `.ai` and EPS import: type filled or stroked with a pattern swatch keeps its pattern, from the art style its text document paint names, #6856; #6865: explicitly requested oversized editing artboard; PDF import: Indic type keeps its marks on their line, and a line whose type would not draw as the file does keeps the file's glyphs as outlines, #5235; previously: newly imported SVG/SVGZ titles are filtered and bounded without changing restored titles, #3626; `.ai` and EPS import, and what every export and `.ai` save draw: type's strokes in an envelope paint at their own weight along its bent outlines, Distort Appearance on or off, #6644; `.ai` and EPS import: centred and right-aligned lines of tracked type are aligned without the tracking after their last character, as the file's page aligns them, #6610; `.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` save and open: layer colour presets Yellow, Magenta and Cyan keep their colour, as the `Lb` colour index now follows the file's numbering, #6502; `.ai` save: stroked type keeps its stroke's cap, join and miter limit in the text document, #6472; `.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) > > Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865). @@ -327,6 +327,8 @@ Stop opacity on translated artboards remains outside this regression coverage. - [x] Authored ordering is protected per stretch; ordinary stretches still reconstruct visual-order text. Outlines mode is unchanged. - [ ] Exact redistribution of one replacement over several lines/styles, and general mixed-direction paragraph layout. Replacement uses the first placed glyph's line/style. Tags deeper than 128 and replacements over 64 KiB encoded (at most 192 KiB decoded) fall back to glyph mappings with a warning; an outer accepted owner survives deeper tags. +- [x] Stroked type (text rendering modes 1, 2, 5 and 6, with or without `ActualText`) keeps the line cap, line join and miter limit of the graphics state it is stroked with, as paths do; glyphs stroked with another cap, join or miter limit are a run of their own (#7741, `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`). The dash array is not carried to the characters yet. + - [x] Place from bytes or unlinked paths, and PDF clipboard paste, keep the selected page's live text. Synthetic two-page regressions in `crates/engine/src/cmd/fileio/tests_pdffidelity.rs` cover both sources, page selection and explicit outlines. The single-page importer honors the same text choice as Open; existing PDF fallback rules are unchanged. ## Corpus download integrity @@ -337,6 +339,7 @@ Stop opacity on translated artboards remains outside this regression coverage. | Date | Change | Summary | |---|---|---| +| 2026-11-15 | minor | PDF import: stroked type (text rendering modes 1, 2, 5 and 6, and glyphs under `ActualText`) came in with butt caps, miter joins and a miter limit of 10 whatever the file stroked it with, while paths kept the graphics state's. The characters' stroke now takes the line cap (`J`), line join (`j`) and miter limit (`M`) of the graphics state, as a path's stroke does, and glyphs stroked with other ones start a run of their own (#7741). Test `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`. Scores and estimates unchanged. | | 2026-11-13 | minor | `.ai` and EPS import: layers that don't draw as the page, even with the drawn art of every live envelope put back, no longer put back the drawn art of every envelope of type. Each live envelope of type is weighed in the page check's own drawing of the pages it lies on, on the pixels that it and its drawn art ink there (the two drawn together on a layer of their own, without the rest of the layers; a pixel inks with an alpha above 8 of 255): it keeps its drawn art and its type, hidden, with the note, where the layers with it differ from the page in more of those pixels than its own check allows against its drawn art (10% of them, or 3% differing with all their neighbours, away from type), or where the envelopes of type that stay live, in the order they were read, would together differ there in more of a page than half of what the page check allows (a budget of the same size as the one their own checks share, kept apart from it). Art that differs elsewhere on the page no longer costs them their editable type; art that differs on their pixels still does. An envelope of type that a clip group or a clipping sublayer clips, an opacity mask masks, or a group with an appearance of its own, another envelope, a repeat or a blend holds keeps its drawn art as before, wherever it lies: drawn on its own it would be weighed elsewhere than it draws, and what holds it can draw it onto the page from off it. An envelope on no page stays live, as the page check doesn't see it, unless the page holds all the art that prints (an EPS's page that is its art's box, where art past the page is compared too): there one reaching past the page keeps its drawn art. Past 12 of them on the pages, all keep it. This holds where the editing data is trusted, where the objects that differ come in as the page's art (the placed files' other placement weighed on its own, as it draws them elsewhere under or over the envelopes; layers that draw as the page with only the envelopes the page holds put back are used as they are; where none of the objects that differ can then come in as the page's art, because an object holds type, the drawn art of every envelope of type is put back and that is tried again, as before), and for a `.ai` whose layers would open alone, which still opens alone: its envelopes of type are all settled again as they were with the page, in the same order and with the same checks, so each that opens live draws as it was weighed, and those the page held get their drawn art back. Where putting the drawn art of every live envelope back makes the layers draw as the page, it is still put back (#5065). In 224 users' files (used locally, never committed): envelopes of type that open live go from 174 to 186, in 8 files, none fewer in any file; no file changes between its layers and its PDF part; 7 renders change, by at most 0.7% of their pixels at 40 ppi (1 closer to the page, 1 alike, 5 further, by at most 0.7 of a percentage point, where VectorCraft's own envelope draws a little apart from the drawn art, within its check); the note for type in an envelope goes from 6 files. Of the 14 envelopes of type that had opened live and lost it to the page check, 2 keep their drawn art: one where the page draws something else where it is, and one in a file that comes in as its PDF part (#7141). Tests `build::candidate_tests::the_page_holds_back_only_envelopes_of_type_where_it_differs_from_them`, `envelopes_of_type_stay_live_within_half_of_what_the_page_check_allows`, `an_envelope_of_type_differing_in_more_of_its_pixels_than_its_check_allows_is_held_back`, `envelopes_of_type_the_layers_dont_draw_in_place_are_held_back`, `on_a_page_that_holds_all_the_art_an_envelope_of_type_past_it_is_held_back`, `objects_come_in_as_the_page_art_with_every_envelope_of_type_put_back_where_a_live_one_holds_a_difference`, `tests_native::layers_that_would_open_alone_keep_an_envelope_of_type_live_where_other_art_differs_from_the_page`, `layers_that_would_open_alone_keep_live_only_the_envelopes_of_type_the_page_doesnt_hold`, `trusted_layers_that_would_open_alone_open_alone_with_an_envelope_of_type_the_page_doesnt_hold_live`, `layers_with_objects_that_come_in_as_the_page_art_keep_an_envelope_of_type_that_draws_as_the_page_live`, `trusted_layers_keep_an_envelope_of_type_live_where_other_art_differs_from_the_page`, `trusted_layers_leave_an_envelope_of_type_the_page_draws_otherwise_its_drawn_art`, `build::candidate_tests::the_other_placement_of_placed_files_is_weighed_on_its_own`, `layers_that_draw_as_the_page_with_the_held_envelopes_of_type_put_back_are_used_so` | | 2026-11-13 | minor | PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke instead of being left out, adding no text (#6971; `empty_actual_text_keeps_the_glyphs_art` in `crates/pdf/src/tests/actual_text.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | `.ai` and EPS import, and what the canvas, every export and `.ai` save draw: a top-object envelope's mesh can save a side's curve in pieces. In users' files (used locally, never committed), 10 top objects save a grid of three patches across whose top and bottom edges are each one cubic split into thirds, and their drawn art follows the whole curve's parameter. VectorCraft gave each piece the share of its control polygon, so their type came out up to about half a point off the drawn art along the side, and one envelope of type kept its drawn art instead of opening live. A side whose pieces are all one curve split at anchors (each within 0.02 pt of that curve's piece) now follows that curve's parameter (#7097). Synthetic test: `a_top_object_whose_top_is_one_curve_split_in_thirds_opens_live`. Scores unchanged. | From 9dafc08f8f579dab851ba639fd884e6f7504abd0 Mon Sep 17 00:00:00 2001 From: HassanKhan Date: Sun, 15 Nov 2026 11:05:54 +0500 Subject: [PATCH 2/3] PDF import: a glyph stroked otherwise gets a run of its own (#7741) A filled glyph joins a run of the same fill whatever its stroke, so with fill and stroke rendering shown a glyph at a time, the stroke replayed over the last glyph restyled the whole run: glyphs before it took its cap, join and miter limit. When that stroke differs from the run's, the glyph's text now splits off into a run of its own; the line's text is unchanged, so slots, gaps and authored flags still line up. Also record the change in the parity and roadmap docs. --- crates/pdf/src/import_text.rs | 39 +++++++++++++++++++++++++++--- crates/pdf/src/tests/textstroke.rs | 16 ++++++++++++ docs/file-format-parity.md | 4 +-- docs/roadmap.md | 5 ++-- docs/target-app-parity.md | 6 +++-- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs index 7c4d0845d9..ec4fff092a 100644 --- a/crates/pdf/src/import_text.rs +++ b/crates/pdf/src/import_text.rs @@ -205,6 +205,9 @@ pub(crate) struct TextLine { /// Runs of glyphs drawn alike: their look, how far the file raised them off the line's /// baseline (points, up; text rise, `Ts`), their text. runs: Vec<(Look, f64, String)>, + /// Where the last glyph's text starts in the last run (bytes): a stroke over that glyph + /// alone restyles only its text ([`stroke_last`](Self::stroke_last)). + last_start: usize, /// The bytes of text the horizontal glyphs added to `runs` so far (counted as they come: a /// sum over the runs for each glyph is quadratic when every glyph is a run). text_bytes: usize, @@ -275,6 +278,7 @@ impl TextLine { last_size: at.size, baseline: vec![], runs: vec![], + last_start: 0, text_bytes: 0, slots: vec![], authored: vec![], @@ -321,6 +325,7 @@ impl TextLine { let size = self.at.size.max(at.size); let Some((last, _, run)) = self.runs.last_mut() else { self.runs.push((look.clone(), 0.0, text.to_string())); + self.last_start = 0; self.record_actual_fill(look, at, content); self.authored.extend(text.chars().map(|_| content.authored())); (self.upright, self.next, self.last) = (Some(upright), at.origin + down * at.size, at.origin); @@ -352,9 +357,11 @@ impl TextLine { && (matches!(content, TextContent::Pen) || (!matches!(content, TextContent::Actual(_)) && !self.authored.last().copied().unwrap_or(false))) { + self.last_start = run.len(); run.push_str(text); } else { self.runs.push((look.clone(), 0.0, text.to_string())); + self.last_start = 0; } self.record_actual_fill(look, at, content); self.authored.extend(text.chars().map(|_| content.authored())); @@ -443,12 +450,15 @@ impl TextLine { && (*last_shift - shift).abs() < 1e-3 && (pen || (!matches!(content, TextContent::Actual(_)) && !self.authored.last().copied().unwrap_or(false))) { + self.last_start = run.len(); run.push_str(text); } else { self.runs.push((look.clone(), shift, text.to_string())); + self.last_start = 0; } } else { self.runs.push((look.clone(), 0.0, text.to_string())); + self.last_start = 0; } self.record_actual_fill(look, at, content); self.text_bytes = self.text_bytes.saturating_add(text.len()); @@ -519,13 +529,34 @@ impl TextLine { Some(bp) } - /// A stroke (`stroke`: paint, width, cap, join and miter limit) over the glyph just drawn at `at` in `font` (fill - /// and stroke rendering), which `overprint`s or not: the run is stroked. `false`: it isn't - /// that glyph. + /// A stroke (`stroke`: paint, width, cap, join and miter limit) over the glyph just drawn at + /// `at` in `font` (fill and stroke rendering), which `overprint`s or not: that glyph is + /// stroked. `false`: it isn't that glyph. + /// + /// A filled glyph joins a run of the same fill whatever its stroke ([`Look::takes`]), so the + /// run can hold glyphs before it that are stroked otherwise or not at all: then the glyph's + /// text becomes a run of its own with this stroke, and the glyphs before it keep theirs + /// (#7741). The line's text is the same either way, so the characters' slots, gaps and + /// authored flags, kept along the whole line, still line up. pub fn stroke_last(&mut self, font: u128, at: Placement, stroke: TypeStroke, overprint: bool) -> bool { - let Some((look, ..)) = self.runs.last_mut().filter(|(l, ..)| l.font == font && (at.origin - self.last).hypot() < self.at.size * 1e-3) else { + let start = self.last_start; + let Some((look, shift, text)) = + self.runs.last_mut().filter(|(l, ..)| l.font == font && (at.origin - self.last).hypot() < self.at.size * 1e-3) + else { return false; }; + let same = look.stroke.as_ref() == Some(&stroke) && look.overprint_stroke == overprint; + // Only the last glyph's text in the run (`start` within it, on a character boundary): + // split it off. A run of that glyph alone is stroked whole, as before. + if !same && start > 0 && start < text.len() && text.is_char_boundary(start) { + let tail = text.split_off(start); + let mut own = look.clone(); + (own.stroke, own.overprint_stroke) = (Some(stroke), overprint); + let shift = *shift; + self.runs.push((own, shift, tail)); + self.last_start = 0; + return true; + } (look.stroke, look.overprint_stroke) = (Some(stroke), overprint); true } diff --git a/crates/pdf/src/tests/textstroke.rs b/crates/pdf/src/tests/textstroke.rs index c5496b53d5..f28e525f27 100644 --- a/crates/pdf/src/tests/textstroke.rs +++ b/crates/pdf/src/tests/textstroke.rs @@ -105,4 +105,20 @@ fn imported_stroked_type_keeps_the_files_cap_join_and_miter_limit() { let runs = styles("0 0 0 RG 4 w BT 1 Tr /F1 60 Tf 40 110 Td 1 j (WA) Tj 0 j (VE) Tj 0 Tr ET"); let joins: Vec<_> = runs.iter().map(|s| s.stroke_join).collect(); assert_eq!(joins, [LineJoin::Round, LineJoin::Miter]); + // Filled and stroked a glyph at a time: the next glyph's fill joins the run before it, and its + // stroke, with another join, restyles that glyph alone. + for mode in ["2", "6"] { + let content = format!("0 g 0 0 0 RG 4 w BT {mode} Tr /F1 60 Tf 40 110 Td 1 j (W) Tj 0 j (A) Tj 0 Tr ET"); + let runs = styles(&content); + let d = import(&page(&content)).unwrap(); + let mut text = String::new(); + d.walk(|n| { + if let NodeKind::Text(t) = &n.kind { + text.push_str(&t.plain_text()); + } + }); + assert_eq!(text, "WA", "{content}"); + let joins: Vec<_> = runs.iter().map(|s| (!s.stroke.is_none()).then_some(s.stroke_join)).collect(); + assert_eq!(joins, [Some(LineJoin::Round), Some(LineJoin::Miter)], "{content}: {runs:?}"); + } } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 34e73b99a1..2d4b937f6f 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -327,7 +327,7 @@ Stop opacity on translated artboards remains outside this regression coverage. - [x] Authored ordering is protected per stretch; ordinary stretches still reconstruct visual-order text. Outlines mode is unchanged. - [ ] Exact redistribution of one replacement over several lines/styles, and general mixed-direction paragraph layout. Replacement uses the first placed glyph's line/style. Tags deeper than 128 and replacements over 64 KiB encoded (at most 192 KiB decoded) fall back to glyph mappings with a warning; an outer accepted owner survives deeper tags. -- [x] Stroked type (text rendering modes 1, 2, 5 and 6, with or without `ActualText`) keeps the line cap, line join and miter limit of the graphics state it is stroked with, as paths do; glyphs stroked with another cap, join or miter limit are a run of their own (#7741, `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`). The dash array is not carried to the characters yet. +- [x] Stroked type (text rendering modes 1, 2, 5 and 6, with or without `ActualText`) keeps the line cap, line join and miter limit of the graphics state it is stroked with, as paths do; glyphs stroked with another cap, join or miter limit are a run of their own, also a glyph filled and then stroked otherwise than the run its fill joined (#7741, `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`). The dash array is not carried to the characters yet. - [x] Place from bytes or unlinked paths, and PDF clipboard paste, keep the selected page's live text. Synthetic two-page regressions in `crates/engine/src/cmd/fileio/tests_pdffidelity.rs` cover both sources, page selection and explicit outlines. The single-page importer honors the same text choice as Open; existing PDF fallback rules are unchanged. @@ -339,7 +339,7 @@ Stop opacity on translated artboards remains outside this regression coverage. | Date | Change | Summary | |---|---|---| -| 2026-11-15 | minor | PDF import: stroked type (text rendering modes 1, 2, 5 and 6, and glyphs under `ActualText`) came in with butt caps, miter joins and a miter limit of 10 whatever the file stroked it with, while paths kept the graphics state's. The characters' stroke now takes the line cap (`J`), line join (`j`) and miter limit (`M`) of the graphics state, as a path's stroke does, and glyphs stroked with other ones start a run of their own (#7741). Test `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`. Scores and estimates unchanged. | +| 2026-11-15 | minor | PDF import: stroked type (text rendering modes 1, 2, 5 and 6, and glyphs under `ActualText`) came in with butt caps, miter joins and a miter limit of 10 whatever the file stroked it with, while paths kept the graphics state's. The characters' stroke now takes the line cap (`J`), line join (`j`) and miter limit (`M`) of the graphics state, as a path's stroke does, and glyphs stroked with other ones start a run of their own, also a glyph filled and then stroked otherwise than the run its fill joined, so the glyphs before it keep their stroke (#7741). Test `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`. Scores and estimates unchanged. | | 2026-11-13 | minor | `.ai` and EPS import: layers that don't draw as the page, even with the drawn art of every live envelope put back, no longer put back the drawn art of every envelope of type. Each live envelope of type is weighed in the page check's own drawing of the pages it lies on, on the pixels that it and its drawn art ink there (the two drawn together on a layer of their own, without the rest of the layers; a pixel inks with an alpha above 8 of 255): it keeps its drawn art and its type, hidden, with the note, where the layers with it differ from the page in more of those pixels than its own check allows against its drawn art (10% of them, or 3% differing with all their neighbours, away from type), or where the envelopes of type that stay live, in the order they were read, would together differ there in more of a page than half of what the page check allows (a budget of the same size as the one their own checks share, kept apart from it). Art that differs elsewhere on the page no longer costs them their editable type; art that differs on their pixels still does. An envelope of type that a clip group or a clipping sublayer clips, an opacity mask masks, or a group with an appearance of its own, another envelope, a repeat or a blend holds keeps its drawn art as before, wherever it lies: drawn on its own it would be weighed elsewhere than it draws, and what holds it can draw it onto the page from off it. An envelope on no page stays live, as the page check doesn't see it, unless the page holds all the art that prints (an EPS's page that is its art's box, where art past the page is compared too): there one reaching past the page keeps its drawn art. Past 12 of them on the pages, all keep it. This holds where the editing data is trusted, where the objects that differ come in as the page's art (the placed files' other placement weighed on its own, as it draws them elsewhere under or over the envelopes; layers that draw as the page with only the envelopes the page holds put back are used as they are; where none of the objects that differ can then come in as the page's art, because an object holds type, the drawn art of every envelope of type is put back and that is tried again, as before), and for a `.ai` whose layers would open alone, which still opens alone: its envelopes of type are all settled again as they were with the page, in the same order and with the same checks, so each that opens live draws as it was weighed, and those the page held get their drawn art back. Where putting the drawn art of every live envelope back makes the layers draw as the page, it is still put back (#5065). In 224 users' files (used locally, never committed): envelopes of type that open live go from 174 to 186, in 8 files, none fewer in any file; no file changes between its layers and its PDF part; 7 renders change, by at most 0.7% of their pixels at 40 ppi (1 closer to the page, 1 alike, 5 further, by at most 0.7 of a percentage point, where VectorCraft's own envelope draws a little apart from the drawn art, within its check); the note for type in an envelope goes from 6 files. Of the 14 envelopes of type that had opened live and lost it to the page check, 2 keep their drawn art: one where the page draws something else where it is, and one in a file that comes in as its PDF part (#7141). Tests `build::candidate_tests::the_page_holds_back_only_envelopes_of_type_where_it_differs_from_them`, `envelopes_of_type_stay_live_within_half_of_what_the_page_check_allows`, `an_envelope_of_type_differing_in_more_of_its_pixels_than_its_check_allows_is_held_back`, `envelopes_of_type_the_layers_dont_draw_in_place_are_held_back`, `on_a_page_that_holds_all_the_art_an_envelope_of_type_past_it_is_held_back`, `objects_come_in_as_the_page_art_with_every_envelope_of_type_put_back_where_a_live_one_holds_a_difference`, `tests_native::layers_that_would_open_alone_keep_an_envelope_of_type_live_where_other_art_differs_from_the_page`, `layers_that_would_open_alone_keep_live_only_the_envelopes_of_type_the_page_doesnt_hold`, `trusted_layers_that_would_open_alone_open_alone_with_an_envelope_of_type_the_page_doesnt_hold_live`, `layers_with_objects_that_come_in_as_the_page_art_keep_an_envelope_of_type_that_draws_as_the_page_live`, `trusted_layers_keep_an_envelope_of_type_live_where_other_art_differs_from_the_page`, `trusted_layers_leave_an_envelope_of_type_the_page_draws_otherwise_its_drawn_art`, `build::candidate_tests::the_other_placement_of_placed_files_is_weighed_on_its_own`, `layers_that_draw_as_the_page_with_the_held_envelopes_of_type_put_back_are_used_so` | | 2026-11-13 | minor | PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke instead of being left out, adding no text (#6971; `empty_actual_text_keeps_the_glyphs_art` in `crates/pdf/src/tests/actual_text.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | `.ai` and EPS import, and what the canvas, every export and `.ai` save draw: a top-object envelope's mesh can save a side's curve in pieces. In users' files (used locally, never committed), 10 top objects save a grid of three patches across whose top and bottom edges are each one cubic split into thirds, and their drawn art follows the whole curve's parameter. VectorCraft gave each piece the share of its control polygon, so their type came out up to about half a point off the drawn art along the side, and one envelope of type kept its drawn art instead of opening live. A side whose pieces are all one curve split at anchors (each within 0.02 pt of that curve's piece) now follows that curve's parameter (#7097). Synthetic test: `a_top_object_whose_top_is_one_curve_split_in_thirds_opens_live`. Scores unchanged. | diff --git a/docs/roadmap.md b/docs/roadmap.md index fbe7ccc0fb..8ea08a6373 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Tech Circuit brush library, G5; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; 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 are in; 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; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; 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; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #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; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (PDF import: stroked type keeps the line cap, line join and miter limit the file strokes it with, a glyph stroked otherwise than the run it joins coming in as a run of its own, #7741; Tech Circuit brush library, G5; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; 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 are in; 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; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; 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; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #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; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) > > Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865). > @@ -159,7 +159,7 @@ Gujarati interface follow-up (2026-11-14): the catalog and zero/one plural rule | M1 | Selection, transform, layers, MCP (common command tools, strict keys, session resources and version-aware result hints) | ✅ mostly done (document-tab artwork drag with lossless resource transfer and one target Undo; malformed MCP app replies discard the connection without replay, #1761; bounded control socket waits and connection-slot recovery, #1726; bounded synthetic control input, #1709; no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); transform reference point snaps to anchors/centres; rotated objects keep a rotated bounding box and their angle; Free Transform, Scale and Reflect along a rotated box pending; the Transform fields take one Tab each, the W/H link after them) | 4–6 | | M2 | Drawing tools + smart guides | ✅ mostly done (#3380 deletion creates independent selected path pieces and keeps bounds-fitted gradients in place; endpoint highlighting remains open; Polygon/Star exact-size count persistence, with repeated-drag, tool-switch and Cancel regression coverage; Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending); a shape just drawn moves by its centre point and turns from the zone outside a corner with its tool still active (#5477); Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537), including the existing Snap to Glyph and Alignment Guides toggles (#6164); no View submenu; Copy and Cut with the Pen or Curvature tool take the anchors a Cmd/Ctrl-lent Direct Selection picked (#6123) | 12–18 | | M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper copies transformed character dimensions (#6404) and styles highlighted text only (atomic transparency rejection); Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork (screen-bounded Edit chip list, #3416); gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool (Shift-double-click enables adjoining adjustment in Width Point Edit; finite-arithmetic follow-up regressions and full CI passed ([Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area))), Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries; CSS library export preserves fractional linear and radial gradient stop positions. Left: freeform gradients in SVG/PDF, mixed spot/process gradients in SVG and PDF gradients mixing a spot color with RGB or Lab stops export as stops or process colors; PDF export writes dithered gradient fills with spot stops, and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, as images in process colors; confirm the Scale Strokes & Effects default; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls; Library panels' lists (swatch, Color Book, brush, symbol and graphic style libraries) fill a taller panel above its footer (#2992; scores and estimates unchanged); Graphic Styles list fills a taller panel above its footer (#2992; scores and estimates unchanged); Swatches list fills a taller panel above its footer (#1638; `the_list_fills_a_tall_panel_above_its_footer`; scores and estimates unchanged); | 4–8 | -| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | Rename artboards in Export for Screens (#7275); Explicit editing-data import keeps oversized capped-page artboards (#6865; estimates unchanged); Save refusal continuation (#6146): ordinary Save offers Save As; unchanged format keeps options, changed format opens its Save options; cancellation/automation isolation; focused UI regressions passed, native acceptance pending; PDF authored ActualText preserved once in editable import (#6165; first-glyph line/style, bounded input; estimates unchanged); PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. `.ai`/EPS type from the text document keeps its paragraphs' tab stops (#6147); `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, and may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them (#6137); ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); | 3–6 (existing scope; Affinity export unestimated) | +| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | PDF import: stroked type keeps the file's line cap, line join and miter limit (#7741; estimates unchanged); Rename artboards in Export for Screens (#7275); Explicit editing-data import keeps oversized capped-page artboards (#6865; estimates unchanged); Save refusal continuation (#6146): ordinary Save offers Save As; unchanged format keeps options, changed format opens its Save options; cancellation/automation isolation; focused UI regressions passed, native acceptance pending; PDF authored ActualText preserved once in editable import (#6165; first-glyph line/style, bounded input; estimates unchanged); PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. `.ai`/EPS type from the text document keeps its paragraphs' tab stops (#6147); `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, and may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them (#6137); ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); | 3–6 (existing scope; Affinity export unestimated) | | M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); opaque solid-fill knockout paths draw once in hard-edge/supersampled raster export, with analytical-AA compositing unchanged; file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); Craquelure/Mosaic Tiles/Stained Glass distance searches optimized with pixel-invariance checks and the `texture_drag_perf` probe (#1257); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 | | M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (live compound shapes: Make, Alt-click a Shape Mode, member modes, Release, Expand (#4522); single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; the slider opens at the Auto-Simplify level; a Direct Selection part of a path simplifies on its own); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open); Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog | 3–6 | | M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | Spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof and the custom dictionary and Ignore All commands and Auto Spell Check with its right-click suggestions, Ignore All and Add to Dictionary, and Spelling › Auto Spell Check in the context menu of type, and the Edit Custom Dictionary dialog (#4936; the Check Spelling dialog remains); Right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops (#6147); A line a soft return ends is justified as a wrapped line is (#6137); Indian-language pattern hyphenation preserves syllables in both composers (#4947); 🟡 Hanging punctuation keeps its published amounts under Glyph Scaling, and Standard burasagari counts a hung line start as room; Fullwidth `!` / `?` keep an upright fallback in vertical type; Hyphenation… and Justification… per paragraph (#2310, #2755), Auto Leading also driving the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles (explicit Auto-leading resets and local leading overrides), per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs (feature-valued alternates and unencoded glyphs applied in one Undo step while keeping Arabic joining, #5514), OpenType panel (including font-defined Stylistic Sets), Find Font (Document, Recent and System replacement sources, #5217), installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, show recently applied families first (#3515), filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas tatweels on the base letter's paint (`tests::harakat`); tab leaders, spell check, vertical type pending | 45–60 | @@ -207,6 +207,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-15 | minor | PDF import: stroked type (text rendering modes 1, 2, 5 and 6, and glyphs under `ActualText`) keeps the line cap, line join and miter limit of the graphics state it is stroked with, as paths do, instead of butt caps, miter joins and a miter limit of 10; a glyph filled and then stroked otherwise than the run its fill joined comes in as a run of its own, so the glyphs before it keep their stroke (#7741; `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`). Scores and estimates unchanged. | | 2026-11-14 | minor | Tech Circuit adds three original procedural brushes (Circuit Trace, Bus Lines, Terminal Nodes); ten built-in brush libraries. G5 remaining estimate 3–13 h (estimated); parity scores and headline totals unchanged. | | 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. | | 2026-11-12 | minor | Gujarati (`gu`): 4,286-row machine-assisted catalog reviewed by a native speaker, zero/one singular plurals, desktop font fallback and optional Gujarati craft-fonts fallback; all 72 translation tests pass; a web font and about 2,600 untranslated messages remain; scores and estimates unchanged | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index af98c88b54..8925edeca0 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Pencil ignores Smart Guides and Snap to Point and shows no Smart Guides, #7252; previously: Pencil continuation, straight-line and closing cursors; scores and estimates unchanged; previously: G5: Weather Icons joins built-in symbol libraries (Sun, Moon, Cloud, Rain, Thunder, Snow); scores and estimates unchanged; previously: a closed Pencil stroke closes with a straight segment; previously: Snap to Point with Smart Guides off draws no Smart Guides labels or lines, #7250; previously: 3D: the seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins, so white-light 3D output is expected to differ from the 2026-10-17 comparison at facet corners (not measured: the scenes were not exported again); previously: `.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; Character panel: the Set Font Height Reference dropdown shows an icon before each reference, #6304; scores and estimates unchanged; previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter, so a top object saved with its sides' curves in pieces draws as its `.ai` drawn art, #7097; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously minor: Object › Rasterize with the Grayscale or Bitmap colour model makes a grayscale image, #6969; scores and estimates unchanged; previously minor: `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, miter tips and moved strokes included, after its glyphs grown by half its strokes' weight (#6824; #6617: Swatches panel menu: Show Recent Colors; scores unchanged; previously minor: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously 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; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously 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; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; 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: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; 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: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; 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 and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; 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; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #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; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (PDF import: stroked type keeps the line cap, line join and miter limit the file strokes it with, a glyph stroked otherwise than the run it joins coming in as a run of its own, #7741; Pencil ignores Smart Guides and Snap to Point and shows no Smart Guides, #7252; previously: Pencil continuation, straight-line and closing cursors; scores and estimates unchanged; previously: G5: Weather Icons joins built-in symbol libraries (Sun, Moon, Cloud, Rain, Thunder, Snow); scores and estimates unchanged; previously: a closed Pencil stroke closes with a straight segment; previously: Snap to Point with Smart Guides off draws no Smart Guides labels or lines, #7250; previously: 3D: the seams of Extrude & Bevel, Inflate and Rotate under a white light, and of the faces of Extrude & Bevel (Classic), Revolve (Classic) and Rotate (Classic), went from miter to round joins, so white-light 3D output is expected to differ from the 2026-10-17 comparison at facet corners (not measured: the scenes were not exported again); previously: `.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; Character panel: the Set Font Height Reference dropdown shows an icon before each reference, #6304; scores and estimates unchanged; previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter, so a top object saved with its sides' curves in pieces draws as its `.ai` drawn art, #7097; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously minor: Object › Rasterize with the Grayscale or Bitmap colour model makes a grayscale image, #6969; scores and estimates unchanged; previously minor: `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, miter tips and moved strokes included, after its glyphs grown by half its strokes' weight (#6824; #6617: Swatches panel menu: Show Recent Colors; scores unchanged; previously minor: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously 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; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously 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; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; 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: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; 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: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; 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 and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; 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; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #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; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Tech Circuit update (2026-11-14): three original procedural brushes; ten built-in brush libraries (G5). > @@ -61,6 +61,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-15 | minor | PDF import: stroked type (text rendering modes 1, 2, 5 and 6, and glyphs under `ActualText`) keeps the line cap, line join and miter limit of the graphics state it is stroked with, as paths do, instead of butt caps, miter joins and a miter limit of 10; a glyph filled and then stroked otherwise than the run its fill joined comes in as a run of its own, so the glyphs before it keep their stroke (#7741; `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`). Scores and estimates unchanged. | | 2026-11-14 | minor | G5: Weather Icons joins Basic Shapes, UI Frames, Map Markers, Inventory Icons and Skill Icons as a sixth built-in symbol library (Sun, Moon, Cloud, Rain, Thunder, Snow; original procedural vector symbols generated in code, no default 9-slice); area score and estimate unchanged. | | 2026-11-14 | minor | G5: Tech Circuit adds three original procedural brushes (Circuit Trace, Bus Lines, Terminal Nodes); ten built-in brush libraries. G5 remaining estimate 3–13 h (estimated); parity scores and headline totals unchanged. | | 2026-11-14 | minor | Pencil and Paintbrush: a closed stroke (Alt at the release, or ending within Close Paths When Ends Are Within of its start) is fitted as the open stroke would be and closed by a straight segment back to its start, instead of curving on past both ends (`pencil_alt_closes_a_stroke_straight_back_to_its_start` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | @@ -525,7 +526,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, row menu commands beyond Collect for Export (now on every layer, sublayer, group and object row), menus on guide and blend spine rows, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers; artboard preset polish (artboard escape routing and document setup modal handoff done; Move Artwork for artboard X/Y edits, Rearrange All Artboards, Document Setup, New Document and the rest of the Layers panel are done) | 4–7 | | View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, Snap to Pixel/Glyph (print tiling is done) | 9–14 | | Guides, grids, smart guides, snapping, rulers | 3 | 85% | global/video rulers, Spacing Guides' distance readouts and spacing for resized or drawn art, Alt swapping a ruler guide's orientation, ruler guides selected together with art (marquee, Shift-click), ruler guides as Layers rows with their own lock and hide (#451: Lock Guides locks them all; Make Guides paths are Layers rows already) (dragging existing guides is done, #414; new guides snapping to side midpoints and artboards, and artboard guides, #451; the drawing tools snapping, with Construction Guides, #506; Spacing Guides for moves, #394; moves snapping to the grid, #740; ruler guides pulling drawn points with Smart Guides on, and "center" and "anchor" labels while hovering with the Selection tool, #812; distance and angle labels while drawing, #2983) | 2–4; status-bar X/Y uses the active ruler origin and General units | -| File formats | 6 | 85% | PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); Explicit editing-data oversized single-artboard import fixed (#6865); scores and estimates unchanged. Legacy `.ai` files that name but omit their prolog open with their layers, or with their art on one layer when the format has no layers; graph data and settings in these files still come in only as drawn art. Affinity export, and the rest of Affinity import (adjustments, blur and bevel effects, brushes, live symbol instances, text threading), DWG (no open spec: DXF instead), PSD placement as layers (Photoshop Import Options; the merged image is done), EPS checked against more real files users send (Inkscape files open as vectors; other generators' constructs are tested with programs written from the PostScript Language Reference only; CID-keyed CJK type and `CCITTFaxDecode` images not read), the rest of Illustrator's editing data (appearances and live effects, symbols, pattern fills, brushes, placed files: #637), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers, presets and Affinity import are done | 6–10 (existing scope; Affinity export unestimated) | +| File formats | 6 | 85% | PDF import: stroked type keeps the file's line cap, line join and miter limit (#7741; scores and estimates unchanged); PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); Explicit editing-data oversized single-artboard import fixed (#6865); scores and estimates unchanged. Legacy `.ai` files that name but omit their prolog open with their layers, or with their art on one layer when the format has no layers; graph data and settings in these files still come in only as drawn art. Affinity export, and the rest of Affinity import (adjustments, blur and bevel effects, brushes, live symbol instances, text threading), DWG (no open spec: DXF instead), PSD placement as layers (Photoshop Import Options; the merged image is done), EPS checked against more real files users send (Inkscape files open as vectors; other generators' constructs are tested with programs written from the PostScript Language Reference only; CID-keyed CJK type and `CCITTFaxDecode` images not read), the rest of Illustrator's editing data (appearances and live effects, symbols, pattern fills, brushes, placed files: #637), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers, presets and Affinity import are done | 6–10 (existing scope; Affinity export unestimated) | | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done, as are the Japanese papers in its Media list: JIS B4/B5/B6, はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号 and 四六判, beside the ISO B4/B5 that settings saved before them keep) | 1–2 | | Automation | 3 | 65% | scripting surface, batch (MCP drops malformed app replies without replay; Variables: text/visibility variables bound to objects, an object may carry several, dataset rows applied in one undo step, panel and Window › Variables with dialogs; image/graph kinds and dataset import/export open) | 6–10 | @@ -578,6 +579,7 @@ since 2026-10-01. - **Save refusal (#6146):** ordinary File > Save to its own location offers Save As after a terminal permission/read-only refusal, suggesting the original name and format, preserving options for the unchanged format and opening the chosen format’s Save options on a change. Cancel keeps the failed-save state. Busy pickers, recovery-copy errors, other failures, explicit destinations and Close/Quit keep the existing dialog. Engine, UI and MCP regression cases are in `cmd/fileio/tests_readonly.rs`, `io/tests_read_only_save.rs` and MCP `tests/fileio.rs`; focused UI regressions passed; engine regression execution and native panel acceptance pending. Scores and hours unchanged. - **PDF authored replacement text (#6165):** `crates/pdf/src/tests/actual_text.rs` covers replacement once with suffix placement, PDF text-string decoding, empty and nested owners, sibling spans, page reset, multiple text-show operations, style/line changes, fill-and-stroke, reader windows and bounded input. Authored logical-order stretches are protected while adjacent ordinary text still undergoes reconstruction. The replacement takes its first placed glyph's style and line; its characters are not redistributed over later lines/styles. Exact mixed-direction paragraph layout is not claimed. No export changes; scores and hours stay at 75% / 10–20 h. +- **PDF stroked type (#7741):** `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs` covers round, projecting and butt caps, round, bevel and miter joins and a miter limit of 3 on type stroked in rendering modes 1, 2, 5 and 6, shown as a word, a glyph at a time and under `ActualText`; glyphs stroked with another join as runs of their own; and a glyph filled and stroked after one stroked otherwise, which keeps the join of the glyph before it. Synthetic PDFs written in the test (standard Helvetica, not embedded); no fonts or fixtures added. The dash array is not yet carried to the characters. - **PDF Indic type (#5235, #6906):** `crates/pdf/src/tests/reshaped.rs` covers a vowel sign the file draws before its consonant (the line keeps the file's glyphs as outlines, with a note), text in reading order (stays type), a mark with no advance (stays on its line, not vertical type of its own), outlined glyphs stroked with different dashes and translucent outlined glyphs (each keeps its own path). Synthetic font only; one real Gujarati file checked by eye on page 1. Indic type in an uninstalled font keeps the file's glyphs as outlines; reordering signs so more lines stay editable is not done. Scores and hours stay at 75% / 10-20 h. From 4065d4c934b698cb9643cea808f915ca4ecdf8ba Mon Sep 17 00:00:00 2001 From: HassanKhan Date: Sun, 15 Nov 2026 13:14:50 +0500 Subject: [PATCH 3/3] docs: note PDF stroked type (#7741) in the current File formats rows --- docs/target-app-parity.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e5bdd802c6..d226eb2457 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -262,9 +262,9 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; large images trace with a warning; Auto Color keeps rare distinct colours) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them | | Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers row menu commands beyond Collect for Export and menus on guide and blend spine rows (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); DXF export's Include Placed Images and Include Fill Hatches options, off in CNC / CAM (`images_and_hatches_are_left_out_independently_and_the_cnc_preset_leaves_out_both`; score, gaps and hours unchanged); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box (`tests_native::aligned_type_lies_as_far_from_its_path_as_the_file_lays_it_out`), and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects other than grouped column, stacked column, line and area graphs the `.ai` reader reads back whole as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, textures, map art and shading in 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. Graphs saved to `.ai` in either form open again as live graphs (`ai_editing::tests::graphs_are_written_with_their_records_and_open_live`, `legacy_ai::tests::graphs_are_written_with_their_records_and_open_live`, `cmd::graph::tests::a_drawn_graph_saved_as_ai_opens_as_a_graph_unless_the_save_warns`); other graphs are still written as their art (`graphs_the_records_cant_keep_are_written_as_their_art`); score and hours unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects other than grouped column, stacked column, line and area graphs the `.ai` reader reads back whole as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. Graphs saved to `.ai` in either form open again as live graphs (`ai_editing::tests::graphs_are_written_with_their_records_and_open_live`, `legacy_ai::tests::graphs_are_written_with_their_records_and_open_live`, `cmd::graph::tests::a_drawn_graph_saved_as_ai_opens_as_a_graph_unless_the_save_warns`); other graphs are still written as their art (`graphs_the_records_cant_keep_are_written_as_their_art`); score and hours unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Save refusal continuation (#6146): `READ_ONLY_LOCATION`, Save As on ordinary UI Save only; regression execution pending; score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); DXF export's Include Placed Images and Include Fill Hatches options, off in CNC / CAM (`images_and_hatches_are_left_out_independently_and_the_cnc_preset_leaves_out_both`; score, gaps and hours unchanged); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`; the distance from the path under Align to Path is measured from the top of each character's em box, half the height of the top of its H or the bottom of its font's bounding box (`tests_native::aligned_type_lies_as_far_from_its_path_as_the_file_lays_it_out`), and the fit under Gravity does not reproduce the file's layout), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. PDF import: stroked type (text rendering modes 1, 2, 5 and 6) keeps the file's line cap, line join and miter limit, as paths do (#7741, `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`); score, gaps and hours unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects other than grouped column, stacked column, line and area graphs the `.ai` reader reads back whole as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, textures, map art and shading in 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. Graphs saved to `.ai` in either form open again as live graphs (`ai_editing::tests::graphs_are_written_with_their_records_and_open_live`, `legacy_ai::tests::graphs_are_written_with_their_records_and_open_live`, `cmd::graph::tests::a_drawn_graph_saved_as_ai_opens_as_a_graph_unless_the_save_warns`); other graphs are still written as their art (`graphs_the_records_cant_keep_are_written_as_their_art`); score and hours unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. PDF import: stroked type (text rendering modes 1, 2, 5 and 6) keeps the file's line cap, line join and miter limit, as paths do (#7741, `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`); score, gaps and hours unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), a saved `.ai`'s editing copy without live effects on objects other than paths, compound paths and groups that qualify (at most a fill and a stroke, a non-zero fill rule when filled, no large canvas) and whose appearance and effects the art-style writer supports, and with live objects other than grouped column, stacked column, line and area graphs the `.ai` reader reads back whole as their art (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified PDF marked-text batching covered by M4 evidence (#6309); cross-line/style ActualText layout (#6165) and styled-cluster limits remain.| 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. Graphs saved to `.ai` in either form open again as live graphs (`ai_editing::tests::graphs_are_written_with_their_records_and_open_live`, `legacy_ai::tests::graphs_are_written_with_their_records_and_open_live`, `cmd::graph::tests::a_drawn_graph_saved_as_ai_opens_as_a_graph_unless_the_save_warns`); other graphs are still written as their art (`graphs_the_records_cant_keep_are_written_as_their_art`); score and hours unchanged. PDF ActualText preserved in editable import (#6165; bounded synthetic coverage below); score/hours unchanged. Save refusal continuation (#6146): `READ_ONLY_LOCATION`, Save As on ordinary UI Save only; regression execution pending; score/hours unchanged. Legacy `.ai` type keeps its manual kerning pairs (`Tk`, `TK`), discretionary hyphens (`T+`, `T-`) and hyphenation language (`XL`) when it opens, and legacy `.ai` saves write manual kerning as `Tk` and soft hyphens as `T+` (`import::ai::tests::legacy_manual_pair_kerning_is_kept`, `legacy_ai::tests::manual_kerning_and_soft_hyphens_are_written_and_read_back`); score, gaps and hours unchanged. PDF import: stroked type (text rendering modes 1, 2, 5 and 6) keeps the file's line cap, line join and miter limit, as paths do (#7741, `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`); score, gaps and hours unchanged. | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged); document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged) | | View & navigation | 3 | 70% | multiple windows and the rest of Arrange (Cascade, Float in Window, Float All in Windows) | 9–14 | estimated; Window › Arrange › Tile and Consolidate All Windows done (score/hours unchanged) | | Guides, grids, smart guides, snapping, rulers | 3 | 80% | "intersect" smart guides for a moved selection's anchors (drawn points and dragged anchors snap to intersections, #1658) | 4–7 | estimated (lowered from 85%: target moved); Control bar snapping quick access includes Snap to Glyph and Alignment Guides (#6164); new ruler-guide Alt orientation implemented; global/artboard ruler modes (`view.globalRulers`) and video rulers (View › Rulers › Show Video Rulers, #2849) done; status-bar X/Y uses the active ruler origin and General units; score unchanged |