Text: rtlCol puts the first column on the right; just/dist anchoring at the bottom; wordArtVertRtl
Observed on PowerPoint's export (w14-xml s12): - a two-column body with rtlCol="1" starts its first column on the right; DeckCraft read and saved the attribute but laid columns out left to right; - anchor="just" and anchor="dist" place the text at the bottom of the box, like anchor="b"; DeckCraft drew them at the top; - wordArtVertRtl stacks characters like wordArtVert but runs its lines from right to left; it was read as wordArtVert (and saved back as it). It is now its own TextDir, kept on save. w14-xml s12 9.47 -> 5.39. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bab3854484
commit
f5fef50364
|
|
@ -57,6 +57,8 @@ pub enum TextDir {
|
|||
EaVertical,
|
||||
/// `mongolianVert`: turned like `vert`, lines running from left to right.
|
||||
MongolianVertical,
|
||||
/// `wordArtVertRtl`: stacked, lines running from right to left.
|
||||
StackedRtl,
|
||||
}
|
||||
|
||||
impl TextDir {
|
||||
|
|
@ -68,6 +70,7 @@ impl TextDir {
|
|||
TextDir::Stacked => "wordArtVert",
|
||||
TextDir::EaVertical => "eaVert",
|
||||
TextDir::MongolianVertical => "mongolianVert",
|
||||
TextDir::StackedRtl => "wordArtVertRtl",
|
||||
}
|
||||
}
|
||||
pub fn from_xml(s: &str) -> Option<Self> {
|
||||
|
|
@ -76,7 +79,8 @@ impl TextDir {
|
|||
"vert" => TextDir::Vertical,
|
||||
"mongolianVert" => TextDir::MongolianVertical,
|
||||
"vert270" => TextDir::Vertical270,
|
||||
"wordArtVert" | "wordArtVertRtl" => TextDir::Stacked,
|
||||
"wordArtVert" => TextDir::Stacked,
|
||||
"wordArtVertRtl" => TextDir::StackedRtl,
|
||||
"eaVert" => TextDir::EaVertical,
|
||||
_ => return None,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -622,5 +622,7 @@ mod tests {
|
|||
let b = body_pr(&d.root);
|
||||
assert_eq!(b.vert, Some(TextDir::MongolianVertical));
|
||||
assert_eq!(b.vert.map(|v| v.xml()), Some("mongolianVert"));
|
||||
let d = parse(br#"<a:bodyPr vert="wordArtVertRtl"/>"#).unwrap();
|
||||
assert_eq!(body_pr(&d.root).vert.map(|v| v.xml()), Some("wordArtVertRtl"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1569,14 +1569,14 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
|||
// stands every character upright, one under the other.
|
||||
let upright_text = match vert {
|
||||
TextDir::EaVertical => Upright::Cjk,
|
||||
TextDir::Stacked => Upright::All,
|
||||
TextDir::Stacked | TextDir::StackedRtl => Upright::All,
|
||||
_ => Upright::None,
|
||||
};
|
||||
// Mongolian vertical and stacked text: turned like vert, their lines run from left to right
|
||||
// (as PowerPoint draws both).
|
||||
let lines_ltr = matches!(vert, TextDir::MongolianVertical | TextDir::Stacked);
|
||||
let rotation = match vert {
|
||||
TextDir::Vertical | TextDir::EaVertical | TextDir::Stacked | TextDir::MongolianVertical => 90.0,
|
||||
TextDir::Vertical | TextDir::EaVertical | TextDir::Stacked | TextDir::StackedRtl | TextDir::MongolianVertical => 90.0,
|
||||
TextDir::Vertical270 => 270.0,
|
||||
TextDir::Horizontal => 0.0,
|
||||
} + bp.rot.unwrap_or(0.0);
|
||||
|
|
@ -1828,6 +1828,10 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
|||
}
|
||||
let total = y;
|
||||
out.content_height = total;
|
||||
// Where column `c` starts: from the left, or from the right with `rtlCol` (PowerPoint puts the
|
||||
// first column on the right).
|
||||
let rtl_cols = bp.rtl_col.unwrap_or(false) && ncols > 1;
|
||||
let col_x = |c: usize| (if rtl_cols { ncols.saturating_sub(1 + c) } else { c }) as f64 * (col_w + col_gap);
|
||||
// Columns: distribute lines by height.
|
||||
let col_h = if ncols > 1 { inner.height() } else { f64::INFINITY };
|
||||
let mut col = 0usize;
|
||||
|
|
@ -1843,16 +1847,20 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
|||
li.top -= dy;
|
||||
li.baseline -= dy;
|
||||
li.bottom -= dy;
|
||||
li.shift_x(col as f64 * (col_w + col_gap));
|
||||
}
|
||||
let cx = col_x(col);
|
||||
if cx != 0.0 {
|
||||
li.shift_x(cx);
|
||||
}
|
||||
}
|
||||
let used_h = if ncols > 1 { all_lines.iter().map(|(l, ..)| l.bottom).fold(0.0, f64::max) } else { total };
|
||||
out.overflow = used_h > inner.height() + 0.5 || (ncols > 1 && all_lines.last().is_some_and(|(l, ..)| l.bottom > inner.height() + 0.5));
|
||||
let anchor = bp.anchor.unwrap_or(Anchor::Top);
|
||||
let dy = match anchor {
|
||||
Anchor::Top | Anchor::Justified | Anchor::Distributed => 0.0,
|
||||
Anchor::Top => 0.0,
|
||||
Anchor::Middle => (inner.height() - used_h) / 2.0,
|
||||
Anchor::Bottom => inner.height() - used_h,
|
||||
// PowerPoint draws justified and distributed anchoring at the bottom.
|
||||
Anchor::Bottom | Anchor::Justified | Anchor::Distributed => inner.height() - used_h,
|
||||
};
|
||||
// anchorCtr: centre the block horizontally.
|
||||
let dx = if bp.anchor_ctr.unwrap_or(false) { ((inner.width() - out.content_width) / 2.0).max(0.0) } else { 0.0 };
|
||||
|
|
@ -1880,10 +1888,7 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
|||
face: st.face.clone(),
|
||||
size: st.size,
|
||||
color: st.color,
|
||||
glyphs: g
|
||||
.into_iter()
|
||||
.map(|(gid, x)| (gid, ox + bx + x + if line.column > 0 { line.column as f64 * (col_w + col_gap) } else { 0.0 }, line.baseline))
|
||||
.collect(),
|
||||
glyphs: g.into_iter().map(|(gid, x)| (gid, ox + bx + x + col_x(line.column), line.baseline)).collect(),
|
||||
cells: vec![],
|
||||
fake_bold: false,
|
||||
fake_italic: false,
|
||||
|
|
|
|||
|
|
@ -1116,6 +1116,11 @@ fn mongolian_lines_run_left_to_right_and_stacked_letters_stand_one_em_apart() {
|
|||
sh2.text.as_mut().unwrap().body.vert = Some(deckcraft_model::text::TextDir::Stacked);
|
||||
let st = lay(&p2, &sh2, 200.0, 400.0);
|
||||
assert!(st.lines[0].top > st.lines[1].top);
|
||||
// wordArtVertRtl stacks the same way with lines from right to left.
|
||||
sh2.text.as_mut().unwrap().body.vert = Some(deckcraft_model::text::TextDir::StackedRtl);
|
||||
let sr = lay(&p2, &sh2, 200.0, 400.0);
|
||||
assert!(sr.lines[0].top < sr.lines[1].top);
|
||||
assert!(sr.runs.iter().filter(|r| !r.cells.is_empty()).all(|r| r.upright));
|
||||
let e = &s.lines[0].edges;
|
||||
assert!((e[1].1 - e[1].0 - 20.0).abs() < 0.01 && (e[2].1 - e[2].0 - 20.0).abs() < 0.01, "{e:?}");
|
||||
}
|
||||
|
|
@ -1166,3 +1171,28 @@ fn strikethrough_sits_at_the_font_strikeout_position() {
|
|||
assert!((l.lines[0].baseline - d.rect.y0 - r.face.strikeout.0 * k).abs() < 0.01, "top at the strikeout position");
|
||||
assert!((d.rect.height() - (r.face.strikeout.1 * k).max(0.5)).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtl_columns_start_on_the_right_and_just_anchoring_sits_at_the_bottom() {
|
||||
let text = "word ".repeat(200);
|
||||
let (p, mut sh) = setup(&text, 400.0, 200.0);
|
||||
let t = sh.text.as_mut().unwrap();
|
||||
t.body.columns = Some(2);
|
||||
t.body.col_spacing = Some(20.0);
|
||||
t.paragraphs[0].runs[0].props.size = Some(12.0);
|
||||
let ltr = lay(&p, &sh, 400.0, 200.0);
|
||||
sh.text.as_mut().unwrap().body.rtl_col = Some(true);
|
||||
let rtl = lay(&p, &sh, 400.0, 200.0);
|
||||
let first = |l: &TextLayout| l.lines.iter().find(|x| x.column == 0).map(|x| x.extent.0).unwrap();
|
||||
let second = |l: &TextLayout| l.lines.iter().find(|x| x.column == 1).map(|x| x.extent.0).unwrap();
|
||||
assert!(first(<r) < second(<r));
|
||||
assert!(first(&rtl) > second(&rtl), "with rtlCol the first column is on the right");
|
||||
// anchor="just" and "dist" draw like "b".
|
||||
let (p, mut sh) = setup("one line", 400.0, 300.0);
|
||||
sh.text.as_mut().unwrap().body.anchor = Some(Anchor::Bottom);
|
||||
let b = lay(&p, &sh, 400.0, 300.0).lines[0].top;
|
||||
for a in [Anchor::Justified, Anchor::Distributed] {
|
||||
sh.text.as_mut().unwrap().body.anchor = Some(a);
|
||||
assert!((lay(&p, &sh, 400.0, 300.0).lines[0].top - b).abs() < 0.01, "{a:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue