From 4547439176de8c46859b2afd891d6e13eb8412f8 Mon Sep 17 00:00:00 2001 From: ham9zah Date: Sun, 18 Oct 2026 00:46:20 +0300 Subject: [PATCH] G9: .ai right-to-left paragraphs keep their alignment A right-to-left paragraph in an Illustrator editing copy (key 33 of its paragraph attributes) stores its alignment relative to its direction: "left" lays the line out ending at the anchor and "right" starting there. The importer read the alignment alone, so RTL point type opened shifted by its own width (Arabic headings past the artboard edge, subtitles starting where their title ends, a label over a QR code). Checked locally against ~1,440 single-line point-type stories in a user's own files (never committed): RTL + left ended at the anchor in 332 of 336, RTL + right started there in 51 of 51; LTR and centred type unchanged. Justified alignments keep their current mapping (too few samples). --- crates/eps/src/import/ate.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/eps/src/import/ate.rs b/crates/eps/src/import/ate.rs index 7269fd631..0a1b674ad 100644 --- a/crates/eps/src/import/ate.rs +++ b/crates/eps/src/import/ate.rs @@ -14,9 +14,11 @@ //! //! Where type sits: a story lays its lines out round a point `F` (the anchor: the start of a line of //! left-aligned type, the middle of a centred one, the end of a right-aligned one), each line with an -//! offset from it. The frame of the story gives a matrix that carries that layout onto the canvas of -//! the app, which is 16383 points a side; the canvas's centre, 8191.5, is the centre of the file's -//! `%AI3_TemplateBox`. See [`Story::place`]. +//! offset from it. A right-to-left paragraph (key `33` of its attributes) swaps the two: its +//! "left"-aligned lines end at the anchor and its "right"-aligned ones start there. The frame of the +//! story gives a matrix that carries that layout onto the canvas of the app, which is 16383 points a +//! side; the canvas's centre, 8191.5, is the centre of the file's `%AI3_TemplateBox`. See +//! [`Story::place`]. use std::collections::BTreeMap; @@ -253,6 +255,9 @@ enum FrameKind { #[derive(Clone, Debug, Default, PartialEq)] struct Para { justify: u8, + /// A right-to-left paragraph: its left and right alignments are each other's (the line of + /// "left"-aligned type ends at its anchor). + rtl: bool, first_indent: f64, left_indent: f64, right_indent: f64, @@ -429,6 +434,7 @@ impl Texts { let num = |k: &str| pick(k).and_then(Val::num).filter(|v| v.abs() < 1e5).unwrap_or(0.0); Para { justify: pick("0").and_then(Val::num).map_or(0, |v| v.clamp(0.0, 6.0) as u8), + rtl: pick("33").and_then(Val::num) == Some(1.0), first_indent: num("1"), left_indent: num("2"), right_indent: num("3"), @@ -747,6 +753,8 @@ impl Story { fn para_style(p: &Para, scale: f64) -> ParaStyle { ParaStyle { justify: match p.justify { + 0 if p.rtl => Justify::Right, + 1 if p.rtl => Justify::Left, 1 => Justify::Right, 2 => Justify::Center, 3 => Justify::JustifyLeft, @@ -795,6 +803,20 @@ fn art_scale(to_doc: Affine) -> f64 { mod tests { use super::*; + #[test] + fn right_to_left_paragraphs_swap_left_and_right_alignment() { + let texts = + Texts { stories: vec![], fonts: vec![], defaults: Val::Dict(BTreeMap::new()), para_defaults: read(b"/33 1").unwrap(), frames: vec![] }; + let justify = |own: &[u8]| para_style(&texts.para(Some(&read(own).unwrap())), 1.0).justify; + // Right to left from the document's defaults, or the paragraph's own. + assert_eq!(justify(b"/0 0"), Justify::Right); + assert_eq!(justify(b"/0 1 /33 1"), Justify::Left); + assert_eq!(justify(b"/0 2"), Justify::Center); + // Left to right keeps them. + assert_eq!(justify(b"/0 0 /33 0"), Justify::Left); + assert_eq!(justify(b"/0 1 /33 0"), Justify::Right); + } + #[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();