diff --git a/crates/pdf/src/import_scan.rs b/crates/pdf/src/import_scan.rs index dac1a7b64..9263bcd5a 100644 --- a/crates/pdf/src/import_scan.rs +++ b/crates/pdf/src/import_scan.rs @@ -634,11 +634,11 @@ impl Walker<'_, '_> { fn note_fonts(&mut self, res: &Resources<'_>) { for key in res.fonts.keys() { if let Some(f) = res.fonts.get::>(key.as_ref()) - && let Some(name) = f.get::>(b"BaseFont") + && let Some(name) = font_name(&f) { self.fonts.entry(f.cache_key()).or_insert_with(|| { let embedded = embeds_glyphs(&f); - FontNote { name: String::from_utf8_lossy(name.as_ref()).into_owned(), embedded, vertical: vertical_metrics(&f, embedded) } + FontNote { name, embedded, vertical: vertical_metrics(&f, embedded) } }); } } @@ -1009,6 +1009,24 @@ fn cid_ranges(a: &Array<'_>, per: usize, mut each: impl FnMut(u32, &[f64])) { } } +/// The PostScript name of font `f`: its `BaseFont`. A composite (Type 0) font's `BaseFont` is its +/// CIDFont's name, a hyphen and its CMap's (`Name-Identity-H`, ISO 32000-1 §9.7.6.1), so its +/// descendant CIDFont's `BaseFont` is taken, the font program's own name (§9.7.4.1); without one, +/// the `BaseFont` without the CMap's name on its end. +fn font_name(f: &Dict<'_>) -> Option { + let base = String::from_utf8_lossy(f.get::>(b"BaseFont")?.as_ref()).into_owned(); + if f.get::>(b"Subtype").is_none_or(|s| s.as_ref() != b"Type0") { + return Some(base); + } + let descendant = f.get::>(b"DescendantFonts").and_then(|a| a.iter::>().next()); + if let Some(name) = descendant.and_then(|d| d.get::>(b"BaseFont")).filter(|n| !n.as_ref().is_empty()) { + return Some(String::from_utf8_lossy(name.as_ref()).into_owned()); + } + let cmap = f.get::>(b"Encoding").map(|n| String::from_utf8_lossy(n.as_ref()).into_owned()); + let stripped = cmap.and_then(|c| base.strip_suffix(c.as_str())?.strip_suffix('-').filter(|b| !b.is_empty()).map(str::to_string)); + Some(stripped.unwrap_or(base)) +} + /// Whether font `f` carries its own glyphs: a font program in its descriptor (a composite font's in /// its descendant's), or a Type 3 font's glyph procedures. A font that doesn't is drawn with a /// stand-in, whose glyphs say nothing about which installed face the document means. diff --git a/crates/pdf/src/import_text.rs b/crates/pdf/src/import_text.rs index dddb0075e..8157759e7 100644 --- a/crates/pdf/src/import_text.rs +++ b/crates/pdf/src/import_text.rs @@ -965,6 +965,12 @@ impl Families { Self(vectorcraft_text::FontDb::global().families().into_iter().map(|f| (norm(&f), f)).collect()) } + /// Just the families `names`, for tests that mustn't depend on the fonts installed. + #[cfg(test)] + pub fn of(names: &[&str]) -> Self { + Self(names.iter().map(|f| (norm(f), f.to_string())).collect()) + } + /// The family and style of a font with base (PostScript) name `name`, or of weight `weight` /// and slant `italic` when the name has no style; whether the family is available. pub fn resolve(&self, name: &str, weight: Option, italic: bool) -> (String, String, bool) { @@ -977,22 +983,74 @@ impl Families { if let Some((family, style)) = vectorcraft_text::FontDb::global().by_postscript_name(name) { return (family, style, true); } - let (fam, style) = name.split_once(['-', ',']).unwrap_or((name, "")); - let found = [fam, strip_ps(fam)].iter().find_map(|f| self.0.get(&norm(f)).cloned()); - let style = match spaced(strip_ps(style)).as_str() { - "" | "Roman" | "Book" | "Normal" | "Plain" => match (weight.is_some_and(|w| w >= 600) || style.contains("Bold"), italic) { - (true, true) => "Bold Italic".to_string(), - (true, false) => "Bold".to_string(), - (false, true) => "Italic".to_string(), - (false, false) => "Regular".to_string(), + // The longest part of the name before a hyphen (or the comma of `Arial,Bold`, ISO 32000-1 + // §9.6.3) that names an available family, the rest its style: a family can hold hyphens + // (`Noto-Sans-Bold` is Noto Sans, Bold). The name is the file's: a few cuts from its end are + // enough for any family, and each costs a pass over the name. + let cuts = std::iter::once(name.len()).chain(name.rmatch_indices(['-', ',']).map(|(i, _)| i).take(16)); + for cut in cuts { + let (fam, style) = (name.get(..cut).unwrap_or(name), name.get(cut + 1..).unwrap_or("")); + if let Some(f) = [fam, strip_ps(fam)].iter().find_map(|f| self.0.get(&norm(f)).cloned()) { + return (f, style_of(style, weight, italic), true); + } + } + // Not available: split at a comma, or before the first part that is a style + // (`Helvetica-BoldOblique`, `Noto-Sans-Bold`, `AGaramondPro-Bold-SC700`); otherwise the + // whole name is the family (`Noto-Sans`, `MS-PGothic`). + let style_at = |i: usize| name.get(i + 1..).and_then(|rest| rest.split('-').next()).is_some_and(|part| is_style(strip_ps(part))); + let (fam, style) = match name.split_once(',') { + Some(split) => split, + None => match name.match_indices('-').map(|(i, _)| i).find(|&i| style_at(i)) { + Some(i) => (name.get(..i).unwrap_or(name), name.get(i + 1..).unwrap_or("")), + None => (name, ""), }, - s => s.to_string(), }; - match found { - Some(f) => (f, style, true), - None => (spaced(strip_ps(fam)).trim().to_string(), style, false), + let family = spaced(strip_ps(fam)).split(['-', ' ']).filter(|w| !w.is_empty()).collect::>().join(" "); + (family, style_of(style, weight, italic), false) + } +} + +/// The style a PostScript name's `style` part names, or the one of weight `weight` and slant +/// `italic` when the part names none (`Roman`, `Book`…). +fn style_of(style: &str, weight: Option, italic: bool) -> String { + match spaced(strip_ps(style)).as_str() { + "" | "Roman" | "Book" | "Normal" | "Plain" => match (weight.is_some_and(|w| w >= 600) || style.contains("Bold"), italic) { + (true, true) => "Bold Italic".to_string(), + (true, false) => "Bold".to_string(), + (false, true) => "Italic".to_string(), + (false, false) => "Regular".to_string(), + }, + s => s.to_string(), + } +} + +/// Whether `s` is made of style words and their usual abbreviations (`BoldOblique`, `SemiboldIt`, +/// `XBold`, `W6`), any case: the last part of a font name that names its style. +fn is_style(s: &str) -> bool { + const WORDS: &str = "bd bk black blk bold book cn cnd compressed con condensed demi ex expanded ext extended extra hairline heavy hv it ita italic light lt md \ + med medium narrow normal obl oblique plain reg regular rg roman sb semi slanted smb super th thin ul ultra w wide x xb xl"; + let s = s.to_ascii_lowercase(); + let s = s.as_bytes(); + // Which starts of `s` the words and digit runs before them reach (a word break, so + // `XBold` is X, Bold even though `xb` also starts it). + let mut reached = vec![false; s.len() + 1]; + if let Some(r) = reached.first_mut() { + *r = !s.is_empty(); + } + for i in 0..s.len() { + if !reached.get(i).copied().unwrap_or(false) { + continue; + } + let rest = s.get(i..).unwrap_or_default(); + let digits = rest.iter().take_while(|b| b.is_ascii_digit()).count(); + let ends = WORDS.split_ascii_whitespace().filter(|w| rest.starts_with(w.as_bytes())).map(|w| i + w.len()); + for end in ends.chain((digits > 0).then_some(i + digits)) { + if let Some(r) = reached.get_mut(end) { + *r = true; + } } } + reached.last().copied().unwrap_or(false) } /// Hebrew or Arabic comes from a PDF in visual order (each glyph where it is drawn): put `t`'s text diff --git a/crates/pdf/src/tests/import_fidelity.rs b/crates/pdf/src/tests/import_fidelity.rs index e08d2e74a..e60f3e98c 100644 --- a/crates/pdf/src/tests/import_fidelity.rs +++ b/crates/pdf/src/tests/import_fidelity.rs @@ -406,6 +406,74 @@ fn font_names_resolve_to_available_families() { assert_eq!((family.as_str(), style.as_str(), found), ("Missing Sans Pro", "Bold Italic", false)); } +/// A family can hold hyphens (`Noto-Sans-Bold` is Noto Sans, Bold): the longest part of the name +/// before a hyphen that names an available family is the family, the rest its style. Synthetic +/// family names, so the host's fonts don't matter. +#[test] +fn hyphenated_font_names_resolve_to_the_longest_available_family() { + let f = crate::import_text::Families::of(&["Vctest", "Vctest Sans", "Vctest Sans CJK JP", "Vctest Mono"]); + let ok = |family: &str, style: &str| (family.to_string(), style.to_string(), true); + assert_eq!(f.resolve("HUHQIP+Vctest-Sans", None, false), ok("Vctest Sans", "Regular")); + assert_eq!(f.resolve("Vctest-Sans-Bold", None, false), ok("Vctest Sans", "Bold")); + assert_eq!(f.resolve("Vctest-Sans-Italic", None, false), ok("Vctest Sans", "Italic")); + assert_eq!(f.resolve("Vctest-Sans", Some(700), true), ok("Vctest Sans", "Bold Italic")); + assert_eq!(f.resolve("Vctest-Sans-CJK-JP", None, false), ok("Vctest Sans CJK JP", "Regular")); + // The family is there but not the style: the family, which then draws its closest style. + assert_eq!(f.resolve("Vctest-Mono-Bold", None, false), ok("Vctest Mono", "Bold")); + // A shorter family still takes the name when no longer one is available. + assert_eq!(f.resolve("Vctest-BoldOblique", None, false), ok("Vctest", "Bold Oblique")); + assert_eq!(f.resolve("Vctest,Bold", None, false), ok("Vctest", "Bold")); +} + +/// A missing font's name is split only at a comma (`Arial,Bold`) or before the first part that is +/// a style (`Family-BoldOblique`); otherwise the whole name is the family, so the missing font is +/// reported as `Vcmissing Sans`, not `Vcmissing`. +#[test] +fn a_missing_font_is_split_only_before_a_style() { + let f = crate::import_text::Families::of(&[]); + let missing = |family: &str, style: &str| (family.to_string(), style.to_string(), false); + assert_eq!(f.resolve("Vcmissing-Sans", None, false), missing("Vcmissing Sans", "Regular")); + assert_eq!(f.resolve("Vcmissing-Sans-CJK-JP", None, false), missing("Vcmissing Sans CJK JP", "Regular")); + assert_eq!(f.resolve("Vcmissing-Sans-BoldOblique", None, false), missing("Vcmissing Sans", "Bold Oblique")); + assert_eq!(f.resolve("VcmissingPro-SemiboldIt", None, false), missing("Vcmissing Pro", "Semibold It")); + assert_eq!(f.resolve("VcmissingPSMT-XBold", None, false), missing("Vcmissing", "XBold")); + assert_eq!(f.resolve("Vcmissing-Bold-SC700", None, false), missing("Vcmissing", "Bold-SC 700")); + assert_eq!(f.resolve("Vcmissing-Regular-Regular", None, false), missing("Vcmissing", "Regular-Regular")); + assert_eq!(f.resolve("Vcmissing,Bold", None, false), missing("Vcmissing", "Bold")); + assert_eq!(f.resolve("-Vcmissing", None, false), missing("Vcmissing", "Regular")); + assert_eq!(f.resolve("Vcmissing-Roman", Some(700), false), missing("Vcmissing", "Bold")); + assert_eq!(f.resolve("Vcmissing-", None, true), missing("Vcmissing", "Italic")); + assert_eq!(f.resolve("-Bold", None, false), missing("", "Bold")); +} + +/// A composite (Type 0) font's base name is its CIDFont's name, a hyphen and its CMap's +/// (`SomeFont-Italic-Identity-H`, ISO 32000-1 §9.7.6.1): the font is noted by its CIDFont's name, +/// the PostScript name of the font program (§9.7.4.1), so it is found among the installed fonts. +#[test] +fn a_composite_font_is_noted_by_its_cid_font_name() { + let noted = |font: &str| { + let resources = format!("/Font << /F1 {font} >>"); + let bytes = one_page("BT /F1 24 Tf 10 30 Td <0001> Tj ET", &resources, &[]); + let pdf = craft_pdf::syntax::Pdf::new(bytes).unwrap(); + let mut fonts = crate::import_scan::FontNames::new(); + let mut ocgs = crate::import_scan::Ocgs::read(&pdf); + crate::import_scan::scan_page(&pdf.pages()[0], &mut ocgs, false, &mut fonts, pdf.decode_limits().max_decoded_stream()).unwrap(); + fonts.into_values().map(|f| f.name).collect::>() + }; + let type0 = |base: &str, enc: &str, descendant: &str| { + let info = "/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>"; + format!( + "<< /Type /Font /Subtype /Type0 /BaseFont /{base} /Encoding /{enc} /DescendantFonts [<< /Type /Font /Subtype /CIDFontType0 {descendant} {info} >>] >>" + ) + }; + assert_eq!(noted(&type0("MGZJFV+Vctest-Italic-Identity-H", "Identity-H", "/BaseFont /MGZJFV+Vctest-Italic")), ["MGZJFV+Vctest-Italic"]); + // No name in the descendant: the CMap's name comes off the end of the base name. + assert_eq!(noted(&type0("Vctest-Italic-UniJIS-UCS2-H", "UniJIS-UCS2-H", "")), ["Vctest-Italic"]); + // A base name without the CMap's is kept, as is a simple font's. + assert_eq!(noted(&type0("Vctest-Italic", "Identity-H", "")), ["Vctest-Italic"]); + assert_eq!(noted("<< /Type /Font /Subtype /Type1 /BaseFont /Vctest-Identity-H >>"), ["Vctest-Identity-H"]); +} + /// A value of the page's font dictionary that the reader can't parse (a name with a broken `#` /// escape, a number with a stray `-`) is left out when the page's fonts are noted, and the fonts /// after it are noted. diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index fac518fdd..ce464cc58 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-31 · **Change:** minor (DXF export: LAYOUT objects write only DXF Reference group codes, for strict readers such as Autodesk Fusion, #3085) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (PDF import: fonts named with hyphens or with a composite font's CMap find the installed family) · **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 @@ -94,7 +94,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | VectorCraft `.vectorcraft` / `.vctemplate` | — | yes | yes | lossless JSON, versioned, save down to v1; property-tested round trips (`format/tests/prop_format.rs`); documents with a lot of path data are written as compact JSON, about a fifth of the size (#1492); a save falls back to a plain fsync and to writing elsewhere and copying in when the folder refuses the usual steps (#1625, #1649, part of #1620; not confirmed on a real network share); the size limits on embedded data also apply to files read from memory (#1783), and saving over a private file on Unix leaves no readable temporary copy beside it (#1778) | | 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, 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+) | 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); 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) | +| 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+) | 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 CJK type not read; #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); 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); 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`) | @@ -132,6 +132,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | Date | Change | Summary | |---|---|---| +| 2026-10-31 | minor | PDF import finds an installed font whose PDF name holds hyphens (`Noto-Sans-Bold` is Noto Sans, Bold: the longest part before a hyphen that names an installed family) and a composite font by its CIDFont's name rather than its base name, which ends in the CMap's (`Name-Identity-H`, [ISO 32000-1 §9.7.6.1](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)); a missing font is split only before a style part (`Helvetica-BoldOblique`), so it is reported as `MS PGothic`, not `MS`; scores unchanged (estimated) | | 2026-10-31 | minor | DXF export (2000–2018): the LAYOUT objects wrote the paper image origin as a point at group codes 148 and 158, but PLOTSETTINGS defines 148 and 149 and no 158, so strict readers could reject the file (Autodesk Fusion reported "DXF File failed to insert", #3085). They now write 148/149, the shade plot mode, resolution and DPI (76–78, 2004 and later) and flag 1024 on the Model layout; the plot style ACDBPLACEHOLDER no longer carries a subclass marker. Test `r2000_and_later_files_have_the_structure_cad_apps_require` (required sections and table entries, handles and references, LAYOUT group codes); exports pass ezdxf's recover and audit; not tested in Fusion itself; scores unchanged | | 2026-10-31 | minor | `.ai`/EPS editing data: an envelope whose contents are being edited is written the other way round from one that isn't: its contents, a group on `%_` lines, then its dictionary, which holds the mesh that shapes them. Those contents were read as art, so the undistorted art drew over the distorted art the page draws. The group on `%_` lines before an envelope's dictionary is now left out whatever it holds (text in it stays as hidden text, as the dictionary's own does), as the envelope's mesh already was (#2405). Test `import::ai::tests::an_envelope_s_contents_being_edited_are_not_read_as_art`; checked locally against a user's file: it opens from its layers, 0.0% from its page (it was 9.7%, over the 5% limit); scores unchanged | | 2026-10-31 | minor | `.ai`/EPS editing data: a transparency (`Xy`) set by art inside a dictionary, as an area text's frame (its `/ConfiningPath`) or a mask's art sets one, stays the current one after the dictionary: the writer goes on from it, so the objects after such a text have no `Xy` of their own. It was dropped when the dictionary closed, and a user's file drew its logo, calligraphy, footer and type after an area text with the Lighten of the glow before it, so they faded into the light background. Test `import::ai::tests::a_transparency_set_in_a_dictionary_s_art_stays_after_it`; checked locally against that file: 8.1% → 1.7% from its page, so it opens from its layers (5% is the limit); scores unchanged | diff --git a/docs/gaps.md b/docs/gaps.md index 16da46629..eb8d42b54 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (G13: Direct Selection snaps a dragged line end square to straight paths and grazing curves; G13: gradient presets done; G1: Average & Join with a corner or smooth choice, Cmd+Alt+Shift+J, done; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (G4: PDF import finds installed fonts whose PDF names hold hyphens or a composite font's CMap; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -330,6 +330,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall - **Improved:** mixed paragraph alignments leave the existing Paragraph/Properties alignment buttons unselected; choosing an alignment still uses the range-aware command. Headless regressions, full integration CI and visual review pass. Other mixed paragraph fields remain; estimate unchanged. - **Improved:** Paragraph panel left/right/first-line indents and space before/after show blank when paragraphs in scope differ; uniform values still display and edits apply through `text.setFormat`. Headless regression in `panels/paragraph.rs`; estimate unchanged. - **Improved (#1258):** full font names (name ID 4) resolve the face instead of falling back, and Missing Fonts searches recognize the same aliases. Synthetic regressions cover loaded/cataloged fonts, localized names, real-family precedence and variable default faces. Windows font-service activation still needs reporter QA; estimate unchanged. +- **Improved:** PDF import finds an installed font whose PDF name holds hyphens (`Noto-Sans-Bold`, `Noto-Sans-CJK-JP`) or a composite font's CMap (`SomeFont-Italic-Identity-H`; ISO 32000-1 §9.7.6.1), and names a missing one by the whole name unless a style part follows (`MS-PGothic` is MS PGothic). Synthetic regressions in `crates/pdf/src/tests/import_fidelity.rs`; estimate unchanged. - **Done:** Paragraph panel menu › Roman Hanging Punctuation (#2756), per paragraph; estimate unchanged. - **Improved:** hanging punctuation (Roman Hanging Punctuation and Optical Margin Alignment) keeps its published amounts when justification scales glyphs, and Standard burasagari counts a hung line start as room; paragraph-level tests with both composers; estimate unchanged. - **Done:** Paragraph panel menu › Justification… (#2755): word and letter spacing, glyph scaling (Minimum, Desired, @@ -675,6 +676,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | Date | Change | Summary | |---|---|---| +| 2026-10-31 | minor | G4: PDF import finds an installed font whose PDF name holds hyphens (`Noto-Sans-Bold` is Noto Sans, Bold: the longest part before a hyphen that names an installed family) and a composite font by its CIDFont's name rather than its base name, which ends in the CMap's (`Name-Identity-H`, [ISO 32000-1 §9.7.6.1](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)); a missing font is split only before a style part (`Helvetica-BoldOblique`), so it is reported as `MS PGothic`, not `MS`. Estimate unchanged. | | 2026-10-31 | minor | G13: Direct Selection snaps the end of an existing straight line, dragged by its end anchor, square to straight paths and grazing curves (Smart Guides › Geometric Guides: Snap to Perpendicular and Snap to Tangent), the case the public Snap to Perpendicular and Snap to Tangent pages add to drawing with the Line Segment and Pen tools (#2530). Scores and estimates unchanged. | | 2026-10-31 | minor | G8: a gradient mesh read from a file is budgeted: at most about a million quads per tessellation, and a mesh with more patches than that is malformed (#3592); estimate unchanged. | | 2026-10-30 | minor | G9: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. | diff --git a/docs/type-parity.md b/docs/type-parity.md index dbf195b8a..4de41b37c 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Highlight Alternate Glyphs: an optional feature (such as `salt`) that changes a lam-alef ligature is highlighted again, only the required ligature staying clear, and a lam-alef with any combining mark (Arabic Extended-A/B/C included) counts as the ligature (#394); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (PDF import finds installed fonts whose PDF names hold hyphens or a composite font's CMap; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the @@ -71,6 +71,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-10-31 | minor | PDF import finds an installed font whose PDF name holds hyphens (`Noto-Sans-Bold` is Noto Sans, Bold: the longest part before a hyphen that names an installed family) and a composite font by its CIDFont's name rather than its base name, which ends in the CMap's (`Name-Identity-H`, [ISO 32000-1 §9.7.6.1](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)); a missing font is split only before a style part (`Helvetica-BoldOblique`), so it is reported as `MS PGothic`, not `MS`; scores unchanged | | 2026-10-31 | minor | Highlight Alternate Glyphs: an optional feature (such as `salt`) that changes a lam-alef ligature is highlighted again, only the required ligature staying clear, and a lam-alef with any combining mark (Arabic Extended-A/B/C included) counts as the ligature (#394); scores unchanged | | 2026-10-30 | minor | Highlight Alternate Glyphs leaves the required lam-alef ligature unhighlighted also in older Arabic fonts (such as legacy Arial) that put it in `liga` rather than `rlig` (#394); scores unchanged | | 2026-10-30 | minor | Character panel menu › Show Snap to Glyph Options shows the Snap to Glyph section (Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides, Anchor Point; `view.snapToGlyph.options`) (#4232); scores unchanged |