From 530605d4fc86c9cbb9faaf56c68e23b0fc24e58c Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:23:22 +0300 Subject: [PATCH] fix(ai-import): keep All Caps and Small Caps on type from the text document Character attribute 12 of the editing data's text document is the case (1 Small Caps, 2 All Caps). The reader skipped it, so type set in All Caps came in as typed, in lower or mixed case where the page draws capitals. Map it onto CharStyle::all_caps and CharStyle::small_caps. --- crates/eps/src/import/ate.rs | 7 +++++++ crates/eps/src/import/tests_native.rs | 24 ++++++++++++++++++++++++ docs/file-format-parity.md | 3 ++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/eps/src/import/ate.rs b/crates/eps/src/import/ate.rs index 7c90b15aa..21198305d 100644 --- a/crates/eps/src/import/ate.rs +++ b/crates/eps/src/import/ate.rs @@ -296,6 +296,8 @@ struct Style { fill: Option, stroke: Option, stroke_width: f64, + /// Key 12, the case: 1 Small Caps, 2 All Caps. + caps: u8, } /// The text document of an editing copy. @@ -476,6 +478,7 @@ impl Texts { fill: if on("56") { Some(pick("53").and_then(paint).unwrap_or(Color::BLACK)) } else { None }, stroke: if matches!(pick("57"), Some(Val::Bool(true))) { pick("54").and_then(paint) } else { None }, stroke_width: num("63").filter(|v| *v >= 0.0 && *v < 1e4).unwrap_or(1.0), + caps: num("12").filter(|v| *v == 1.0 || *v == 2.0).map_or(0, |v| v as u8), } } } @@ -824,6 +827,9 @@ fn char_style(s: &Style, scale: f64, leading: Option) -> CharStyle { fill: s.fill.map_or(Paint::None, Paint::solid), stroke: s.stroke.map_or(Paint::None, Paint::solid), stroke_width: if s.stroke.is_some() { s.stroke_width * scale } else { 0.0 }, + all_caps: s.caps == 2, + // At the default Small Caps size of Document Setup: the file's own isn't read. + small_caps: (s.caps == 1).then(|| vectorcraft_doc::DocSetup::default().small_caps_size), ..CharStyle::default() } } @@ -1006,6 +1012,7 @@ mod tests { fill: Some(Color::BLACK), stroke: None, stroke_width: 1.0, + caps: 0, }, )], } diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 200bf0404..db7960c9f 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -685,6 +685,30 @@ fn type_on_a_hidden_layer_comes_from_the_text_document() { assert!(!r.warnings.iter().any(|w| w.contains("couldn't be read")), "{:?}", r.warnings); } +#[test] +fn type_from_the_text_document_keeps_its_case_setting() { + // Key 12 of a character style is its case: 1 Small Caps, 2 All Caps. The characters stay as + // typed; the setting shows them as capitals. + let doc = text_document(&[ + ("hon. guest\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0), + ("Book Now\r", 0, (8200.0, 8170.0), (0.0, 0.0), 0.0), + ("plain\r", 0, (8200.0, 8160.0), (0.0, 0.0), 0.0), + ]) + .replacen("/1 14.0 /53", "/1 14.0 /12 2 /53", 1) + .replacen("/1 14.0 /53", "/1 14.0 /12 1 /53", 1); + let spare = text_object(0) + &text_object(1) + &text_object(2); + let art = native_with_text(&(layer("Back", true, &square(10, 10)) + &layer("Spare", false, &spare)), &doc); + let r = import(&eps(&page_square(10, 10), &art)).unwrap(); + let texts = text_of(&r.document.layers[1]); + assert_eq!(texts.len(), 3, "{:?}", r.warnings); + let case = |i: usize| texts[i].runs.iter().map(|r| (r.style.all_caps, r.style.small_caps)).collect::>(); + assert_eq!(texts[0].plain_text(), "hon. guest"); + assert_eq!(case(0), [(true, None)]); + assert_eq!(texts[1].plain_text(), "Book Now"); + assert_eq!(case(1), [(false, Some(r.document.setup.small_caps_size))]); + assert_eq!(case(2), [(false, None)]); +} + #[test] fn type_with_a_frame_it_cant_read_is_left_out_with_a_note() { // A story whose frame is of a kind this doesn't know isn't guessed at. diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 45eb9f834..6961c1983 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-30 · **Change:** minor (`.ai` import: placed files at their box's corner where that draws as the page) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`.ai` import: All Caps and Small Caps type from the editing data keeps its case setting) · **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 @@ -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-30 | minor | `.ai` import: type read from the editing data's text document keeps its case setting (character attribute 12: 1 Small Caps, at Document Setup's default size, 2 All Caps); it came in as typed, so a heading set in All Caps showed in lower or mixed case where the page draws capitals. Test `import::tests_native::type_from_the_text_document_keeps_its_case_setting`; checked locally against a user's files; scores unchanged | | 2026-10-30 | minor | PDF import: a patch or triangle mesh shading of up to 16,384 patches comes in as gradient meshes (it was 4,096; Illustrator writes up to 14,355 in files users sent), and a stroke painted with one (how Illustrator writes a freeform gradient on a stroke) is the meshes clipped to the area the stroke paints, with its width, caps, joins and dashes; both came in as a flat grey. In two files of a user's set, the formerly grey area now matches a reference rendering in 99% of its pixels. Own-fixture tests in `tests::import_fidelity`; scores unchanged | | 2026-10-30 | minor | `.ai`/EPS editing data: gradient meshes the reader marked unread, which made the whole file come in as its page or PDF part, are read: a mesh starts grey and its `/CS` is written only where it changes, before the corner it applies to (within a patch too), so each corner takes the space last set before it; named colours (`[c m y k /DeviceCMYK (name) /Process]` or `/Spot`) paint at the corner's tint and `/NChannel` inks overprint at theirs; meshes up to 128 patches a side and 16,384 patches are read (files users sent had up to 97 × 72). Ten of eleven such files from a user's set now open from their layers; meshes with handle weights other than 1 still don't. Own-fixture tests in `import::ai::mesh`; scores unchanged | | 2026-10-30 | minor | `.ai` import: a placed (linked) file is put where its matrix takes the canvas point of art space's origin, and, when the layers then don't draw as the page, at the corner of the box it is written with, which many files use; six files of a user's set whose photos landed off their artboards now open from their layers. Own-fixture test in `import::tests_native`; scores unchanged |