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).
This commit is contained in:
ham9zah
2026-10-18 18:08:58 +07:00
committed by bflatastic
parent 797e899c56
commit 4547439176
+25 -3
View File
@@ -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();