diff --git a/crates/eps/src/legacy_ai.rs b/crates/eps/src/legacy_ai.rs index e256e305c2..10af4f8f9a 100644 --- a/crates/eps/src/legacy_ai.rs +++ b/crates/eps/src/legacy_ai.rs @@ -33,7 +33,10 @@ //! - Type (§15): point type, area type and type on a path as text objects, `type To`, `a b c d tx //! ty startPt Tp` (the text matrix; the frame or path in art space between it and `TP`), each //! run's paint style, `Tr` (fill, stroke, both or neither), `/_font size ascent descent Tf`, -//! `Ts Tz Tt TA Tl Ta Ti` and its characters `(…) Tx` (`\r` ends a paragraph), `TO`. Only the +//! `Ts Tz Tt TA Tl Ta Ti` and its characters `(…) Tx` (`\r` ends a paragraph), `TO`. Standard +//! Vertical Roman Alignment is written as `1 Tv` and `0 Tv`, and Tate-chu-yoko in horizontal +//! type as a Kumi block recorded for later use, `1 TY` … `0 TY` (§15.3.4); neither operator is +//! written for type that has neither mark. Only the //! revisable form is written: readers lay the type out again (§15.1). Fonts are named by their //! PostScript names in `%%DocumentFonts` and `%%IncludeFont` (§3.1) and re-encoded to //! Windows-1252 (`TE`, then `%AI3_BeginEncoding: _name name`, `TZ`, §3.3), the 8-bit encoding diff --git a/crates/eps/src/legacy_ai/prolog.rs b/crates/eps/src/legacy_ai/prolog.rs index b0d606bba9..5ee77f89cf 100644 --- a/crates/eps/src/legacy_ai/prolog.rs +++ b/crates/eps/src/legacy_ai/prolog.rs @@ -106,7 +106,7 @@ VCLegacy begin /TE {cleartomark} bd /TZ {cleartomark} bd /To {pop} bd /Tp {7 {pop} repeat} bd /TP {} bd /TO {} bd /Tr {pop} bd /Tf {pop pop pop pop} bd /Ts {pop} bd /Tz {pop pop} bd /Tt {pop} bd /TA {pop} bd -/Tl {pop pop} bd /Ta {pop} bd /Ti {pop pop pop} bd /Tx {pop} bd +/Tl {pop pop} bd /Ta {pop} bd /Ti {pop pop pop} bd /Tx {pop} bd /Tv {pop} bd /TY {pop} bd % The page trailer (3): annotatepage marks nothing. /annotatepage {} bd end diff --git a/crates/eps/src/legacy_ai/tests.rs b/crates/eps/src/legacy_ai/tests.rs index 43b28c934a..d0dd1f64e8 100644 --- a/crates/eps/src/legacy_ai/tests.rs +++ b/crates/eps/src/legacy_ai/tests.rs @@ -1193,6 +1193,59 @@ fn variable_font_settings_are_warned_as_left_out() { assert!(String::from_utf8(out.bytes).unwrap().contains("(H\\351llo \\2005\\r) Tx\n"), "still written as text"); } +/// Standard Vertical Roman Alignment is written as `Tv` and Tate-chu-yoko in horizontal type as a +/// Kumi block recorded for later use, `TY` (§15.3.4), each where it changes: the marks read back +/// on the same characters, a block is closed before its text object ends, and type with neither +/// mark has neither operator. +#[test] +fn vertical_roman_and_tate_chu_yoko_are_written_and_read_back() { + use vectorcraft_doc::{TextObject, TextRun}; + let plain = style(12.0, Paint::solid(Color::BLACK)); + let roman = vectorcraft_doc::CharStyle { standard_vertical_roman: true, ..plain.clone() }; + let tcy = vectorcraft_doc::CharStyle { tate_chu_yoko: true, ..plain.clone() }; + let both = vectorcraft_doc::CharStyle { tate_chu_yoko: true, standard_vertical_roman: true, ..plain.clone() }; + let mut doc = Document::new(300.0, 200.0); + let layer = doc.layers[0].id; + let mut marked = TextObject::point(vectorcraft_geom::Point::new(20.0, 40.0), "A", plain.clone()); + for (text, st) in [("12", &tcy), ("b", &roman), ("c", &both), ("34", &tcy)] { + marked.runs.push(TextRun::new(text, st.clone())); + } + let next = TextObject::point(vectorcraft_geom::Point::new(20.0, 80.0), "D", plain.clone()); + for t in [marked, next] { + let n = Node::new(doc.alloc_id(), NodeKind::Text(Box::new(t))); + doc.insert(Some(layer), usize::MAX, n).unwrap(); + } + let out = write(&doc); + assert!(!out.warnings.iter().any(|w| w == TYPE || w == TYPE_ATTRIBUTES), "{:?}", out.warnings); + let text = String::from_utf8(out.bytes.clone()).unwrap(); + assert_eq!((text.matches("1 TY\n").count(), text.matches("0 TY\n").count()), (2, 2), "{text}"); + assert_eq!((text.matches("1 Tv\n").count(), text.matches("0 Tv\n").count()), (1, 1), "{text}"); + // The last block ends before its text object does. + assert!(text.contains("(34) Tx\n0 TY\nTO\n"), "{text}"); + let read = texts(&crate::import(&out.bytes).unwrap().document); + let flags = |t: &TextObject| -> Vec<(String, bool, bool)> { + let mut out: Vec<(String, bool, bool)> = vec![]; + for r in &t.runs { + let key = (r.style.tate_chu_yoko, r.style.standard_vertical_roman); + match out.last_mut() { + Some(last) if (last.1, last.2) == key => last.0.push_str(&r.text), + _ => out.push((r.text.clone(), key.0, key.1)), + } + } + out + }; + let s = |x: &str| x.to_string(); + assert_eq!( + flags(&read[0].1), + [(s("A"), false, false), (s("12"), true, false), (s("b"), false, true), (s("c"), true, true), (s("34"), true, false)] + ); + assert_eq!(flags(&read[1].1), [(s("D"), false, false)]); + + // Type with neither mark has neither operator. + let text = String::from_utf8(write(&typed()).bytes).unwrap(); + assert!(!text.contains(" Tv\n") && !text.contains(" TY\n"), "{text}"); +} + /// A legacy `.ai` text path holds the path and where the type starts on it, and no Align to Path /// or end bracket: type on a path with either is written as text, with the warning that they are /// left out. An end bracket at the end of an open path, or once around a closed one, is kept. A diff --git a/crates/eps/src/legacy_ai/writer.rs b/crates/eps/src/legacy_ai/writer.rs index 8f8fc4ccf2..9bcd5f4f5d 100644 --- a/crates/eps/src/legacy_ai/writer.rs +++ b/crates/eps/src/legacy_ai/writer.rs @@ -39,6 +39,10 @@ pub(super) struct Writer<'a> { in_hidden: bool, /// The fonts type is set in: PostScript name, and whether it's TrueType. pub(super) fonts: Vec<(String, bool)>, + /// The Standard Vertical Roman Alignment the last `Tv` set (it lasts into later text objects). + roman: bool, + /// Inside a Kumi block `1 TY` began (Tate-chu-yoko in horizontal type). + kumi: bool, } impl<'a> Writer<'a> { @@ -59,6 +63,8 @@ impl<'a> Writer<'a> { hidden_art: vec![], in_hidden: false, fonts: vec![], + roman: false, + kumi: false, } } @@ -815,6 +821,10 @@ impl<'a> Writer<'a> { self.line(" Tx"); } } + if self.kumi { + self.line("0 TY"); + self.kumi = false; + } self.line("TO"); } @@ -834,7 +844,8 @@ impl<'a> Writer<'a> { } /// A run's character attributes and paint style: `Tr`, its fill and stroke, `Tf`, `Ts`, `Tz`, - /// `Tt`, `TA`, and the leading with the space before its paragraph, `Tl`. + /// `Tt`, `TA`, and the leading with the space before its paragraph, `Tl`; then `Tv` and `TY` + /// where Standard Vertical Roman Alignment or Tate-chu-yoko changes. fn char_style(&mut self, st: &CharStyle, p: &ParaStyle) { let fill = !st.fill.is_none(); let stroke = st.has_stroke(); @@ -872,6 +883,14 @@ impl<'a> Writer<'a> { let leading = st.leading.unwrap_or_else(|| p.justification.auto_leading_for(st.size)); ps::push_nums(&mut self.body, &[finite(leading, size * 1.2), finite(p.space_before, 0.0)]); self.line("Tl"); + if st.standard_vertical_roman != self.roman { + self.roman = st.standard_vertical_roman; + self.line(if self.roman { "1 Tv" } else { "0 Tv" }); + } + if st.tate_chu_yoko != self.kumi { + self.kumi = st.tate_chu_yoko; + self.line(if self.kumi { "1 TY" } else { "0 TY" }); + } } /// Type as its glyphs' outlines: a group of a compound path for each run, with the run's fill diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 23a36a7e9a..2f2c88abeb 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -155,7 +155,8 @@ private editing data. VectorCraft: under them, clipped to where they show, along a smooth outline traced at the image's own resolution; that art stays) or omitted (Transparent Images, `transparentImages: "flatten"|"omit"`). Type is written as editable text objects (point type, area type and type on a path, with their fonts' PostScript names, sizes, colours, alignment, indents, leading, tracking and - scaling), which the reading app sets in those fonts; characters outside Windows-1252, vertical type, ruby, inline + scaling, and the Standard Vertical Roman Alignment and Tate-chu-yoko marks as `Tv` and `TY`), which the reading + app sets in those fonts; characters outside Windows-1252, vertical type, ruby, inline graphics and gradient or pattern fills make that type outlines, and so do rotated characters (Character Rotation, which a version 7 text object can't hold: the outlines keep the rotation, and the type is no longer editable text in the file), and Text: Outlines (`outlineText: true`) writes