Merge pull request #6842 from dotCooCoo/fix/legacy-ai-vertical-roman-tcy

Legacy .ai: type keeps Standard Vertical Roman Alignment and Tate-chu-yoko
This commit is contained in:
BFlat
2026-11-12 21:03:45 +07:00
committed by GitHub
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,
}
}
@@ -819,6 +825,10 @@ impl<'a> Writer<'a> {
self.line(" Tx");
}
}
if self.kumi {
self.line("0 TY");
self.kumi = false;
}
self.line("TO");
}
@@ -838,7 +848,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();
@@ -876,6 +887,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