Legacy .ai: type keeps Standard Vertical Roman Alignment and Tate-chu-yoko

Horizontal type keeps both marks, and the legacy .ai writer writes such
type as text, but the marks were dropped without a warning. Version 7
text has operators for both (section 15.3.4 of the published format
specification): Tv for Roman characters upright in vertical writing,
and TY for a Kumi block in horizontal text recorded for later vertical
use. The writer now writes 1 Tv and 0 Tv, and 1 TY and 0 TY, where a
run's mark changes, closes an open Kumi block before the text object
ends, and writes neither operator for type without the marks. The
legacy .ai reader reads both back onto the same characters. The prolog
defines Tv and TY so the file still runs as PostScript.
This commit is contained in:
dotCooCoo
2026-11-12 01:08:24 -08:00
parent eb608bb8b5
commit cdcac345a9
5 changed files with 80 additions and 4 deletions
+4 -1
View File
@@ -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
+1 -1
View File
@@ -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
+53
View File
@@ -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
+20 -1
View File
@@ -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
+2 -1
View File
@@ -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