diff --git a/crates/engine/src/tests_pattern.rs b/crates/engine/src/tests_pattern.rs index 437f5e288..787c6ceb2 100644 --- a/crates/engine/src/tests_pattern.rs +++ b/crates/engine/src/tests_pattern.rs @@ -419,15 +419,63 @@ fn svg_export_writes_pattern_def() { assert!(svg[start..end].contains(" + + + + + + + +"##; + let d = vectorcraft_svg::import(svg).unwrap(); + let rep = vectorcraft_pdf::export_with_report(&d, &Default::default()).unwrap(); assert!(rep.warnings.iter().all(|w| !w.contains("pattern")), "{:?}", rep.warnings); - assert!(rep.bytes.len() > 500); + assert!(rep.bytes.len() < 8_000, "{} bytes", rep.bytes.len()); + let plain = vectorcraft_pdf::export(&d, &vectorcraft_pdf::PdfOptions::uncompressed()).unwrap(); + assert!(String::from_utf8_lossy(&plain).contains("/PatternType 1")); + let back = vectorcraft_pdf::import(&rep.bytes).unwrap(); + assert_eq!(back.patterns.len(), 1); + assert_eq!(back.patterns[0].art.len(), 2, "the hatch's two stripes"); + let mut leaves = vec![]; + back.walk(|n| { + if !n.is_container() { + leaves.push(n.appearance.fill_paint()); + } + }); + assert!(leaves.len() <= 2, "{leaves:?}"); + assert!(leaves.iter().any(|p| matches!(p, Paint::Pattern { pattern, .. } if *pattern == back.patterns[0].name)), "{leaves:?}"); +} + +/// A pattern fill nested more than four patterns deep is written as its tiles, and the export says so. +#[test] +fn pdf_export_writes_patterns_nested_too_deep_as_tiles_with_a_warning() { + use std::sync::Arc; + use vectorcraft_doc::pattern::{PatternDef, pattern_paint}; + use vectorcraft_doc::{Appearance, Document}; + let square = |paint: Paint| { + Node::path(NodeId(0), vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::basic(paint, Paint::None, 0.0)) + }; + let mut d = Document::new(100.0, 100.0); + d.patterns.push(PatternDef::new("P0", vec![Arc::new(square(Paint::solid(vectorcraft_color::Color::rgb(1.0, 0.0, 0.0))))])); + for i in 1..=4 { + d.patterns.push(PatternDef::new(&format!("P{i}"), vec![Arc::new(square(pattern_paint(&format!("P{}", i - 1))))])); + } + // P3 nests four tiling patterns (P3 to P0); P4 one more, which is written as its tiles. + for (top, tiles) in [("P3", false), ("P4", true)] { + let mut d = d.clone(); + let mut n = square(pattern_paint(top)); + n.id = d.alloc_id(); + let layer = d.layers[0].id; + d.insert(Some(layer), 0, n).unwrap(); + let rep = vectorcraft_pdf::export_with_report(&d, &Default::default()).unwrap(); + assert_eq!(rep.warnings.iter().any(|w| w.contains("are written as their tiles")), tiles, "{top}: {:?}", rep.warnings); + } } #[test] diff --git a/crates/engine/src/tests_pdffidelity.rs b/crates/engine/src/tests_pdffidelity.rs index 21acfe4a7..35fe314a2 100644 --- a/crates/engine/src/tests_pdffidelity.rs +++ b/crates/engine/src/tests_pdffidelity.rs @@ -1,5 +1,5 @@ -//! PDF vector fidelity: pattern strokes are written as their tiles clipped to the stroke's -//! outline, freeform gradients as images of their colour field clipped to what they paint, and +//! PDF vector fidelity: pattern fills and strokes are written as tiling patterns (strokes clipped +//! to the stroke's outline), freeform gradients as images of their colour field clipped to what they paint, and //! both read back looking like the canvas (within 2% at 150 ppi), with nothing to report. use serde_json::{Value, json}; @@ -55,7 +55,7 @@ fn assert_faithful(what: &str, s: &Session, image: bool, rough: &Document) { } #[test] -fn pattern_strokes_are_tiles_clipped_to_the_stroke() { +fn pattern_strokes_are_tiling_patterns_clipped_to_the_stroke() { let mut s = Session::new(); run(&mut s, "file.new", json!({"width": 200, "height": 200})); let dot = run(&mut s, "shape.ellipse", json!({"x": 0, "y": 0, "width": 6, "height": 6}))["id"].clone(); @@ -73,6 +73,23 @@ fn pattern_strokes_are_tiles_clipped_to_the_stroke() { assert_faithful("pattern stroke", &s, false, &repainted(&s, &frame, None, Some(grey))); } +/// A brick tiling whose art reaches over the next tiles repeats as one period of the tiling (#3849). +#[test] +fn brick_tilings_with_overlapping_tiles_are_tiling_patterns() { + let mut s = Session::new(); + run(&mut s, "file.new", json!({"width": 200, "height": 200})); + let dot = run(&mut s, "shape.ellipse", json!({"x": 0, "y": 0, "width": 12, "height": 12}))["id"].clone(); + run(&mut s, "select.set", json!({"ids": [dot]})); + run(&mut s, "paint.setFill", json!({"color": "#d02020"})); + run(&mut s, "object.pattern.make", json!({"name": "Bricks", "tileType": "brickByRow", "width": 10, "height": 10})); + run(&mut s, "object.pattern.done", json!({})); + let frame = run(&mut s, "shape.rectangle", json!({"x": 30, "y": 30, "width": 140, "height": 120}))["id"].clone(); + run(&mut s, "select.set", json!({"ids": [frame]})); + run(&mut s, "paint.setFill", json!({"swatch": "Bricks"})); + let grey = Paint::solid(Color::rgb(0.5, 0.5, 0.5)); + assert_faithful("brick fill", &s, false, &repainted(&s, &frame, Some(grey), None)); +} + #[test] fn freeform_gradients_are_images_of_their_colour_field() { let mut s = Session::new(); diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 26482b137..1854db945 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -621,6 +621,8 @@ pub(crate) struct Exporter<'a> { /// Inline graphics being drawn inside inline graphics (text in a symbol shown inline in /// text…): deeper ones are left out. inline_depth: u32, + /// Tiling patterns being drawn inside tiling patterns ([`Self::tiling`]). + pattern_depth: u32, /// Keys of the images left out because the writer couldn't decode them /// ([`leaving_out_undecodable`]). pub left_out: HashSet, @@ -629,6 +631,9 @@ pub(crate) struct Exporter<'a> { /// How deep inline graphics nest before they are left out. const MAX_INLINE_DEPTH: u32 = 4; +/// How deep tiling patterns nest: deeper pattern fills are written as their tiles. +const MAX_PATTERN_DEPTH: u32 = 4; + /// The warning for an image left out because it can't be decoded: the note raster exports give /// (`vectorcraft-engine`'s `rasterfx::undecoded_warning`). const UNDECODED: &str = "an image couldn't be decoded and is missing from the picture"; @@ -664,6 +669,7 @@ impl<'a> Exporter<'a> { overprinted: false, invisible: false, inline_depth: 0, + pattern_depth: 0, left_out: HashSet::new(), } } @@ -1304,9 +1310,9 @@ impl Exporter<'_> { /// Paint the area `clip` (filled by `rule`; bounds `area`) with `p` at `transparency` (opacity, /// blend mode) when it is an [area paint](Self::area_paint) of a fill (`fill`) or a stroke: a - /// pattern's tile instances covering it, or an image of a gradient drawn as pixels placed on - /// `bounds` (the painted object's box, as on the canvas), clipped to it. False (nothing drawn) - /// for other paints. + /// pattern's [tiling pattern](Self::tiling) (else its tile instances covering it), or an image + /// of a gradient drawn as pixels placed on `bounds` (the painted object's box, as on the + /// canvas), clipped to it. False (nothing drawn) for other paints. #[allow(clippy::too_many_arguments)] fn area( &mut self, @@ -1323,26 +1329,40 @@ impl Exporter<'_> { return false; } let (opacity, mode) = transparency; + let tiling = match p { + Paint::Pattern { pattern, xf } => self.tiling(s, pattern, *xf), + _ => None, + }; let mut pushes = 0; if mode != BlendMode::Normal { s.push_blend_mode(blend(mode)); pushes += 1; } - s.push_clip_path(clip, &rule); - pushes += 1; - if opacity < 1.0 { - s.push_opacity(norm(opacity)); + if let Some(paint) = tiling { + s.set_stroke(None); + s.set_fill(Some(Fill { paint, opacity: norm(opacity), rule })); + s.draw_path(clip); + s.set_fill(None); + } else { + s.push_clip_path(clip, &rule); pushes += 1; - } - let doc = self.doc; - match p { - Paint::Pattern { pattern, xf } => { - for inst in doc.pattern(pattern).map(|def| def.instances_in(*xf, area)).unwrap_or_default() { - self.node(s, &inst, area, true); + if opacity < 1.0 { + s.push_opacity(norm(opacity)); + pushes += 1; + } + let doc = self.doc; + match p { + Paint::Pattern { pattern, xf } => { + if let Some(def) = doc.pattern(pattern) { + self.warn("pattern fills nested more than 4 patterns deep, or with tiles too large for PDF, are written as their tiles"); + for inst in def.instances_in(*xf, area) { + self.node(s, &inst, area, true); + } + } } + Paint::Gradient(g) => self.field_image(s, g, bounds, area, fill), + _ => {} } - Paint::Gradient(g) => self.field_image(s, g, bounds, area, fill), - _ => {} } for _ in 0..pushes { s.pop(); @@ -1350,6 +1370,43 @@ impl Exporter<'_> { true } + /// Pattern swatch `name` placed by `at` (pattern space → the current space) as a PDF tiling + /// pattern (ISO 32000-1, 8.7.3.1): one period of the tiling ([`vectorcraft_doc::PatternDef::period`], + /// what the canvas repeats too) with every tile instance reaching into it, in paint order, so + /// brick and hex tilings and overlapping tiles repeat as they are; clipped to the period when + /// art reaches out of it. `None` for a pattern nested deeper than [`MAX_PATTERN_DEPTH`], a period + /// or placement beyond [`MAX_COORD`] or a placement that flattens the tiles. + fn tiling(&mut self, s: &mut Surface, name: &str, at: Affine) -> Option { + let doc = self.doc; + let def = doc.pattern(name)?; + let (pw, ph) = def.period(); + if self.pattern_depth >= MAX_PATTERN_DEPTH || !in_range(&[pw, ph]) || !in_range(&at.as_coeffs()) || at.determinant().abs() <= 1e-12 { + return None; + } + let cell = Rect::new(0.0, 0.0, pw, ph); + let tiles = def.instances_in(Affine::IDENTITY, cell); + let mut sb = s.stream_builder(); + let mut cs = sb.surface(); + let reaches_out = tiles.iter().any(|t| t.visual_bounds().is_some_and(|b| cell.intersect(b) != b)); + let clip = reaches_out.then(|| to_path(&cs, &cell.to_path(0.1))).flatten(); + if let Some(c) = &clip { + cs.push_clip_path(c, &krilla::paint::FillRule::NonZero); + } + // Pattern art is a picture of its own: it takes no part in a knockout group around the object. + let knockout = std::mem::take(&mut self.knockout); + self.pattern_depth += 1; + for t in &tiles { + self.node(&mut cs, t, cell, true); + } + self.pattern_depth -= 1; + self.knockout = knockout; + if clip.is_some() { + cs.pop(); + } + cs.finish(); + Some(krilla::paint::Pattern { stream: sb.finish(), transform: xf(at), width: pw as f32, height: ph as f32 }.into()) + } + /// Draw gradient `g`, placed on `bounds`, as an image over `area`, sampled at the document's /// raster effects resolution where it is drawn (`area` as the current transform sizes it, so /// scaled type gets its pixels too): a freeform gradient's colour field (at most diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 6595e4a62..798efd3d6 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-10-23 · **Change:** minor (SVG: type written as outlines stacks its colour glyphs under their run's paint, as the canvas does; previously: SVG keeps attributes in other applications' XML namespaces on their objects from import to export, the rest of #522; PDF import: a line's spacing is measured from the file's own glyph widths and kept within a hundredth of an em of where the file puts each glyph, so type opens as one run where it is placed; previously: PDF text opens where the file draws it, #722; PDF/X-1a and PDF/X-3 flattening keeps a CMYK document's inks where it rasterizes, #2160; EPS and `.ai` editing data: type keeps stacked art styles as a live appearance; point type frames without a matrix read; `editingData: "always"` for EPS; PDF invisible text, as a scanned page's recognized text is, opens as type with no fill or stroke and is written back as invisible text, #2675; DXF circular cubic paths export as native CIRCLE/ARC where stroke semantics permit (R13–2018 for weighted strokes; R12 weighted strokes remain POLYLINE) (#1299); non-circular curves stay SPLINE; scores and hours unchanged; an Illustrator 3 file without layers, as image editors export their paths, opens with its art on one layer, storytold/photocraft#6036; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; batch exporting, #1935; legacy `.ai` runs as PostScript with a prolog of its own, #868; legacy `.ai` of version 7 saved, with type as text, hidden objects and an artboard picker, and its type read, #868; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: SVG `:not()` selectors, #1557, stacked selectors, #1703, linked files, #1870; DXF VPORT, #1789; nesting caps, #1852; hostile PDF, `.ai`, EPS, Affinity and native data, #1862, #1863, #1871–#1873; failed saves, #1685, #1693, #1694; type in symbols after reopening, #1738; scores unchanged; verified PngSuite downloads, #1729; EPS stitched shadings, #1653; oversized images in WMF/EMF refused from the header, #1776; PDF name trees with shared nodes, #1781; native size limits for files read from memory, #1783; private temporary files on Unix, #1778; ruzstd 0.9.1, #1807; scores unchanged; SVG import: CSS transforms and the `font` shorthand, #1565, percentage lengths in symbols, #1596, Inkscape flowed text and pages, #1568, #1601, filter warnings, #1598; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment in JPEG, TIFF and PSD, #1584; saves to folders that refuse the temporary file, #1620 in part; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; compact native saves for documents with a lot of path data, #1492; PDF and EPS paste as vector art, #1499; `.ai` gradient stops listed upwards, #1530; SVG text on basic shapes, #1527; PDF soft masks drawn as images, #1551; Type 42 fonts in EPS, #1553; clips that only seem to cover the page, #1542; images too large to decode are reported, #1540; a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (PDF export: pattern fills are written as tiling patterns, #3849; previously: SVG: type written as outlines stacks its colour glyphs under their run's paint, as the canvas does; SVG keeps attributes in other applications' XML namespaces on their objects from import to export, the rest of #522; PDF import: a line's spacing is measured from the file's own glyph widths and kept within a hundredth of an em of where the file puts each glyph, so type opens as one run where it is placed; previously: PDF text opens where the file draws it, #722; PDF/X-1a and PDF/X-3 flattening keeps a CMYK document's inks where it rasterizes, #2160; EPS and `.ai` editing data: type keeps stacked art styles as a live appearance; point type frames without a matrix read; `editingData: "always"` for EPS; PDF invisible text, as a scanned page's recognized text is, opens as type with no fill or stroke and is written back as invisible text, #2675; DXF circular cubic paths export as native CIRCLE/ARC where stroke semantics permit (R13–2018 for weighted strokes; R12 weighted strokes remain POLYLINE) (#1299); non-circular curves stay SPLINE; scores and hours unchanged; an Illustrator 3 file without layers, as image editors export their paths, opens with its art on one layer, storytold/photocraft#6036; EPS editing data: legacy mesh corners, vector opacity masks and compound member paint retained; raster page comparison filters minification without relaxing limits; scores and hours unchanged; batch exporting, #1935; legacy `.ai` runs as PostScript with a prolog of its own, #868; legacy `.ai` of version 7 saved, with type as text, hidden objects and an artboard picker, and its type read, #868; PDF.R1: shared PDF reader with windowed content scanning and explicit incomplete-content errors; scores and estimates unchanged; PDF.W1: guarded shared PDF writer; output behavior, scores and estimates unchanged; later 2026-10-14 merges: SVG `:not()` selectors, #1557, stacked selectors, #1703, linked files, #1870; DXF VPORT, #1789; nesting caps, #1852; hostile PDF, `.ai`, EPS, Affinity and native data, #1862, #1863, #1871–#1873; failed saves, #1685, #1693, #1694; type in symbols after reopening, #1738; scores unchanged; verified PngSuite downloads, #1729; EPS stitched shadings, #1653; oversized images in WMF/EMF refused from the header, #1776; PDF name trees with shared nodes, #1781; native size limits for files read from memory, #1783; private temporary files on Unix, #1778; ruzstd 0.9.1, #1807; scores unchanged; SVG import: CSS transforms and the `font` shorthand, #1565, percentage lengths in symbols, #1596, Inkscape flowed text and pages, #1568, #1601, filter warnings, #1598; type on a path from PDF and `.ai`, #1585, #1586; ICC tag alignment in JPEG, TIFF and PSD, #1584; saves to folders that refuse the temporary file, #1620 in part; scores unchanged; TIFF is available in Export for Screens and Asset Export with shared format settings; scores and hours unchanged; compact native saves for documents with a lot of path data, #1492; PDF and EPS paste as vector art, #1499; `.ai` gradient stops listed upwards, #1530; SVG text on basic shapes, #1527; PDF soft masks drawn as images, #1551; Type 42 fonts in EPS, #1553; clips that only seem to cover the page, #1542; images too large to decode are reported, #1540; a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/mod.rs`). Illustrator's list comes from its public @@ -130,6 +130,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | Date | Change | Summary | |---|---|---| +| 2026-10-24 | minor | PDF export: a pattern fill or stroke is written as a PDF tiling pattern (ISO 32000-1, 8.7.3.1) of one period of the tiling, as the canvas repeats it, so grid, brick and hex tilings and overlapping tiles keep their look in a few kilobytes, and PDF import reads it back as one object with a pattern swatch instead of hundreds of tile paths (#3849). Patterns nested more than four deep, or with tiles beyond a billion points, are still written as their tiles, now with a warning. Tests: the issue's hatch round trip, a brick tiling read back against the canvas, the nesting warning; scores and hours unchanged | | 2026-10-23 | minor | SVG: type written as outlines stacks each run's colour glyphs (bitmap images, COLR and SVG art) under the run's paint, as the canvas does, so a plain character set over a colour one stays on top; scores unchanged | | 2026-10-23 | minor | PDF import: a line's spacing is measured from the file's own glyph widths, so type in a font the file doesn't carry, drawn with an installed font whose advances its widths round off (Helvetica on macOS: 2048 units an em), keeps one tracking value on its letters instead of a run per character (#722 follow-up); test with built-in Inter, independent of the fonts installed; scores and hours unchanged | | 2026-10-23 | minor | SVG: attributes in other applications' XML namespaces (`plotter:pen="2"` with `xmlns:plotter`) are kept on the objects and layers they belong to, saved with them in `.vectorcraft` files and written back on export, each namespace declared once on the root under the file's prefix where it is free. SVG's, XLink's, XML's, Inkscape's and Sodipodi's own aside; at most 64 per object of at most 64 KiB each, with a warning for what is left out (the rest of #522). Scores and hours unchanged |