diff --git a/crates/eps/src/import/ai/legacy_text.rs b/crates/eps/src/import/ai/legacy_text.rs index 4e6164e4b..1fb369053 100644 --- a/crates/eps/src/import/ai/legacy_text.rs +++ b/crates/eps/src/import/ai/legacy_text.rs @@ -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, /// 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; } diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index fd1e2ece3..e736fe6da 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -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.