diff --git a/crates/eps/src/import/tests_type42.rs b/crates/eps/src/import/tests_type42.rs index 546d0d1e9..21399afe7 100644 --- a/crates/eps/src/import/tests_type42.rs +++ b/crates/eps/src/import/tests_type42.rs @@ -144,6 +144,44 @@ fn named_fonts_take_the_show_operators_spacing() { assert!(t[0].runs.len() == 1 && t[0].runs[0].style.tracking == 0.0, "{:?}", t[0].runs); } +/// `kshow` runs its procedure between each two characters with their codes (PLRM 3rd ed., +/// `kshow`): where it moves the current point, the next character follows (kept on live type as +/// tracking), and what it draws is drawn over the type. +#[test] +fn kshow_procedures_kern_live_type() { + let (h, wh) = glyph('H'); + let (i, wi) = glyph('I'); + let f = font(&[h, i, h]); + // Only the pair of codes 1 then 2 is kerned, 20 units. + let kern = "{ exch 1 eq exch 2 eq and { 20 0 rmoveto } if }"; + let r = read(&format!("{f}/Plain42 findfont 50 scalefont setfont 10 20 moveto {kern} (\\001\\002\\003) kshow")); + let t = texts(&r); + assert_eq!(t.len(), 1, "{:?}", all(&r.document)); + assert_eq!(t[0].plain_text(), "HIH"); + let o = origins(&t[0]); + assert!((o[1] - o[0] - (50.0 * wh + 20.0)).abs() < 0.05, "{o:?}"); + assert!((o[2] - o[1] - 50.0 * wi).abs() < 0.05, "{o:?}"); + let check = |cond: String| super::tests::check(&format!("{f} /Plain42 findfont 50 scalefont setfont {cond}")); + check(format!( + "gsave nulldevice 10 20 moveto {kern} (\\001\\002\\003) kshow currentpoint grestore pop {} sub abs 0.01 lt", + 10.0 + 50.0 * (2.0 * wh + wi) + 20.0 + )); + // A font found by name kerns the same way. + let r = read("/Helvetica findfont 20 scalefont setfont 10 20 moveto { pop pop -3 0 rmoveto } (HIH) kshow"); + let t = texts(&r); + let plain = read("/Helvetica findfont 20 scalefont setfont 10 20 moveto (HIH) show"); + let (o, p) = (origins(&t[0]), origins(&texts(&plain)[0])); + assert!((o[1] - o[0] - (p[1] - p[0] - 3.0)).abs() < 0.05 && (o[2] - o[1] - (p[2] - p[1] - 3.0)).abs() < 0.05, "{o:?} {p:?}"); + // A procedure that draws: a square after the type, over it. + let r = read("/Helvetica findfont 20 scalefont setfont 10 20 moveto { pop pop currentpoint 2 2 rectfill } (AB) kshow"); + let kinds: Vec = all(&r.document) + .iter() + .filter(|n| matches!(n.kind, NodeKind::Text(_) | NodeKind::Path { .. })) + .map(|n| matches!(n.kind, NodeKind::Text(_))) + .collect(); + assert_eq!(kinds, [true, false]); +} + /// Characters the installed font would join in a ligature (`fi`) keep the places the file gives /// each of them: the type is set without ligatures and each character's tracking is its own. #[test] diff --git a/crates/eps/src/import/text.rs b/crates/eps/src/import/text.rs index 1cbe37ca7..dd1d77878 100644 --- a/crates/eps/src/import/text.rs +++ b/crates/eps/src/import/text.rs @@ -308,11 +308,7 @@ impl Interp<'_> { let w = self.type3_glyph(font, Glyph::Code(code), at, true)?; let d = spacing.advance(i, i64::from(code), w); self.g.cur = Some(self.xf() * (at + d)); - if let (Spacing::Kern(p), Some(next)) = (spacing, s.get(i + 1)) { - self.push(Obj::Int(i64::from(code)))?; - self.push(Obj::Int(i64::from(*next)))?; - self.call(p.clone())?; - } + self.kern(spacing, s, i, at + d)?; } let text: String = s.iter().map(|b| char::from(*b)).filter(|c| !c.is_control()).collect(); self.group_glyphs(start, &text); @@ -544,6 +540,9 @@ impl Interp<'_> { let inv = f1.inverse(); let mut moves = Vec::with_capacity(s.len()); let mut k = 0; + // What `kshow`'s procedure draws goes after the type. + let start = self.out.drawn.len(); + let mut pen = at; for (i, (&code, c)) in s.iter().zip(&chars).enumerate() { let own = match &plain { Some((_, glyphs)) => glyphs.get(i).map_or(0.0, |g| g.width), @@ -551,7 +550,9 @@ impl Interp<'_> { None => 0.0, }; k += usize::from(c.is_some()); - let d = spacing.advance(i, i64::from(code), em(own)); + let mut d = spacing.advance(i, i64::from(code), em(own)); + d += self.kern(spacing, s, i, pen + d)?; + pen += d; let e = inv * Point::new(d.x, d.y) - inv * Point::ZERO; if ![d.x, d.y, e.x * size, e.y * size].iter().all(|v| v.is_finite()) { return ps_err("undefinedresult", "a show"); @@ -618,21 +619,36 @@ impl Interp<'_> { t.xf = Affine::translate(self.xf() * Point::new(lead.x, lead.y) - self.xf() * Point::ZERO) * t.xf; } let clips = self.g.clips.clone(); - self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips); + if self.out.push(Node::new(NodeId(0), NodeKind::Text(Box::new(t))), &clips).is_some() + && let Some(after) = self.out.drawn.get_mut(start..) + { + after.rotate_right(1); + } } self.g.cur = Some(self.xf() * (at + total)); Ok(()) } + /// `kshow` between character `i` of `s` and the next: its procedure run with the two codes + /// and the current point at user point `pen` (PLRM 3rd ed., `kshow`: the procedure may + /// change the current point, as kerning does) → how far it moved the point (user space). + /// Nothing for other show operators or after the last character. + fn kern(&mut self, spacing: &Spacing, s: &[u8], i: usize, pen: Point) -> Res { + let (Spacing::Kern(p), Some(&code), Some(&next)) = (spacing, s.get(i), s.get(i + 1)) else { return Ok(Vec2::ZERO) }; + self.g.cur = Some(self.xf() * pen); + self.push(Obj::Int(i64::from(code)))?; + self.push(Obj::Int(i64::from(next)))?; + self.call(p.clone())?; + Ok(self.user_point()? - pen) + } + /// `s` in a Type 42 font, some of whose glyphs stand for no character, drawn from the font's /// outlines at the current point (one group), spaced as `spacing` says. fn show_type42_outlines(&mut self, data: &Embedded, glyphs: &[Plain], s: &[u8], spacing: &Spacing, at: Point, f1: Affine) -> Res { - // Each glyph's place, checked before anything is painted: moves that overflow (two - // `1e308` numbers in `xshow`) are an error, as in `show_plain`, not glyphs at infinity. - let mut places = Vec::with_capacity(glyphs.len()); + // The moves, checked before anything is painted: moves that overflow (two `1e308` + // numbers in `xshow`) are an error, as in `show_plain`, not glyphs at infinity. let mut pen = at; for (i, (g, &code)) in glyphs.iter().zip(s).enumerate() { - places.push(pen); pen += spacing.advance(i, i64::from(code), f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO); if !(pen.x.is_finite() && pen.y.is_finite()) { return ps_err("undefinedresult", "a show"); @@ -640,11 +656,19 @@ impl Interp<'_> { } let start = self.out.drawn.len(); let mut text = String::new(); - for (g, place) in glyphs.iter().zip(places) { + let mut pen = at; + for (i, (g, &code)) in glyphs.iter().zip(s).enumerate() { let glyph = data.glyph(g.gid); self.spend(data.take_work())?; if let Some(outline) = glyph.map(|o| o.0) { - self.type42_paint(outline, place, f1, Draw::Fill)?; + self.type42_paint(outline, pen, f1, Draw::Fill)?; + } + let d = spacing.advance(i, i64::from(code), f1 * Point::new(g.width, 0.0) - f1 * Point::ZERO); + // `kshow`'s procedure moves the point as it runs, so its moves are checked as they come. + let d = d + self.kern(spacing, s, i, pen + d)?; + pen += d; + if !(pen.x.is_finite() && pen.y.is_finite()) { + return ps_err("undefinedresult", "a show"); } text.push(g.ch.unwrap_or('\u{2022}')); } diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 4b84312c3..6fe9230e3 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-07 · **Change:** minor (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-07 · **Change:** minor (EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public @@ -281,7 +281,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, | Illustrator template `.ait` | open, save | yes | PDF-compatible only, from Save as Template (`.vctemplate` stays its default) | New from Template opens a saved `.ait` untitled with its layers and art (`tests_pdfedit.rs`) | | Illustrator `.ai` | open, save | yes (PDF-compatible + editing-data structure with template layers and ruler guides, File Info) | PDF-compatible, or legacy version 7 (layers and groups) | see above; no real-file corpus | | PDF | open, place, save | yes (layers, masks, editable text, security, overprint, File Info from the XMP packet and the document information dictionary; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+; objects' URLs as link annotations, not in PDF/X) | fuzzed import; shared guarded writer and windowed reader ([pdf-backend.md](pdf-backend.md)), metadata, incomplete-content and dependency-source regressions; PDF text opens where the file draws it, glyph by glyph (#722: kerning or none, spacing per character, text rise as baseline shift, vertical writing, justified text boxes); alpha soft masks drawn as images import with the mask (#1551); a clip only counts as covering the page when its shape does (#1542); a PDF another app copied pastes as editable vector art (#1499); letter-spaced type on a path keeps its length and its glyph-by-glyph stroke comes in as one text object (#1586, #1585); a crafted name tree with shared nodes no longer takes exponential time on open (#1781); fonts named with hyphens (`Noto-Sans-Bold`) or with a composite font's CMap (`Name-Identity-H`) find the installed family; invisible text (rendering mode 3: a scanned page's recognized text) opens as type with no fill or stroke and saves back as invisible, searchable text (#2675); PDF/X-1a and X-3 flattening in a CMYK document keeps CMYK inks where it rasterizes (#2160) | -| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed fonts in a FontSet (Compact Font Format) and CIDFonts the file doesn't carry not drawn; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); an embedded Type 1 font's encrypted part is read for its glyphs, which space its type, outline it and draw glyphs without a character, in plain and composite fonts; a Type 42 font that carries its TrueType font gives each code its character by its glyph name or, for names that say nothing (`/g12`), by the glyph's entry in the font's own character map, spaces its type by its own widths and draws glyphs that stand for no character from its outlines; the show operators' spacing along the baseline (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`) is kept on live type as tracking, so type in a font that isn't installed keeps the file's line lengths (spacing that moves characters off the baseline, as `yshow` and `xyshow` can, places each character as its own text object); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and set other descendants' characters as type at the composite font's size (other mappings are still set as type in the font of their name); CID-keyed fonts (`composefont`, `FMapType` 9 over a CMap built with the CIDInit operators or `Identity-H`/`Identity-V`) draw the glyphs of the Type 0 CIDFonts (Type 1 charstrings in their `StartData` binary data or `GlyphDirectory`) and Type 2 CIDFonts (TrueType glyphs through `CIDMap`) they carry; a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) | +| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed fonts in a FontSet (Compact Font Format) and CIDFonts the file doesn't carry not drawn; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); an embedded Type 1 font's encrypted part is read for its glyphs, which space its type, outline it and draw glyphs without a character, in plain and composite fonts; a Type 42 font that carries its TrueType font gives each code its character by its glyph name or, for names that say nothing (`/g12`), by the glyph's entry in the font's own character map, spaces its type by its own widths and draws glyphs that stand for no character from its outlines; the show operators' spacing along the baseline (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`, and the moves of `kshow`'s procedure, run between each two characters) is kept on live type as tracking, so type in a font that isn't installed keeps the file's line lengths (spacing that moves characters off the baseline, as `yshow`, `xyshow` or a `kshow` procedure can, places each character as its own text object); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and set other descendants' characters as type at the composite font's size (other mappings are still set as type in the font of their name); CID-keyed fonts (`composefont`, `FMapType` 9 over a CMap built with the CIDInit operators or `Identity-H`/`Identity-V`) draw the glyphs of the Type 0 CIDFonts (Type 1 charstrings in their `StartData` binary data or `GlyphDirectory`) and Type 2 CIDFonts (TrueType glyphs through `CIDMap`) they carry; a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) | | PostScript `.ps` | open, place | yes (the EPS reader reads its first page; `.epsf` files open and place as EPS) | via Print to PostScript | `tests_epsimport.rs` | | SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527); CSS transforms and the `font` shorthand (#1565), percentage lengths in symbols (#1596), Inkscape flowed text as area type (#1568) and Inkscape pages as artboards (#1601) import; an unsupported filter is named in its warning (#1598); an artboard exported on its own (Export As with Use Artboards, Export for Screens, Asset Export) has the artboard's name as ``, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`) | | DXF | open, export | yes | yes (R12–2018) | R13–2018 closed SPLINE flags and control-point closure on import verified (#1299); circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE while non-circular curves keep the existing fallback (`crates/cad/src/tests.rs`); [Autodesk group 70](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-E1F884F8-AA90-4864-A215-3182D47A9C74.htm). DXF 2000–2018 LAYOUT objects write the paper image origin as 148/149 (was a stray 158), the shade plot codes (2004+) and the model type flag, and the plot style placeholder carries no subclass marker (#3085; structure test `r2000_and_later_files_have_the_structure_cad_apps_require`, checked with ezdxf's audit; not yet confirmed in Autodesk Fusion). CNC preset and VCarve QA remain open | @@ -320,6 +320,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors, | Date | Change | Summary | |---|---|---| +| 2026-11-07 | minor | EPS import: `kshow` (PLRM 3rd ed., chapter 8) runs its procedure between each two characters of type in a base font (not a Type 3 or composite font), with the two character codes, instead of only between a Type 3 font's glyphs: where it moves the current point (kerning), the next character follows, kept on the live type as tracking, and what it draws is drawn over the type. | | 2026-11-07 | minor | EPS import: an embedded Type 1 font's encrypted part (`eexec`, [Adobe Type 1 Font Format](https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf), chapter 7) is no longer only skipped: its charstrings are read, and the font dictionary gets the `CharStrings` dictionary the part would have given it. Its type then takes its characters from its glyph names (the standard and ISO Latin-1 encodings by their codes' names, PLRM 3rd ed., Appendix E), its widths move the current point and space the live type as tracking (as Type 42 fonts do), `stringwidth` and `charpath` read its glyphs, glyphs whose names say no character are drawn from its outlines, and a composite font's Type 1 descendants draw their glyphs. A font found by name, re-encoded with glyph names at other codes (codes 1 to 12 for `T`, `r`, `a`… in pdftocairo's subsets), sets the characters its names say instead of its codes read as Latin-1 (such type was left out). | | 2026-11-07 | minor | EPS import: CID-keyed fonts (PLRM 3rd ed., §5.11; [Adobe Technical Note #5014](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5014.CIDFont_Spec.pdf)) draw from the font data the file carries instead of stopping the program at `composefont` or the CIDInit operators: CMaps built with `begincmap` … `endcmap` (codespace ranges, CID and notdef mappings, `usefont`, `usecmap`; mappings to base-font codes are not read) and the predefined `Identity-H` and `Identity-V`; `composefont` and `FMapType` 9 fonts, whose codes take as many bytes as their codespace range says, an invalid or unmapped code falling back as §5.11.5 says; Type 0 CIDFonts' Type 1 charstrings, read by `StartData` (binary or hexadecimal) or from a `GlyphDirectory`, with their FDArray dictionaries' matrices and subroutines (`Subrs` or the binary data's subroutine table); Type 2 CIDFonts' TrueType glyphs through `CIDMap` (strings, a dictionary or an offset); `Metrics` widths; vertical writing mode set downwards. A CIDFont the file doesn't carry draws nothing, with a warning; FontSet (Compact Font Format) data is skipped. | | 2026-11-07 | minor | EPS import: a plain Type 42 font that carries its TrueType font ([Adobe Technical Note #5012](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5012.Type42_Spec.pdf)) gives each code its character by its glyph name (single characters, `uniXXXX`, the standard encoding's and Windows-1252's names) or, when the name says nothing (`/g12`), by the glyph `CharStrings` gives it in the font's own character map or `post` names, instead of reading the code as Latin-1 (a Euro at code 128 was dropped); the font's own widths move the current point (`stringwidth`, `show`), `charpath` outlines its glyphs, and a string with a glyph that stands for no character is drawn from the font's outlines. Live type keeps the spacing the file gives it: where the show operator's numbers (`ashow`, `widthshow`, `xshow`, `yshow`, `xyshow`: PLRM 3rd ed., chapter 8) or the embedded font's widths differ from the installed or fallback font's advances, each character's tracking makes up the difference, as PDF import does, so justified type in a font that isn't installed no longer drifts (pdftocairo and pdftops EPS of a justified PDF page set in a missing font: 10.2% and 9.0% of the page differing from Ghostscript's render, now 6.2% and 5.8%). Type under `nulldevice` no longer shows. |