diff --git a/crates/eps/src/import/native/plan.rs b/crates/eps/src/import/native/plan.rs index 5dd918fb5..eaa50b6bb 100644 --- a/crates/eps/src/import/native/plan.rs +++ b/crates/eps/src/import/native/plan.rs @@ -1,7 +1,7 @@ use super::layers::closed_where_ends_meet; use super::slots::{ Content, MAX_PAIRS, OUTLINE_REACH, PAGE_TYPE_LEFT_OUT, PAGE_TYPE_UNCHECKED, SlotInfo, assign, covered, extent, join_pieces, letters, same_type, - words, + word_pieces, words, }; use super::{Affine, Arc, Node, NodeId, NodeKind, Point, Rect, Story, TextKind}; @@ -283,9 +283,10 @@ fn plain_text(n: &Node) -> String { if let NodeKind::Text(t) = &n.kind { t.plain_text() } else { String::new() } } -/// The words of the page's text object `o`, each as its [`spelling`], the empty ones left out. +/// The words of the page's text object `o` (see [`word_pieces`]), each as its [`spelling`], the +/// empty ones left out. fn object_words(o: &[Arc]) -> Vec { - o.iter().flat_map(|n| plain_text(n).split_whitespace().map(spelling).collect::>()).filter(|w| !w.is_empty()).collect() + o.iter().flat_map(|n| word_pieces(&plain_text(n)).map(spelling).collect::>()).filter(|w| !w.is_empty()).collect() } /// Each slot's made type as its content, else nothing yet. Made type that shows has where it draws, diff --git a/crates/eps/src/import/native/slots.rs b/crates/eps/src/import/native/slots.rs index f0cda3088..bce43140d 100644 --- a/crates/eps/src/import/native/slots.rs +++ b/crates/eps/src/import/native/slots.rs @@ -446,7 +446,21 @@ pub(super) fn letters(s: &str) -> String { /// the page draws a right-to-left paragraph's words in the order they show (" 120.5 mm " as /// "mm 120.5"), its text object keeps them in the order they are typed. pub(super) fn words(s: &str) -> Vec { - s.split_whitespace().map(letters).filter(|w| !w.is_empty()).collect() + word_pieces(s).map(letters).filter(|w| !w.is_empty()).collect() +} + +/// The words of `s`, split at spaces and at private-use characters too. The page's type names a +/// font's ligature glyph that has no character of its own (`uniE028`, the Adobe Glyph List +/// specification's `uniXXXX` names: ) by a +/// private-use character, where its text object has the letters ("Qual\u{E028}y" for +/// "Quality"): what is around it is compared, each piece as a word. +pub(super) fn word_pieces(s: &str) -> impl Iterator { + s.split(|c: char| c.is_whitespace() || private_use(c)).filter(|w| !w.is_empty()) +} + +/// Is `c` in one of Unicode's private use areas (Unicode Standard §23.5)? +fn private_use(c: char) -> bool { + matches!(u32::from(c), 0xE000..=0xF8FF | 0xF0000..=0xFFFFD | 0x100000..=0x10FFFD) } /// Does `n` hold type that is where `story` lays one of its lines out: the line, or a piece of it diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index a9bb8cf27..d5946b008 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -4661,6 +4661,22 @@ fn a_small_label_of_the_page_in_no_text_object_is_noted() { assert!(note.is_some_and(|w| crate::is_loss(w)), "{:?}", r.warnings); } +/// A ligature the page shows by a glyph its font's encoding names `uniE028` (a private-use +/// character, without the letters it stands for) is the letters of its text object: the page's type +/// has one, and nothing is noted as left out. +#[test] +fn a_ligature_glyph_without_letters_is_matched_to_its_text_object() { + let doc = text_document(&[("Quality here\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); + let art = native_with_text(&layer("Art", true, &text_object(0)), &doc); + let page = "0 0 0 1 setcmykcolor /Helvetica findfont dup length dict copy \ + dup /Encoding StandardEncoding 256 array copy dup 127 /uniE028 put put /Lig exch definefont \ + 14 scalefont setfont 58.5 61.5 moveto (Qual\\177y here) show"; + let r = import(&eps(page, &art)).unwrap(); + assert_eq!(names(&r.document), ["Art"], "{:?}", r.warnings); + assert!(!r.warnings.iter().any(|w| w.contains("so it is left out")), "{:?}", r.warnings); + assert_eq!(text_of(&r.document.layers[0]).iter().map(|t| t.plain_text()).collect::>(), ["Quality here"], "{:?}", r.warnings); +} + /// A line break inside a paragraph (`\x03` in the text document) stays a line break in it. #[test] fn a_soft_return_is_a_line_break_in_its_paragraph() { diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 32478e930..9b86156bf 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: 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-07 · **Change:** minor (EPS and `.ai` editing data: a ligature the page names by a private-use glyph matches its text object; EPS import: CIDFonts only named, over a Unicode CMap, set as live type; EPS import: CMap mappings to base-font codes and glyph names; EPS import: CID-keyed fonts in FontSets (CFF) draw their glyphs; EPS import: `kshow` procedures kern live type; EPS import: embedded Type 1 fonts' glyphs and widths; EPS import: CID-keyed fonts draw their glyphs; EPS import: Type 42 fonts' characters and widths from the font they carry, show operators' spacing kept as tracking; EPS import: an image decoder's own buffers stop counting against the memory budget once it is decoded; EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x) Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`, `OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public @@ -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 and `.ai` editing data: when the page's type is matched with the text objects, a glyph the page's font encoding names with a private-use character (a ligature such as `uniE028`, which says nothing of the letters it stands for) splits the page's word there, and the pieces are matched as words. A ligature in "Quality" no longer reads "Qualy" against the text object: four users' Illustrator 30 EPS files (used locally, not committed) lose the false "some of the page's type has no text object" note, their renders unchanged | | 2026-11-07 | minor | EPS import: a CID-keyed font over a predefined Unicode CMap the file doesn't define (Adobe's public CMap resources named `Uni…-UCS2`, `-UTF8`, `-UTF16` or `-UTF32`, `-H` or `-V`: [cmap-resources](https://github.com/adobe-type-tools/cmap-resources)), found by its `CIDFont-CMap` name (`/Ryumin-Light-UniJIS-UCS2-H findfont`, the usual name of a CID-keyed font: its CIDFont's and CMap's names joined by a hyphen) or made with `composefont`, sets its codes' characters as live type in the font of its CIDFont's name, and `stringwidth` measures them. Such type was read as Latin-1 codes, or stopped the program at `composefont`. Vertical CMaps are set horizontally, and the show operators' extra spacing isn't kept; CIDFonts over other CMaps the file doesn't carry still draw nothing, with a warning. | | 2026-11-07 | minor | EPS import: CMap mappings to base fonts (`beginbfchar` … `endbfchar`, `beginbfrange` … `endbfrange` after `usefont`: [Adobe Technical Note #5099](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5099.CMapResources.pdf); PLRM 3rd ed., §5.11.4) are read instead of dropped: a code mapped to a character code (a range's codes counting up from its last byte) or to a glyph name (one, or an array of them) draws that glyph of the base font it names, a Type 3, Type 42 or embedded Type 1 font's own glyph or, in a font found by name, its character as type. Such codes took font 0's CID 0 and drew nothing. | | 2026-11-07 | minor | EPS import: a FontSet's Compact Font Format data (`/name n StartData` in the FontSetInit procedure set: PLRM 3rd ed., §5.8.1; [Adobe Technical Note #5176](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf)) is read instead of skipped: each CID-keyed font in it (a top DICT with `ROS`) is defined as a CIDFont of its name, and type in a CID-keyed font over it draws its glyphs, picked by CID through the charset and drawn from their Type 2 charstrings with their FDSelect subfont's subroutines and widths. Name-keyed fonts in a FontSet are still set as type by name. |