From ab86a62ea0f70cf855108c11302debf939e482b2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 5 Nov 2026 04:09:01 -0800 Subject: [PATCH] .ai import: type with the older TS, Te and TM operators opens The version 7 and version 3 file format specifications list three text operators the editing-data reader did not dispatch, so a file whose type used any of them on a visible layer did not open from its editing data: - `(text) justified TS` (special characters): the reader adds its characters to the story, as it does for `Tx` and `Tj`. - `Te` (end render) is read and ignored. The reader accepts it with any number of operands. - `TM` (pop text matrix) is read and ignored. Tests cover each operator in a text object, `TS` without a string and outside a text object, and `Te` inside a tab definition. The import fuzz tests add the operators to the legacy type tokens. --- crates/engine/tests/import_fuzz.rs | 5 +++++ crates/eps/src/import/ai/read/ops.rs | 6 +++++- crates/eps/src/import/ai/tests.rs | 23 +++++++++++++++++++++++ docs/file-format-parity.md | 3 ++- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 220bb8667..7159c8b1f 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -4053,6 +4053,11 @@ fn arb_ai_token() -> impl Strategy { "(\\r) TX", "() Tj", "(x) Tx", + "(\\t) 1 TS", + "() /x TS", + "Te", + "0 0 1e308 -1e308 Te", + "TM", "7 Tr", "-1e308 Ts", "1e308 0 Tz", diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index 906ccdbb1..9f58cd90d 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -356,11 +356,15 @@ impl<'a> Reader<'a> { "Tp" => self.begin_text_path(&vals), "TP" => self.end_text_path(), "TO" => self.end_text(hidden)?, - "Tx" | "Tj" | "TX" => self.text_body(w, &vals), + // `(text) justified TS`, special characters (§22.6): the reader adds its characters like `Tx`. + "Tx" | "Tj" | "TX" | "TS" => self.text_body(w, &vals), "Tr" | "Tf" | "Ts" | "Tz" | "Tt" | "TA" | "Tl" | "Ta" | "Ti" | "TC" | "TW" | "Tq" | "Th" => self.text_attr(w, &vals), // Attributes the reader doesn't keep (kerning pairs, Far-Eastern type) and final-form // positions (§15.3). "Tk" | "TK" | "Tc" | "Tw" | "Tm" | "Td" | "T*" | "TR" | "T+" | "T-" | "TV" | "Tv" | "Ty" | "TY" | "TG" | "Tg" | "Tu" | "Xu" => {} + // `TM` (pop text matrix) and `Te` (end render), listed in §22.6, keep nothing either; the + // reader takes `Te` with any number of operands. + "TM" | "Te" => {} // The hyphenation language and tab rulers, which the reader doesn't keep either (§22): `XL`, // `Xb`/`XB` a tab definition (and the `Xe` tab descriptions between them). "XL" | "Xb" | "XB" | "Xe" => {} diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 20c2b6e1c..b42e158f6 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -3001,6 +3001,29 @@ fn legacy_type_attributes_with_bad_operands_stay_as_they_were() { assert_eq!(bad.xf, good.xf); } +/// `(text) justified TS` (special characters, §22.6) adds its characters to the story like `Tx` +/// and `Tj`, whichever its flag. Without a string, or outside a text object, it adds nothing. +#[test] +fn legacy_special_characters_join_the_story() { + let texts = legacy_texts(&point_type("", "One").replace("TO\n", "(\\t) 0 TS\n(Two\\r) 1 TS\n(Three) Tx\nTO\n")); + let [t] = texts.as_slice() else { panic!("{texts:?}") }; + assert_eq!(t.plain_text(), "One\tTwo\nThree"); + let texts = legacy_texts(&format!("(x) 0 TS\n{}", point_type("0 TS", "Hi"))); + let [t] = texts.as_slice() else { panic!("{texts:?}") }; + assert_eq!(t.plain_text(), "Hi"); +} + +/// `Te` (end render, §22.6) reads in a text object with no operands and with four, inside a tab +/// definition too, and keeps nothing; so does `TM` (pop text matrix, §22.6). +#[test] +fn legacy_end_render_and_text_matrix_pop_read() { + for te in ["Te", "0 0 1 36 Te", "36 0 Xb\n0 0 1 36 Te\nXB", "1 0 0 1 10 50 Tm\nTM"] { + let texts = legacy_texts(&point_type(te, "Hi")); + let [t] = texts.as_slice() else { panic!("{te}: {texts:?}") }; + assert_eq!(t.plain_text(), "Hi", "{te}"); + } +} + /// Spacing is kept to the ranges the Justification options take, a Minimum, Desired and Maximum /// out of order go back to their defaults, and hyphenation's letter counts and limit to the ranges /// `text.setFormat` takes. diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index b9db1bece..0cf2be098 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -122,7 +122,8 @@ private editing data. VectorCraft: without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868. Graphs (`%_Gs` … `%_GS`) come in as their drawn art, as plain groups, paths and type, without their data or settings, with a note. without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868, with its paragraphs' letter - and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and hyphenation (`Th`). Its patterns in + and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and hyphenation (`Th`); its special characters + (`TS`) join the story, and end render (`Te`) and pop text matrix (`TM`) are read and ignored. Its patterns in the version 7 form (a list of `@` and `&` layers, `_ &` for the tile's background) open as pattern swatches, with a fill's offset, scale, rotation, reflection and shear applied before its matrix. Its area type with several linked areas opens as threaded frames, one for each area with its own matrix,