diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs index dec514d5c..780e7ddd0 100644 --- a/crates/eps/src/ai_editing/tests.rs +++ b/crates/eps/src/ai_editing/tests.rs @@ -3393,7 +3393,7 @@ fn type_lines(doc: &Document) -> Vec { for r in &t.runs { let s = &r.style; out.push(format!( - " run {:?} {} {} {} {:?} {} {} {} {} fill {} stroke {} {} caps={} small={:?} tcy={}", + " run {:?} {} {} {} {:?} {} {} {} {} fill {} stroke {} {} caps={} small={:?} tcy={} roman={}", r.text, s.font_family, s.font_style, @@ -3409,6 +3409,7 @@ fn type_lines(doc: &Document) -> Vec { s.all_caps, s.small_caps.map(r3), s.tate_chu_yoko, + s.standard_vertical_roman, )); } } @@ -3556,6 +3557,62 @@ fn type_is_written_again_as_the_same_bytes() { assert_eq!(String::from_utf8_lossy(&again.program), String::from_utf8_lossy(&first.program)); } +/// Standard Vertical Roman Alignment comes back on or off, in vertical type and in horizontal +/// type, and Tate-chu-yoko stays as it was: each run's key 35 is 1 (Standard) when it's on, 2 +/// (rotated) when it's off and 3 for Tate-chu-yoko, and the document's defaults and the normal +/// character style carry 1. Each object lays out as it did. A Tate-chu-yoko run with the +/// setting on comes back without it, and that is reported. +#[test] +fn standard_vertical_roman_alignment_comes_back() { + use vectorcraft_doc::{CharStyle, TextObject, TextRun}; + let plain = style(14.0, Color::BLACK); + let roman = CharStyle { standard_vertical_roman: true, ..plain.clone() }; + let tcy = CharStyle { tate_chu_yoko: true, ..plain.clone() }; + let mut doc = Document::new(200.0, 200.0); + let layer = doc.layers[0].id; + let mut down = TextObject::point(vectorcraft_geom::Point::new(150.0, 20.0), "", plain.clone()); + down.runs = vec![TextRun::new("Up AB ", roman.clone()), TextRun::new("turned CD ", plain.clone()), TextRun::new("12", tcy.clone())]; + down.vertical = true; + let mut across = TextObject::point(vectorcraft_geom::Point::new(20.0, 40.0), "", plain.clone()); + across.runs = vec![TextRun::new("Marked ", roman.clone()), TextRun::new("plain", plain.clone())]; + for t in [down, across] { + let n = text_node(&mut doc, t); + doc.insert(Some(layer), usize::MAX, n).unwrap(); + } + let out = export_ai_editing(&doc, &opts()).unwrap(); + assert!(out.warnings.is_empty(), "{:?}", out.warnings); + let back = read_back(&doc); + same_lines(&type_lines(&back), &type_lines(&doc)); + let texts = |d: &Document| -> Vec { + d.layers[0].children().unwrap().iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some((**t).clone()) } else { None }).collect() + }; + let flags = |t: &TextObject| t.runs.iter().map(|r| (r.style.standard_vertical_roman, r.style.tate_chu_yoko)).collect::>(); + let (written, read) = (texts(&doc), texts(&back)); + assert_eq!(read.iter().map(flags).collect::>(), [vec![(true, false), (false, false), (false, true)], vec![(true, false), (false, false)]]); + let origins = |t: &TextObject| { + vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).glyphs.iter().map(|g| (r3(g.origin.x), r3(g.origin.y))).collect::>() + }; + for (w, r) in written.iter().zip(&read) { + assert_eq!(origins(r), origins(w), "vertical={}", w.vertical); + } + let decoded = text_document(&program(&doc)); + // Two runs on, two off and one Tate-chu-yoko; the defaults and the normal style add a 1 each. + for (key, count) in [("/35 1 ", 4), ("/35 2 ", 2), ("/35 3 ", 1)] { + assert_eq!(decoded.matches(key).count(), count, "{key}: {decoded}"); + } + let normal = decoded.find("[Normal Character Style]").unwrap(); + let sheet = &decoded[normal..decoded.find("[Normal Paragraph Style]").unwrap()]; + assert!(sheet.contains("/35 1 "), "{sheet}"); + // Key 35 holds one value: Tate-chu-yoko wins, and the setting it leaves out is reported. + let mut both = doc.clone(); + let id = both.layers[0].children().unwrap()[0].id; + if let Some(NodeKind::Text(t)) = both.node_mut(id).map(|n| &mut n.kind) { + t.runs[2].style.standard_vertical_roman = true; + } + assert_eq!(export_ai_editing(&both, &opts()).unwrap().warnings, [writer::TYPE_ATTRIBUTES]); + assert_eq!(flags(&texts(&read_back(&both))[0])[2], (false, true)); +} + /// The text document of program `text` (LF line ends), decoded, with its UTF-16 strings' zero /// bytes left out. fn text_document(text: &str) -> String { diff --git a/crates/eps/src/ai_editing/writer/texts.rs b/crates/eps/src/ai_editing/writer/texts.rs index e8ccda106..f494e859a 100644 --- a/crates/eps/src/ai_editing/writer/texts.rs +++ b/crates/eps/src/ai_editing/writer/texts.rs @@ -29,7 +29,7 @@ use vectorcraft_geom::{Affine, PathData, Point}; use crate::ps; /// What the text document holds of type, and what it leaves out. -pub(in crate::ai_editing) const TYPE_ATTRIBUTES: &str = "attributes of type other than its font, size, leading, tracking, baseline shift, horizontal and vertical scale, solid fill, solid stroke and stroke weight, capitals, small capitals at the document's size, Tate-chu-yoko, paragraph alignment, indents, space before and after, hyphenation on or off and right-to-left direction (for example underline, kerning, OpenType features, swatch links of its colors, character and paragraph styles, tabs, lists, optical margin alignment, area type's inset spacing, vertical alignment and flow, a minimum first baseline with the Ascent first baseline, and type on a path's effect, alignment and spacing and an end bracket at the very end of an open path) are left out of the editing data for other apps"; +pub(in crate::ai_editing) const TYPE_ATTRIBUTES: &str = "attributes of type other than its font, size, leading, tracking, baseline shift, horizontal and vertical scale, solid fill, solid stroke and stroke weight, capitals, small capitals at the document's size, Tate-chu-yoko, Standard Vertical Roman Alignment, paragraph alignment, indents, space before and after, hyphenation on or off and right-to-left direction (for example underline, kerning, OpenType features, swatch links of its colors, character and paragraph styles, tabs, lists, optical margin alignment, area type's inset spacing, vertical alignment and flow, a minimum first baseline with the Ascent first baseline, and type on a path's effect, alignment and spacing and an end bracket at the very end of an open path) are left out of the editing data for other apps"; /// The version of the text document's form (`/98 << /0 n >>`), as the newest files users sent have it. const STREAM_VERSION: u32 = 14; @@ -350,6 +350,8 @@ pub(in crate::ai_editing) fn loses(t: &TextObject) -> bool { all_caps: s.all_caps, small_caps: s.small_caps.filter(|v| *v == default_small_caps), tate_chu_yoko: s.tate_chu_yoko, + // Key 35 holds one of the two: a Tate-chu-yoko run is read back without the other. + standard_vertical_roman: s.standard_vertical_roman && !s.tate_chu_yoko, ..CharStyle::default() } }; @@ -403,6 +405,18 @@ pub(in crate::ai_editing) fn loses(t: &TextObject) -> bool { && (t.path_effect != PathEffect::default() || t.path_align != PathAlign::default() || t.path_spacing != 0.0 || end_read_as_none(t))) } +/// Key 35 of a run in style `st`, as the reader's `Texts::style` reads it: 3 for Tate-chu-yoko, 1 +/// (Standard) for Standard Vertical Roman Alignment, else 2 (rotated). +fn baseline_direction(st: &CharStyle) -> i64 { + if st.tate_chu_yoko { + 3 + } else if st.standard_vertical_roman { + 1 + } else { + 2 + } +} + /// The name the font of style `st` is written under, and whether it's TrueType: an installed /// face's PostScript name; else its family and style as `family-style` (the family alone for /// Regular), the form the reader's `family_style` splits. @@ -556,9 +570,7 @@ impl TextDoc { 0 }; d.push((12, V::Int(caps))); - if st.tate_chu_yoko { - d.push((35, V::Int(3))); - } + d.push((35, V::Int(baseline_direction(st)))); let fill = solid(&st.fill).flatten(); // A stroke of no width keeps its paint. let stroke = solid(&st.stroke).flatten(); @@ -943,6 +955,8 @@ impl TextDoc { (8, real(0.0)), (9, real(0.0)), (12, V::Int(0)), + // Every run writes its own key 35, so this only sets where new type starts: Standard (1). + (35, V::Int(1)), (53, paint(&Color::gray(1.0))), (56, V::Bool(true)), (57, V::Bool(false)), diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 55801b8fd..ebf72a690 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -102,7 +102,10 @@ private editing data. VectorCraft: gutters and first baseline, threaded frames included) and type on a path are written as text: a text object for each, naming its story in the text document, with the characters, fonts, sizes, leading, tracking, baseline shift, scales, solid fills and strokes, capitals and paragraph alignment, indents, spacing and direction; other character - and paragraph attributes are left out with a warning. Type in opacity masks, pattern tiles, symbols and art cut to a + and paragraph attributes are left out with a warning. Each run's baseline direction is written in the form the reader + below reads it: 1 when Standard Vertical Roman Alignment is on, 2 when it is off and 3 for Tate-chu-yoko; the + document's defaults and the normal character style carry 1. A run with both Tate-chu-yoko and Standard Vertical Roman + Alignment is written as Tate-chu-yoko, with the type attributes warning. Type in opacity masks, pattern tiles, symbols and art cut to a clipping object, mirrored type, turned or unevenly scaled area type and type on a path, area type in an open frame, type on an open path whose ends meet, area type that flows around a text wrap, type of only white space, threads of type on a path, threads whose frames differ in orientation, type and stories past the sizes VectorCraft's reader reads, and