From 027884b0a452ac142e08116541186a28291cbdff Mon Sep 17 00:00:00 2001 From: ham9zah Date: Wed, 28 Oct 2026 21:28:27 +0300 Subject: [PATCH 1/3] Import: read one alternative of versioned content, not two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Versioned content can hold three alternatives: newer files write a Dimension tool group, the same group again after `%AI17_Alternate_Content_if_version_gt:…` for a version in between, then plain art after `%AI17_Alternate_Content`. The reader skipped from the plain `Alternate_Content` marker only, so the middle alternative was read as art too and those objects came in twice. The reader now reads the first alternative and skips from the next one, whatever its marker, to the end of the block, nested blocks included. A block that doesn't end is read whole with a note instead of failing the file. The palette reader tracked alternatives with one flag that a block nested in an alternative reset; it now tracks the depth. --- crates/eps/src/import/ai/lex.rs | 5 ++ crates/eps/src/import/ai/palette.rs | 33 +++++++++--- crates/eps/src/import/ai/read.rs | 2 +- crates/eps/src/import/ai/read/directives.rs | 14 ++++- crates/eps/src/import/ai/read/types.rs | 5 +- crates/eps/src/import/ai/tests.rs | 60 +++++++++++++++++++++ 6 files changed, 109 insertions(+), 10 deletions(-) diff --git a/crates/eps/src/import/ai/lex.rs b/crates/eps/src/import/ai/lex.rs index 071176bb4..c7904948b 100644 --- a/crates/eps/src/import/ai/lex.rs +++ b/crates/eps/src/import/ai/lex.rs @@ -92,6 +92,11 @@ impl<'a> Lexer<'a> { self.pos } + /// Look for the next token at `pos` again (the end of the data at most). + pub fn seek(&mut self, pos: usize) { + self.pos = pos.min(self.src.len()); + } + /// The data from `from` to `to` (empty where that isn't in it). pub fn slice(&self, from: usize, to: usize) -> &'a [u8] { self.src.get(from..to).unwrap_or_default() diff --git a/crates/eps/src/import/ai/palette.rs b/crates/eps/src/import/ai/palette.rs index bf659cfe9..43955d5fc 100644 --- a/crates/eps/src/import/ai/palette.rs +++ b/crates/eps/src/import/ai/palette.rs @@ -69,19 +69,25 @@ pub(super) fn palette(data: &[u8]) -> Palette { let mut cell: Option = None; // Inside a gradient's `Bb` … `BB`. let mut in_gradient = false; - // Inside versioned content's alternate, which repeats what came before it. - let mut alternate = false; + // How deep in versioned content, and the depth of the block whose later alternatives (which + // repeat its first) are being skipped, nested blocks in them included. + let (mut depth, mut skipping) = (0usize, None::); while let Some(t) = lex.next_token() { if let Tok::Comment(c) = t.tok { let line = String::from_utf8_lossy(c); - if line.contains("_Alternate_Content") { - alternate = true; - } else if line.contains("_End_Versioned_Content") || line.contains("_Begin_Content_if_version") { - alternate = false; + if line.contains("_Begin_Content_if_version") { + depth = depth.saturating_add(1); + } else if line.contains("_Alternate_Content") { + skipping = skipping.or(Some(depth)); + } else if line.contains("_End_Versioned_Content") { + if skipping == Some(depth) { + skipping = None; + } + depth = depth.saturating_sub(1); } continue; } - if alternate { + if skipping.is_some() { continue; } let w = match t.tok { @@ -211,6 +217,19 @@ mod tests { assert_eq!(p.swatches.len(), 1); } + /// Three alternatives, and versioned content nested in a later one: still one swatch. + #[test] + fn versioned_swatches_with_more_alternatives_are_read_once() { + let p = palette( + b"0 0 Pb\n%AI17_Begin_Content_if_version_gt:24 15\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\n\ + %AI17_Alternate_Content_if_version_gt:24 10\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\n\ + %AI17_Begin_Content_if_version_gt:23 1\n0 0 0 1 k\n(Black)\nPc\n%AI17_Alternate_Content\n%AI17_End_Versioned_Content\n\ + 0 0 1 0 k\n(Yellow)\nPc\n\ + %AI17_Alternate_Content\n0 1 1 0 (Red ink) 0 x\n(Red ink)\nPc\n%AI17_End_Versioned_Content\n0 0 0 1 k\n(After)\nPc\nPB\n", + ); + assert_eq!(p.swatches.iter().map(|e| e.name.as_str()).collect::>(), ["Red ink", "After"]); + } + #[test] fn what_it_doesnt_know_leaves_the_other_swatches() { let p = palette(b"0 0 Pb\n1 2 3 Zz\n(Odd)\nPc\n(no paint) Pc\n0 0 0 1 k\n(Black)\nPc\nPB\n"); diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs index bba345254..50b4211b9 100644 --- a/crates/eps/src/import/ai/read.rs +++ b/crates/eps/src/import/ai/read.rs @@ -79,7 +79,7 @@ pub(crate) use document::{slot_frame, slot_of}; pub use types::IMAGE_TOO_LARGE; use types::{BLEND_BUDGET, FLIP, Frame, GState, Kind, PatternHead, Place, Raster}; #[cfg(test)] -pub use types::{NON_NATIVE_ART, NON_NATIVE_ART_UNREAD}; +pub use types::{NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, VERSIONED_UNENDED}; pub(super) use types::{blend_mode, blend_name}; #[path = "legacy_text.rs"] diff --git a/crates/eps/src/import/ai/read/directives.rs b/crates/eps/src/import/ai/read/directives.rs index 47eef88dd..d2a4b6cc5 100644 --- a/crates/eps/src/import/ai/read/directives.rs +++ b/crates/eps/src/import/ai/read/directives.rs @@ -1,5 +1,5 @@ use super::art::{four, sane, two}; -use super::types::{FLIP, Kind, MAX_HEX, MAX_PATTERNS, MAX_STACK, PatternHead, Place, Raster, SKIPPED}; +use super::types::{FLIP, Kind, MAX_HEX, MAX_PATTERNS, MAX_STACK, PatternHead, Place, Raster, SKIPPED, VERSIONED_UNENDED}; use super::{Arc, Node, PatternDef, Reader, Rect, Tok, V, marker}; impl<'a> Reader<'a> { @@ -69,6 +69,18 @@ impl<'a> Reader<'a> { if m == "BeginEncoding" { self.type_state.encoding_comment(line); } + // Versioned content holds alternatives of the same art, the newest first: the first is + // read, and the next one (`%AI17_Alternate_Content`, or `…_if_version_gt:` for a version + // in between) starts what is skipped, up to the end of the block, nested blocks included. + if m.starts_with("Alternate_Content") { + let at = self.lex.offset(); + if self.skip_to("End_Versioned_Content").is_err() { + // A block that doesn't end: the rest of the file is read rather than lost. + self.lex.seek(at); + self.warn(VERSIONED_UNENDED); + } + return Ok(()); + } if let Some((_, end)) = SKIPPED.iter().find(|(b, _)| *b == m) { return self.skip_to(end); } diff --git a/crates/eps/src/import/ai/read/types.rs b/crates/eps/src/import/ai/read/types.rs index 309061f3c..e4a404697 100644 --- a/crates/eps/src/import/ai/read/types.rs +++ b/crates/eps/src/import/ai/read/types.rs @@ -244,9 +244,12 @@ pub(super) const SKIPPED: &[(&str, &str)] = &[ ("BeginSymbolList", "EndSymbolList"), ("BeginTextDocument", "EndTextDocument"), ("BeginEncoding", "EndEncoding"), - ("Alternate_Content", "End_Versioned_Content"), ]; +/// The note for versioned content whose `%AI17_End_Versioned_Content` is missing: every +/// alternative after the first is read too, so some art may come in twice. +pub const VERSIONED_UNENDED: &str = "versioned content that doesn't end was read whole: some art may come in twice"; + /// The note for a `/ForeignObject` (non-native art, the content of a placed PDF, which the editor /// shows but doesn't edit) whose PDF can't be read. pub const NON_NATIVE_ART_UNREAD: &str = "non-native art (a placed PDF's content) that VectorCraft can't read is left out"; diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index 721365c25..5077a30f2 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -953,6 +953,66 @@ fn sections_it_skips() { assert_eq!(art(&doc).len(), 1); } +/// A filled square at x = `10 × i` (5 pt wide), to tell the alternatives of versioned content apart. +fn alt_square(i: u32) -> String { + let x = f64::from(i) * 10.0; + rect_path(x, 0.0, x + 5.0, 5.0) + "f\n" +} + +/// The left edges of the layer's art, in order: which [`alt_square`]s came in. +fn squares(doc: &Document) -> Vec { + art(doc).iter().filter_map(|n| n.geometric_bounds()).map(|b| b.x0.round()).collect() +} + +/// Versioned content holds alternatives of the same art for newer and older versions (newer +/// files write a Dimension tool group, then the same group again for a version in between, then +/// plain art): only the first alternative is read, so the art comes in once, however many +/// alternatives follow and whatever versioned content is nested in them. +#[test] +fn versioned_content_reads_one_alternative() { + let (begin, between, alternate, end) = ( + "%AI17_Begin_Content_if_version_gt:24 11\n", + "%AI17_Alternate_Content_if_version_gt:24 10\n", + "%AI17_Alternate_Content\n", + "%AI17_End_Versioned_Content\n", + ); + let (a, b, c) = (alt_square(1), alt_square(2), alt_square(3)); + let cases = [ + ("one alternative", format!("{begin}{a}{end}"), vec![10.0]), + ("two alternatives", format!("{begin}{a}{alternate}{b}{end}"), vec![10.0]), + ("three alternatives", format!("{begin}{a}{between}{b}{alternate}{c}{end}"), vec![10.0]), + // Blocks nested in the first alternative read their own first; those in a later one aren't read. + ( + "nested", + format!( + "{begin}{a}{begin}{b}{alternate}{c}{end}{between}{}{begin}{}{alternate}{}{end}{alternate}{}{end}", + alt_square(4), + alt_square(5), + alt_square(6), + alt_square(7) + ), + vec![10.0, 20.0], + ), + // Art after the block is read as usual. + ("art after it", format!("{begin}{a}{between}{b}{alternate}{c}{end}{}", alt_square(8)), vec![10.0, 80.0]), + ]; + for (case, body, want) in cases { + let s = read_ok(&layer(&body)); + assert_eq!(squares(&s.doc), want, "{case}"); + assert!(!s.warnings.iter().any(|w| w == read::VERSIONED_UNENDED), "{case}: {:?}", s.warnings); + } +} + +/// Versioned content whose end is missing is read whole, with a note, rather than losing the rest +/// of the file. +#[test] +fn versioned_content_that_does_not_end_is_read_with_a_note() { + let body = format!("%AI17_Begin_Content_if_version_gt:24 11\n{}%AI17_Alternate_Content_if_version_gt:24 10\n{}", alt_square(1), alt_square(2)); + let s = read_ok(&layer(&body)); + assert_eq!(squares(&s.doc), [10.0, 20.0]); + assert!(s.warnings.iter().any(|w| w == read::VERSIONED_UNENDED), "{:?}", s.warnings); +} + /// Non-native art (a placed PDF's content) keeps its PDF as ASCII85 in comment lines after /// `/Data ,`. ASCII85 has `_`, so a line may start with `%_`, which otherwise reads as hidden /// tokens, and a `(` in it would swallow the rest of the layer (a letterhead saved by Illustrator From 2dcfbb0bf02626b76f0714cca29cec1772fee16f Mon Sep 17 00:00:00 2001 From: ham9zah Date: Thu, 29 Oct 2026 10:37:09 +0300 Subject: [PATCH 2/3] Import: a right-to-left paragraph of the editing data lays out right to left The text document's paragraph key 33 (from the paragraph or the document's defaults) said a paragraph runs right to left, but the reader only used it to swap left and right alignment: the paragraph's direction stayed unset, so it went by its first strong letter, and a label with no Arabic or Hebrew letters (" 120.5 mm ") laid out left to right instead of reading "mm 120.5". The paragraph style now gets that direction. --- crates/eps/src/import/ate.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/eps/src/import/ate.rs b/crates/eps/src/import/ate.rs index 24a3a2736..ff5b2906e 100644 --- a/crates/eps/src/import/ate.rs +++ b/crates/eps/src/import/ate.rs @@ -24,7 +24,7 @@ use std::collections::BTreeMap; use kurbo::{BezPath, CubicBez, ParamCurve, ParamCurveArclen}; use vectorcraft_color::{Color, Paint}; -use vectorcraft_doc::{CharStyle, Justify, Node, NodeId, NodeKind, ParaStyle, TextKind, TextObject, TextRun}; +use vectorcraft_doc::{CharStyle, Justify, Node, NodeId, NodeKind, ParaDirection, ParaStyle, TextKind, TextObject, TextRun}; use vectorcraft_geom::{Affine, PathData, Point, Rect}; /// Longest story read (bytes). @@ -790,6 +790,9 @@ fn para_style(p: &Para, scale: f64) -> ParaStyle { space_before: p.space_before * scale, space_after: p.space_after * scale, hyphenate: p.hyphenate, + // A right-to-left paragraph lays its text out from the right whatever letters it has + // (" 120.5 mm " reads "mm 120.5"); without the key, from its first strong letter. + direction: p.rtl.then_some(ParaDirection::RightToLeft), ..ParaStyle::default() } } @@ -838,6 +841,20 @@ mod tests { assert_eq!(justify(b"/0 1 /33 0"), Justify::Right); } + /// A right-to-left paragraph is laid out right to left even when it has no right-to-left + /// letters (a dimension's label " 120.5 mm " reads "mm 120.5"); a left-to-right one, or one + /// that doesn't say, goes by its letters. + #[test] + fn right_to_left_paragraphs_set_their_direction() { + let texts = + Texts { stories: vec![], fonts: vec![], defaults: Val::Dict(BTreeMap::new()), para_defaults: read(b"/33 1").unwrap(), frames: vec![] }; + let direction = |own: &[u8]| para_style(&texts.para(Some(&read(own).unwrap())), 1.0).direction; + assert_eq!(direction(b"/0 2"), Some(ParaDirection::RightToLeft), "from the document's defaults"); + assert_eq!(direction(b"/0 2 /33 0"), None, "the paragraph's own"); + let plain = Texts { para_defaults: Val::Dict(BTreeMap::new()), ..texts }; + assert_eq!(para_style(&plain.para(None), 1.0).direction, None); + } + #[test] fn values_read_nested_dictionaries_lists_and_utf16_strings() { let v = read(b" /1 << /0 [ 1 2.5 -3 ] /2 (\xfe\xff\x00h\x00i) /3 true /99 /Name >> /0 (a\\)b\\n)").unwrap(); From 0cd82d8d7dbed9aaeef229ddcb4ac1194827ed9e Mon Sep 17 00:00:00 2001 From: ham9zah Date: Thu, 29 Oct 2026 10:37:09 +0300 Subject: [PATCH 3/3] Import: the page's type of a right-to-left or turned label is its text object's A file's PDF part draws a right-to-left paragraph's words in the order they show ("mm 120.5"), while its text object keeps them in the order they are typed (" 120.5 mm "). The page's type was matched to the text objects by their letters in order, so it matched none and stayed beside the label as a second copy. It is now matched word by word. A point type label turned a quarter (a vertical dimension's) lay outside the box its text object was looked for in, which was always level; that box now runs along the type's baseline. --- crates/eps/src/import/ate.rs | 33 +++++++++++++++++++++++++-- crates/eps/src/import/native/plan.rs | 30 ++++++++++++------------ crates/eps/src/import/native/slots.rs | 7 ++++++ crates/eps/src/import/tests_native.rs | 19 +++++++++++++++ 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/crates/eps/src/import/ate.rs b/crates/eps/src/import/ate.rs index ff5b2906e..7c90b15aa 100644 --- a/crates/eps/src/import/ate.rs +++ b/crates/eps/src/import/ate.rs @@ -618,7 +618,7 @@ impl Story { let r = match self.frames.first()? { FrameKind::Area(s) => bounds(s)?, FrameKind::OnPath { segments, .. } => bounds(segments)?.inflate(size * 1.5, size * 1.5), - FrameKind::Point(_) => { + FrameKind::Point(m) => { let longest = self.text.split(['\r', '\x03']).map(|l| l.chars().count()).max().unwrap_or(0) as f64; let reach = longest * size + size; let points: Vec = self.line_starts(template).into_iter().map(|(_, (x, y))| to_doc * Point::new(x, y)).collect(); @@ -626,7 +626,19 @@ impl Story { let (x, y) = self.place(template); to_doc * Point::new(x, y) }); - points.iter().fold(Rect::from_points(first, first), |r, p| r.union_pt(*p)).inflate(reach, size * 1.5) + // Along the baseline as far as a line reaches, across it a line and a half: turned + // with the type (a dimension's label turned a quarter runs down the page). + // (The canvas is y down, the art y up: see [`to_art`].) + let along = to_doc * Point::new(m[0], -m[1]) - to_doc * Point::ZERO; + let along = if along.hypot() > 1e-9 && along.is_finite() { along / along.hypot() } else { kurbo::Vec2::new(1.0, 0.0) }; + let across = kurbo::Vec2::new(-along.y, along.x); + let mut r = Rect::from_points(first, first); + for p in points.iter().chain([&first]) { + for (t, n) in [(-reach, -1.5), (-reach, 1.5), (reach, -1.5), (reach, 1.5)] { + r = r.union_pt(*p + along * t + across * (n * size)); + } + } + r } }; r.is_finite().then_some(r) @@ -841,6 +853,23 @@ mod tests { assert_eq!(justify(b"/0 1 /33 0"), Justify::Right); } + /// Where point type lies runs along its baseline: a label turned a quarter (as a vertical + /// dimension's is) lies in a tall box, not a wide one. + #[test] + fn point_type_lies_along_its_baseline() { + let para = + Texts { stories: vec![], fonts: vec![], defaults: Val::Dict(BTreeMap::new()), para_defaults: Val::Dict(BTreeMap::new()), frames: vec![] } + .para(None); + let mut s = story(" 80 mm ", &[0.0], ¶); + let size = |s: &Story| s.region((0.0, 0.0), Affine::IDENTITY).map(|r| (r.width(), r.height())).unwrap(); + let (w, h) = size(&s); + assert!(w > 3.0 * h, "level: {w} × {h}"); + // Turned −90° on the canvas (y down): the baseline runs down the page. + s.frames = vec![FrameKind::Point([0.0, 1.0, -1.0, 0.0, 0.0, 0.0])]; + let (w, h) = size(&s); + assert!(h > 3.0 * w, "turned: {w} × {h}"); + } + /// A right-to-left paragraph is laid out right to left even when it has no right-to-left /// letters (a dimension's label " 120.5 mm " reads "mm 120.5"); a left-to-right one, or one /// that doesn't say, goes by its letters. diff --git a/crates/eps/src/import/native/plan.rs b/crates/eps/src/import/native/plan.rs index a89ad0dd9..cc2772f8e 100644 --- a/crates/eps/src/import/native/plan.rs +++ b/crates/eps/src/import/native/plan.rs @@ -1,5 +1,5 @@ use super::layers::closed_where_ends_meet; -use super::slots::{Content, SlotInfo, assign, covered, extent, join_pieces, letters, same_type}; +use super::slots::{Content, SlotInfo, assign, covered, extent, join_pieces, letters, same_type, words}; use super::{Affine, Arc, Node, NodeId, NodeKind, Point, Rect, Story, TextKind}; /// Match page objects to the nearest slot whose story describes them, including hidden slots. @@ -103,28 +103,28 @@ pub(super) fn plan( // with its words? (A copied object's lines are where its original's are, so they don't say.) let hidden_type = |object: &[Arc]| { let Some(r) = extent(object) else { return false }; - let words: Vec = - object.iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(letters(&t.plain_text())) } else { None }).collect(); + let texts: Vec> = + object.iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(words(&t.plain_text())) } else { None }).collect(); hidden_at.iter().any(|(k, place)| { place.intersect(r).area() >= 0.5 * r.area().max(1e-9) - && lines - .get(*k) - .and_then(Option::as_ref) - .is_some_and(|l| !words.is_empty() && words.iter().all(|w| l.iter().any(|(text, _)| letters(text).contains(w.as_str())))) + && lines.get(*k).and_then(Option::as_ref).is_some_and(|l| { + !texts.is_empty() + && texts.iter().all(|ws| !ws.is_empty() && ws.iter().all(|w| l.iter().any(|(text, _)| letters(text).contains(w.as_str())))) + }) }) }; // The letters of each slot's story, in lower case. let spelled: Vec = stories.iter().map(|s| s.as_ref().map(|s| letters(s.text()).to_ascii_lowercase()).unwrap_or_default()).collect(); - // For each slot: is its made type where `object` is, with the letters of one of `object`'s texts in - // its story? All are false unless such slots have the letters of every text between them. + // For each slot: is its made type where `object` is, with the words of one of `object`'s texts in + // its story? All are false unless such slots have the words of every text between them. let spelling = |object: &[Arc], content: &[Content]| -> Vec { let r = extent(object); - let words: Vec = object + let texts: Vec> = object .iter() - .filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(letters(&t.plain_text()).to_ascii_lowercase()) } else { None }) - .filter(|w| !w.is_empty()) + .filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(words(&t.plain_text().to_ascii_lowercase())) } else { None }) + .filter(|ws| !ws.is_empty()) .collect(); - let has = |k: usize| spelled.get(k).is_some_and(|s| words.is_empty() || words.iter().any(|w| s.contains(w.as_str()))); + let has = |k: usize| spelled.get(k).is_some_and(|s| texts.is_empty() || texts.iter().any(|ws| ws.iter().all(|w| s.contains(w.as_str())))); let near: Vec = content .iter() .enumerate() @@ -133,7 +133,7 @@ pub(super) fn plan( _ => false, }) .collect(); - let every = words.iter().all(|w| near.iter().zip(&spelled).any(|(n, s)| *n && s.contains(w.as_str()))); + let every = texts.iter().flatten().all(|w| near.iter().zip(&spelled).any(|(n, s)| *n && s.contains(w.as_str()))); if every { near } else { vec![] } }; let mut spare: Vec>> = vec![]; @@ -150,7 +150,7 @@ pub(super) fn plan( Some(c) => *c = Content::Page(object.clone()), None => {} }, - // What lies where made type is and has that type's letters is that type as the page draws it. + // What lies where made type is and has that type's words is that type as the page draws it. None if covered(object, &content, spells) => { let r = extent(object).unwrap_or(Rect::ZERO); for (k, c) in content.iter_mut().enumerate() { diff --git a/crates/eps/src/import/native/slots.rs b/crates/eps/src/import/native/slots.rs index 524fbcc81..1548a2e9a 100644 --- a/crates/eps/src/import/native/slots.rs +++ b/crates/eps/src/import/native/slots.rs @@ -323,6 +323,13 @@ pub(super) fn letters(s: &str) -> String { s.chars().filter(char::is_ascii_alphanumeric).collect() } +/// The [`letters`] of each word of `s`, the empty ones left out. Type is compared word by word: +/// the page draws a right-to-left paragraph's words in the order they show (" 120.5 mm " as +/// "mm 120.5"), its text object keeps them in the order they are typed. +pub(super) fn words(s: &str) -> Vec { + s.split_whitespace().map(letters).filter(|w| !w.is_empty()).collect() +} + /// Does `n` hold type that is where `story` lays one of its lines out: the line, or a piece of it /// (the page splits a line where its kerning or its styles change), on that line's baseline and /// after its start? How far from where the line starts it is, if so. diff --git a/crates/eps/src/import/tests_native.rs b/crates/eps/src/import/tests_native.rs index 7d804142c..c0fa58c22 100644 --- a/crates/eps/src/import/tests_native.rs +++ b/crates/eps/src/import/tests_native.rs @@ -779,6 +779,25 @@ fn type_of_the_page_with_the_letters_of_made_type_where_it_lies_is_that_type() { } } +/// A right-to-left paragraph's page type has its words in the order they show, not the order they +/// are typed (a dimension's label " 120.5 mm " is drawn "mm 120.5"): it is still that text +/// object's type as the page draws it, not a second copy of it with the nearest text object. +#[test] +fn type_of_the_page_with_the_words_of_made_type_in_another_order_is_that_type() { + let doc = text_document(&[("Hi there\r", 0, (8200.0, 8180.0), (0.0, 0.0), 0.0)]); + let art = native_with_text(&layer("Art", true, &text_object(0)), &doc); + let page = "0 0 0 1 setcmykcolor /Helvetica findfont 14 scalefont setfont 58.5 58.5 moveto (there Hi) show"; + let r = import(&eps(page, &art)).unwrap(); + let mut all = vec![]; + r.document.walk(|n| { + if let NodeKind::Text(t) = &n.kind { + all.push(t.plain_text()); + } + }); + assert_eq!(all, ["Hi there"], "{:?}", r.warnings); + assert!(!r.warnings.iter().any(|w| w.contains("with another's")), "{:?}", r.warnings); +} + /// Page type of no text object that lies where the made type of two text objects is, with the /// letters of one of them in each of its pieces, is the type of both as the page draws it. #[test]