.ai import: legacy overflow breaks keep the direction and paragraph attributes they were read with

The reader holds an overflow paragraph break (`(\r) TX`) until more type
follows it. When that type came, the break took the state in force then,
not the state where the break was read:

- A story that started with the break took the `TV` direction of the
  characters after it, so `1 TV (\r) TX 0 TV (Body) Tx` opened horizontal
  and gave no warning.
- A break read in the other direction inside a story did not count as a
  direction change.
- The empty paragraph that the break ended took the attributes of the
  paragraph after it, while `(\r) Tx` keeps its own.

The pending break now records `TV` and, when it ends an empty paragraph,
that paragraph's attributes.
This commit is contained in:
dotCooCoo
2026-11-08 11:22:39 +07:00
committed by bflatastic
parent 09aa816394
commit 37245779aa
2 changed files with 50 additions and 5 deletions
+25 -5
View File
@@ -78,6 +78,17 @@ struct ParaRun {
tabs_version: u64,
}
/// An overflow paragraph break (`(\r) TX`) read before any more type, with the state in force
/// where it was read.
#[derive(Debug)]
struct PendingBreak {
/// `TV` at the break.
vertical: bool,
/// The attributes and [`TypeState::tabs_version`] of the empty paragraph the break ends, when
/// it ends one that no character started.
para: Option<(ParaStyle, u64)>,
}
/// A text object being read.
#[derive(Debug, Default)]
pub(super) struct LegacyText {
@@ -101,7 +112,7 @@ pub(super) struct LegacyText {
/// The current paragraph's attributes are in `paras`.
para_taken: bool,
/// An overflow paragraph break (`(\r) TX`) not yet followed by type: the story's end.
pending_break: bool,
pending_break: Option<PendingBreak>,
/// Characters were set in a mask rendering mode (`Tr` 4 to 7): the text object is a clipping
/// path (§5.7).
clips: bool,
@@ -632,7 +643,10 @@ impl Reader<'_> {
}
// Overflow text ending the story with a paragraph break (`(\r) TX`) adds no paragraph.
if w == "TX" && decoded == "\n" {
t.pending_break = true;
if t.pending_break.is_none() {
let para = (!t.para_taken).then(|| (st.para.clone(), st.tabs_version));
t.pending_break = Some(PendingBreak { vertical: st.vertical, para });
}
return;
}
// As many characters as the file's allowance has left.
@@ -642,10 +656,13 @@ impl Reader<'_> {
}
let decoded: String = decoded.chars().take(room).collect();
st.chars += decoded.chars().count();
// A break read before these characters keeps the direction and paragraph attributes it was
// read with, and is the story's first character when nothing came before it.
let brk = t.pending_break.take();
if t.runs.is_empty() {
t.vertical = st.vertical;
t.vertical = brk.as_ref().map_or(st.vertical, |b| b.vertical);
}
t.direction_changed |= st.vertical != t.vertical;
t.direction_changed |= st.vertical != t.vertical || brk.as_ref().is_some_and(|b| b.vertical != t.vertical);
let gs = &self.gs;
let mut style = st.style.clone();
style.tate_chu_yoko |= st.kumi_recorded && !t.vertical;
@@ -664,7 +681,10 @@ impl Reader<'_> {
(style.stroke_cap, style.stroke_join, style.stroke_miter_limit, style.stroke_dash) = (gs.cap, gs.join, gs.miter, gs.dash.clone());
(style.overprint_fill, style.overprint_stroke) = (gs.overprint_fill, gs.overprint_stroke);
let mut text = String::new();
if std::mem::take(&mut t.pending_break) {
if let Some(brk) = brk {
if let Some((mut para, tabs_version)) = brk.para {
t.begin_para(&mut para, tabs_version);
}
text.push('\n');
t.para_taken = false;
}
+25
View File
@@ -3554,6 +3554,31 @@ fn legacy_type_keeps_its_writing_direction() {
let after = point_type("", "Hi").replace("TO\n", "1 TV\nTO\n");
let after = legacy_texts(&format!("{after}{}", point_type("", "Next")));
assert_eq!((after[0].vertical, after[1].vertical), (false, true));
// An overflow break (`(\r) TX`) keeps the direction in force where it was read: at the start of
// the story it sets the object's direction, and later it warns when that differs.
let leading = legacy_read(&point_type("1 TV\n(\\r) TX\n0 TV", "Body"));
let leading = (art_texts(&leading.doc), leading.warnings);
assert_eq!((leading.0[0].vertical, leading.0[0].plain_text().as_str()), (true, "\nBody"));
assert_eq!(leading.1, [DIRECTION_CHANGE_LEFT_OUT]);
let inside = legacy_read(&point_type("", "a").replace("TO\n", "1 TV\n(\\r) TX\n0 TV\n(b) Tx\nTO\n"));
let inside = (art_texts(&inside.doc), inside.warnings);
assert_eq!((inside.0[0].vertical, inside.0[0].plain_text().as_str()), (false, "a\nb"));
assert_eq!(inside.1, [DIRECTION_CHANGE_LEFT_OUT]);
}
/// An overflow break (`(\r) TX`) that type follows ends a paragraph as `(\r) Tx` does: an empty
/// paragraph it ends keeps the attributes in force where the break was read.
#[test]
fn an_overflow_break_keeps_the_paragraph_attributes_it_was_read_with() {
use vectorcraft_doc::Justify::{Center, Right};
for op in ["Tx", "TX"] {
let leading = &legacy_texts(&point_type(&format!("1 Ta\n(\\r) {op}\n2 Ta"), "Body"))[0];
assert_eq!((leading.para_at(0).justify, leading.para_at(1).justify), (Center, Right), "{op}");
let empty = point_type("1 Ta", "a\\r").replace("TO\n", &format!("(\\r) {op}\n2 Ta\n(b) Tx\nTO\n"));
let empty = &legacy_texts(&empty)[0];
assert_eq!(empty.plain_text(), "a\n\nb", "{op}");
assert_eq!((empty.para_at(1).justify, empty.para_at(2).justify), (Center, Right), "{op}");
}
}
/// The glyph origins of a legacy-format text object.