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. |