mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 13:23:22 +00:00
Merge pull request #5885 from dotCooCoo/fix/ai-tab-stops-shared
.ai import: legacy type keeps one copy of tab stops its paragraphs share
This commit is contained in:
@@ -30,6 +30,13 @@ const MAX_FONTS: usize = 4096;
|
||||
const MAX_TABS: usize = 1024;
|
||||
/// Largest tab stop position read (points), as `text.tabs.set` takes it.
|
||||
const MAX_TAB_POSITION: f64 = 100_000.0;
|
||||
/// Most tab stops copied in one text object into its runs of paragraphs with the same attributes
|
||||
/// while it is read, and again, counted apart, into each of its paragraphs when it has more than one
|
||||
/// run and into the frames of its linked areas after the first. The runs, paragraphs and frames past
|
||||
/// it take the default stops, with a warning.
|
||||
const MAX_STORY_TABS: usize = 1 << 20;
|
||||
/// The warning when [`MAX_STORY_TABS`] leaves tab stops out.
|
||||
const TABS_LEFT_OUT: &str = "tab stops past VectorCraft's limits were left out";
|
||||
/// Most values read before one operator in a re-encoding section.
|
||||
const MAX_ENCODING_VALUES: usize = 1024;
|
||||
/// Most text paths read in one text object (its linked areas); the frames of others are left out,
|
||||
@@ -56,6 +63,17 @@ struct TextPath {
|
||||
path: Option<(BezPath, Vec<AnchorKind>)>,
|
||||
}
|
||||
|
||||
/// Paragraphs in a row that start with the same attributes.
|
||||
#[derive(Debug)]
|
||||
struct ParaRun {
|
||||
/// The attributes, as they were at the first character of each paragraph.
|
||||
style: ParaStyle,
|
||||
/// How many paragraphs.
|
||||
count: usize,
|
||||
/// [`TypeState::tabs_version`] when the last of them started.
|
||||
tabs_version: u64,
|
||||
}
|
||||
|
||||
/// A text object being read.
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct LegacyText {
|
||||
@@ -69,8 +87,13 @@ pub(super) struct LegacyText {
|
||||
past_limit: bool,
|
||||
/// The story: the characters after every text path, in file order.
|
||||
runs: Vec<TextRun>,
|
||||
/// Each paragraph's attributes, as they were at its first character.
|
||||
paras: Vec<ParaStyle>,
|
||||
/// The paragraphs' attributes, as they were at the first character of each, by runs of
|
||||
/// paragraphs with the same attributes.
|
||||
paras: Vec<ParaRun>,
|
||||
/// The tab stops that the runs in `paras` hold.
|
||||
tab_stops: usize,
|
||||
/// [`MAX_STORY_TABS`] left tab stops out.
|
||||
tabs_left_out: bool,
|
||||
/// 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.
|
||||
@@ -83,11 +106,74 @@ pub(super) struct LegacyText {
|
||||
hidden: bool,
|
||||
}
|
||||
|
||||
impl LegacyText {
|
||||
/// A paragraph starts, with the attributes `para` at [`TypeState::tabs_version`]
|
||||
/// `tabs_version`. When they are those of the paragraph before it, it joins that paragraph's
|
||||
/// run; else a new run starts, with the tab stops that [`MAX_STORY_TABS`] leaves room for.
|
||||
fn begin_para(&mut self, para: &mut ParaStyle, tabs_version: u64) {
|
||||
if let Some(run) = self.paras.last_mut() {
|
||||
// No tab definition since the run's last paragraph started: the stops are the same.
|
||||
let same = if run.tabs_version == tabs_version { same_but_tabs(&mut run.style, para) } else { run.style == *para };
|
||||
if same {
|
||||
run.count += 1;
|
||||
run.tabs_version = tabs_version;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let style = within_tabs(para, &mut self.tab_stops, &mut self.tabs_left_out);
|
||||
self.paras.push(ParaRun { style, count: 1, tabs_version });
|
||||
}
|
||||
}
|
||||
|
||||
/// Are `a` and `b` the same, leaving their tab stops out?
|
||||
fn same_but_tabs(a: &mut ParaStyle, b: &mut ParaStyle) -> bool {
|
||||
let (a_tabs, b_tabs) = (std::mem::take(&mut a.tabs), std::mem::take(&mut b.tabs));
|
||||
let same = a == b;
|
||||
(a.tabs, b.tabs) = (a_tabs, b_tabs);
|
||||
same
|
||||
}
|
||||
|
||||
/// A copy of `para`, with its tab stops when `copied` (the tab stops copied so far) has room for
|
||||
/// them within [`MAX_STORY_TABS`], else with the default stops and `left_out` set.
|
||||
fn within_tabs(para: &ParaStyle, copied: &mut usize, left_out: &mut bool) -> ParaStyle {
|
||||
let mut style = para.clone();
|
||||
if MAX_STORY_TABS.saturating_sub(*copied) >= style.tabs.len() {
|
||||
*copied += style.tabs.len();
|
||||
} else {
|
||||
*left_out = true;
|
||||
style.tabs = Vec::new();
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
/// The attributes of each paragraph of the runs `runs`, in order. A paragraph keeps its tab stops
|
||||
/// while `copied` (the tab stops copied so far) has room for them within [`MAX_STORY_TABS`]; the
|
||||
/// paragraphs past it take the default stops, and `left_out` is set.
|
||||
fn each_paragraph(runs: Vec<ParaRun>, copied: &mut usize, left_out: &mut bool) -> Vec<ParaStyle> {
|
||||
let mut each = Vec::with_capacity(runs.iter().map(|r| r.count).sum());
|
||||
for run in runs {
|
||||
let stops = run.style.tabs.len();
|
||||
let room = MAX_STORY_TABS.saturating_sub(*copied);
|
||||
let kept = room.checked_div(stops).map_or(run.count, |n| run.count.min(n));
|
||||
*copied += kept * stops;
|
||||
let bare = (kept < run.count).then(|| ParaStyle { tabs: Vec::new(), ..run.style.clone() });
|
||||
each.extend(std::iter::repeat_n(run.style, kept));
|
||||
if let Some(bare) = bare {
|
||||
*left_out = true;
|
||||
each.extend(std::iter::repeat_n(bare, run.count - kept));
|
||||
}
|
||||
}
|
||||
each
|
||||
}
|
||||
|
||||
/// The modal type attributes of the legacy format, which last from one text object to the next.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct TypeState {
|
||||
style: CharStyle,
|
||||
para: ParaStyle,
|
||||
/// How many tab definitions set `para`'s tab stops: paragraphs that start at the same count
|
||||
/// start with the same stops.
|
||||
tabs_version: u64,
|
||||
/// `Tr`.
|
||||
render: i64,
|
||||
/// The encoding strings are in, by code (`TE`); None where it says nothing.
|
||||
@@ -109,6 +195,7 @@ impl Default for TypeState {
|
||||
Self {
|
||||
style: CharStyle::default(),
|
||||
para: ParaStyle::default(),
|
||||
tabs_version: 0,
|
||||
render: 0,
|
||||
encoding: vec![None; 256],
|
||||
fonts: vec![],
|
||||
@@ -488,6 +575,7 @@ impl Reader<'_> {
|
||||
tabs.sort_by(|a, b| a.position.total_cmp(&b.position));
|
||||
tabs.dedup_by(|a, b| (a.position - b.position).abs() < 1e-6);
|
||||
st.para.tabs = tabs;
|
||||
st.tabs_version = st.tabs_version.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -538,13 +626,13 @@ impl Reader<'_> {
|
||||
}
|
||||
for c in decoded.chars() {
|
||||
if c != '\n' && !t.para_taken {
|
||||
t.paras.push(st.para.clone());
|
||||
t.begin_para(&mut st.para, st.tabs_version);
|
||||
t.para_taken = true;
|
||||
}
|
||||
text.push(c);
|
||||
if c == '\n' {
|
||||
if !t.para_taken {
|
||||
t.paras.push(st.para.clone());
|
||||
t.begin_para(&mut st.para, st.tabs_version);
|
||||
}
|
||||
t.para_taken = false;
|
||||
}
|
||||
@@ -562,7 +650,7 @@ impl Reader<'_> {
|
||||
/// Point type is read at its first text path; area type and type on a path at the first one
|
||||
/// that has a frame or path (else at the first one, as point type).
|
||||
pub(super) fn end_text(&mut self, hidden: bool) -> Result<(), String> {
|
||||
let Some(t) = self.text.take() else { return Ok(()) };
|
||||
let Some(mut t) = self.text.take() else { return Ok(()) };
|
||||
if t.runs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -581,13 +669,15 @@ impl Reader<'_> {
|
||||
if t.kind == 1 && (t.past_limit || all_areas > placed.len()) {
|
||||
self.warn(LINKED_AREAS_LEFT_OUT);
|
||||
}
|
||||
let mut paras = t.paras;
|
||||
let mut paras = std::mem::take(&mut t.paras);
|
||||
if paras.is_empty() {
|
||||
paras.push(self.type_state.para.clone());
|
||||
paras.push(ParaRun { style: self.type_state.para.clone(), count: 1, tabs_version: 0 });
|
||||
}
|
||||
let last_para = paras.last().cloned().unwrap_or_default();
|
||||
let last_para = paras.last().map(|r| r.style.clone()).unwrap_or_default();
|
||||
let last_style = t.runs.last().map(|r| r.style.clone()).unwrap_or_default();
|
||||
let mut story = Some((t.runs, paras));
|
||||
let mut story = Some((std::mem::take(&mut t.runs), paras));
|
||||
// The tab stops copied into the paragraphs and frames.
|
||||
let mut copied = 0;
|
||||
let mut nodes = Vec::with_capacity(placed.len());
|
||||
for (m, kind) in placed {
|
||||
let mut obj = TextObject::point(kurbo::Point::ZERO, "", last_style.clone());
|
||||
@@ -596,12 +686,19 @@ impl Reader<'_> {
|
||||
match story.take() {
|
||||
Some((runs, paras)) => {
|
||||
obj.runs = runs;
|
||||
obj.set_paragraph_styles(paras);
|
||||
// Paragraphs that all have the same attributes share one copy of them.
|
||||
match <[ParaRun; 1]>::try_from(paras) {
|
||||
Ok([one]) => obj.set_all_paras(one.style),
|
||||
Err(paras) => obj.set_paragraph_styles(each_paragraph(paras, &mut copied, &mut t.tabs_left_out)),
|
||||
}
|
||||
}
|
||||
None => obj.set_paragraph_styles(vec![last_para.clone()]),
|
||||
None => obj.set_all_paras(within_tabs(&last_para, &mut copied, &mut t.tabs_left_out)),
|
||||
}
|
||||
nodes.push(self.new_node(NodeKind::Text(Box::new(obj)), &t.gs));
|
||||
}
|
||||
if t.tabs_left_out {
|
||||
self.warn(TABS_LEFT_OUT);
|
||||
}
|
||||
let frames: Vec<NodeId> = nodes.iter().map(|n| n.id).collect();
|
||||
// Type used as a mask clips the objects of its clipping group, as a `W` path does. A
|
||||
// clipping path is one object, and wraparound text and compound paths don't clip their
|
||||
|
||||
@@ -3313,6 +3313,44 @@ fn legacy_tab_definitions_skipped_capped_and_left_open() {
|
||||
assert!(texts[1].para_at(0).tabs.is_empty(), "the next object's `To` ends the open definition");
|
||||
}
|
||||
|
||||
/// Point type whose 1024 tab stops are followed by 2048 paragraph breaks, each after `attrs(i)`.
|
||||
fn paragraphs_after_many_tabs(attrs: impl Fn(usize) -> String) -> String {
|
||||
let many: String = (0..1024).map(|i| format!("0 0 1 {i} Xe\n")).collect();
|
||||
let breaks: String = (0..2048).map(|i| format!("{}(x\\r) Tx\n", attrs(i))).collect();
|
||||
point_type(&format!("36 1024 Xb\n{many}XB"), "A").replace("TO\n", &format!("{breaks}TO\n"))
|
||||
}
|
||||
|
||||
/// Paragraphs that start with the same attributes all keep the same tab stops, however many
|
||||
/// there are, also when an attribute is set again to the value it has.
|
||||
#[test]
|
||||
fn legacy_tab_stops_last_through_many_paragraphs() {
|
||||
for again in ["", "0 Ta\n"] {
|
||||
let r = legacy_read(¶graphs_after_many_tabs(|_| again.to_string()));
|
||||
let texts = art_texts(&r.doc);
|
||||
let [t] = texts.as_slice() else { panic!("{texts:?}") };
|
||||
assert_eq!(t.paragraph_count(), 2049);
|
||||
assert!((0..2049).all(|i| t.para_at(i).tabs.len() == 1024), "{again}");
|
||||
assert!(r.warnings.is_empty(), "{again}: {:?}", r.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
/// Each paragraph whose attributes differ from the paragraph before it holds its own copy of its
|
||||
/// tab stops, and a text object's paragraphs hold no more than 1,048,576 stops: the paragraphs
|
||||
/// past them take the default stops and keep their other attributes, with a warning.
|
||||
#[test]
|
||||
fn legacy_tab_stops_copied_into_paragraphs_are_capped() {
|
||||
let r = legacy_read(¶graphs_after_many_tabs(|i| format!("{} Ta\n", i % 2)));
|
||||
let texts = art_texts(&r.doc);
|
||||
let [t] = texts.as_slice() else { panic!("{texts:?}") };
|
||||
let paras = t.paragraph_styles();
|
||||
assert_eq!(paras.len(), 2049);
|
||||
assert_eq!(paras.iter().map(|p| p.tabs.len()).sum::<usize>(), 1 << 20);
|
||||
assert!(paras[..1024].iter().all(|p| p.tabs.len() == 1024));
|
||||
assert!(paras[1024..].iter().all(|p| p.tabs.is_empty()));
|
||||
assert_eq!((paras[2046].justify, paras[2047].justify), (vectorcraft_doc::Justify::Left, vectorcraft_doc::Justify::Center));
|
||||
assert_eq!(r.warnings, ["tab stops past VectorCraft's limits were left out"]);
|
||||
}
|
||||
|
||||
/// `Te` (end render, §22.6) reads in a text object with no operands and with four, inside a tab
|
||||
/// definition too, and keeps nothing; so does `TM` (pop text matrix, §22.6).
|
||||
#[test]
|
||||
@@ -3625,6 +3663,23 @@ fn legacy_area_type_reads_up_to_its_limit_of_linked_areas() {
|
||||
assert_eq!(r.warnings, [LINKED_AREAS_LEFT_OUT]);
|
||||
}
|
||||
|
||||
/// The frames of linked areas after the first copy the story's last paragraph attributes, and
|
||||
/// those copies hold no more than 1,048,576 tab stops: the frames past them take the default
|
||||
/// stops, with a warning.
|
||||
#[test]
|
||||
fn legacy_linked_area_frames_cap_their_copied_tab_stops() {
|
||||
let many: String = (0..1024).map(|i| format!("0 0 1 {i} Xe\n")).collect();
|
||||
let area = format!("1 0 0 1 0 0 0 Tp\n{}n\nTP\n", rect_path(0.0, 0.0, 50.0, 40.0));
|
||||
let text = format!("1 To\n{}0 Tr\n36 1024 Xb\n{many}XB\n(a) Tx\nTO\n", area.repeat(4096));
|
||||
let r = legacy_read(&text);
|
||||
let frames = art_frames(&r.doc);
|
||||
assert_eq!(frames.len(), 4096);
|
||||
let stops: Vec<usize> = frames.iter().map(|(_, f)| f.para_at(0).tabs.len()).collect();
|
||||
assert_eq!(stops.iter().filter(|&&n| n == 1024).count(), 1025);
|
||||
assert_eq!(stops.iter().filter(|&&n| n == 0).count(), 3071);
|
||||
assert_eq!(r.warnings, ["tab stops past VectorCraft's limits were left out"]);
|
||||
}
|
||||
|
||||
/// The warning when linked areas are left out.
|
||||
const LINKED_AREAS_LEFT_OUT: &str = "linked type areas past VectorCraft's limits were left out";
|
||||
|
||||
|
||||
@@ -109,7 +109,10 @@ private editing data. VectorCraft:
|
||||
open live); legacy `.ai` without the prolog opens since #1027, its legacy type (`To` … `TO`) since #868; guides
|
||||
open as ruler guides and guide paths since #5057 (#779).
|
||||
Legacy `.ai` type keeps its paragraphs' letter and word spacing (`TC`, `TW`), hanging punctuation (`Tq`) and
|
||||
hyphenation (`Th`); its tab definitions (`Xb` … `XB`) set its paragraphs' tab stops; its special characters
|
||||
hyphenation (`Th`); its tab definitions (`Xb` … `XB`) set its paragraphs' tab stops. Paragraphs in a row with
|
||||
the same attributes share one copy of them while the text object is read, and those copies hold at most
|
||||
1,048,576 stops. The paragraphs and linked frames of one text object then get at most 1,048,576 more copied
|
||||
stops. Paragraphs and frames past either limit take the default stops, with a warning. Its special characters
|
||||
(`TS`) join the story, and end render (`Te`) and pop text matrix (`TM`) are read and ignored. Its type in a
|
||||
font re-encoded with encoding pairs of its own (`TZ`) is decoded with those pairs, and with the file's encoding
|
||||
(`TE`) for the codes they leave out unless the `TZ` sets `useDefault` to 0.
|
||||
|
||||
Reference in New Issue
Block a user