Text: mongolianVert and wordArtVert (stacked) text as PowerPoint draws them
mongolianVert was read as vert (and saved back as vert). PowerPoint turns it like vert but runs its lines from left to right, the anchor mirrored with them (bottom-anchored text sits on the right). It is now its own TextDir, kept on save; its lines' places are mirrored across the box. wordArtVert (Stacked) was turned like vert. PowerPoint stands every character upright, one under the other (Latin letters about one em apart, spaces their own width), with lines running left to right. Stacked text now stands each character upright in an em of its own and mirrors the line order. Also: a bullet in the second or later column of a multi-column body is drawn on its line's baseline (it kept the first column's). w01-vertical s4 (mongolianVert) 11.54 -> 9.38, s5 (wordArtVert) 13.84 -> 13.56; w14-xml s13 6.78 -> 6.60. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1b1b21210a
commit
de85c8bdbe
|
|
@ -55,6 +55,8 @@ pub enum TextDir {
|
|||
Vertical270,
|
||||
Stacked,
|
||||
EaVertical,
|
||||
/// `mongolianVert`: turned like `vert`, lines running from left to right.
|
||||
MongolianVertical,
|
||||
}
|
||||
|
||||
impl TextDir {
|
||||
|
|
@ -65,12 +67,14 @@ impl TextDir {
|
|||
TextDir::Vertical270 => "vert270",
|
||||
TextDir::Stacked => "wordArtVert",
|
||||
TextDir::EaVertical => "eaVert",
|
||||
TextDir::MongolianVertical => "mongolianVert",
|
||||
}
|
||||
}
|
||||
pub fn from_xml(s: &str) -> Option<Self> {
|
||||
Some(match s {
|
||||
"horz" => TextDir::Horizontal,
|
||||
"vert" | "mongolianVert" => TextDir::Vertical,
|
||||
"vert" => TextDir::Vertical,
|
||||
"mongolianVert" => TextDir::MongolianVertical,
|
||||
"vert270" => TextDir::Vertical270,
|
||||
"wordArtVert" | "wordArtVertRtl" => TextDir::Stacked,
|
||||
"eaVert" => TextDir::EaVertical,
|
||||
|
|
|
|||
|
|
@ -616,5 +616,11 @@ mod tests {
|
|||
assert_eq!(b.vert, Some(TextDir::Vertical270));
|
||||
assert_eq!(b.columns, Some(2));
|
||||
assert_eq!(b.autofit, Some(AutoFit::Shrink { font_scale: 0.625, line_reduction: 0.2 }));
|
||||
// mongolianVert is its own direction (PowerPoint runs its lines left to right) and is
|
||||
// written back as it was read.
|
||||
let d = parse(br#"<a:bodyPr vert="mongolianVert"/>"#).unwrap();
|
||||
let b = body_pr(&d.root);
|
||||
assert_eq!(b.vert, Some(TextDir::MongolianVertical));
|
||||
assert_eq!(b.vert.map(|v| v.xml()), Some("mongolianVert"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -703,7 +703,7 @@ fn shape_para(
|
|||
opts: &Opts,
|
||||
number: Option<u32>,
|
||||
no_kern: bool,
|
||||
vertical: bool,
|
||||
vertical: Upright,
|
||||
) -> ParaShaped {
|
||||
let db = FontDb::global();
|
||||
let Some(para) = body.paragraphs.get(pi) else {
|
||||
|
|
@ -831,7 +831,7 @@ fn shape_cells(
|
|||
styles: &[Style],
|
||||
faces: &mut Vec<Arc<FontFace>>,
|
||||
rtl: bool,
|
||||
vertical: bool,
|
||||
vertical: Upright,
|
||||
) -> Vec<Cell> {
|
||||
let db = FontDb::global();
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
|
|
@ -911,9 +911,9 @@ fn shape_cells(
|
|||
.collect();
|
||||
// In East Asian vertical text, ideographs, kana, Hangul, full-width forms and the like stand
|
||||
// upright (a whole grapheme at a time); other text lies on its side.
|
||||
if vertical {
|
||||
if vertical != Upright::None {
|
||||
for &(a, b) in &graphemes {
|
||||
let up = chars.get(a).is_some_and(|c| is_upright(*c));
|
||||
let up = chars.get(a).is_some_and(|c| if vertical == Upright::All { !c.is_whitespace() } else { is_upright(*c) });
|
||||
for c in cells.get_mut(a..b).into_iter().flatten() {
|
||||
c.upright = up;
|
||||
}
|
||||
|
|
@ -940,6 +940,30 @@ fn shape_cells(
|
|||
for (a, b) in items {
|
||||
shape_item(&mut cells, styles, faces, text, &byte_of, &char_at_byte, a..b, scripts.get(a).and_then(|s| s.0), lang(a), upper(a));
|
||||
}
|
||||
// Stacked text: each character takes one em down the line, centred across it (PowerPoint
|
||||
// stacks 18 pt Latin letters about 18 pt apart, a space only its own width).
|
||||
if vertical == Upright::All {
|
||||
for i in 0..n {
|
||||
let Some(c) = cells.get(i) else { continue };
|
||||
if !c.upright || c.head != i || c.glyphs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let size = styles.get(c.style).map(|st| st.size).unwrap_or(18.0);
|
||||
let width: f64 = (i..n).take_while(|&j| cells.get(j).is_some_and(|d| d.head == i)).filter_map(|j| cells.get(j)).map(|d| d.adv).sum();
|
||||
let pad = (size - width) / 2.0;
|
||||
if let Some(c) = cells.get_mut(i) {
|
||||
for g in &mut c.glyphs {
|
||||
g.1 += pad;
|
||||
}
|
||||
}
|
||||
for j in i..n {
|
||||
match cells.get_mut(j) {
|
||||
Some(d) if d.head == i => d.adv = if j == i { size } else { 0.0 },
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cells
|
||||
}
|
||||
|
||||
|
|
@ -983,6 +1007,17 @@ fn is_upright(c: char) -> bool {
|
|||
)
|
||||
}
|
||||
|
||||
/// Which characters stand upright in vertical text.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Upright {
|
||||
/// None (horizontal text, vert, vert270, mongolianVert).
|
||||
None,
|
||||
/// Ideographs, kana and the like (eaVert).
|
||||
Cjk,
|
||||
/// Every character, one under the other (wordArtVert).
|
||||
All,
|
||||
}
|
||||
|
||||
/// Size of superscript and subscript text relative to the run's size.
|
||||
const SUB_SUPER: f64 = 0.66;
|
||||
|
||||
|
|
@ -1553,10 +1588,18 @@ fn spacing_stops_kerning(ctx: &Ctx, shape: &Shape, body: &TextBody) -> bool {
|
|||
fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts: &Opts, scale: f64, reduce: f64, no_kern: bool) -> TextLayout {
|
||||
let r = opts.rect;
|
||||
let vert = bp.vert.unwrap_or(TextDir::Horizontal);
|
||||
// East Asian vertical text: CJK stands upright in text turned a quarter turn.
|
||||
let upright_text = vert == TextDir::EaVertical;
|
||||
// East Asian vertical text: CJK stands upright in text turned a quarter turn; stacked text
|
||||
// stands every character upright, one under the other.
|
||||
let upright_text = match vert {
|
||||
TextDir::EaVertical => Upright::Cjk,
|
||||
TextDir::Stacked => 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 => 90.0,
|
||||
TextDir::Vertical | TextDir::EaVertical | TextDir::Stacked | TextDir::MongolianVertical => 90.0,
|
||||
TextDir::Vertical270 => 270.0,
|
||||
TextDir::Horizontal => 0.0,
|
||||
} + bp.rot.unwrap_or(0.0);
|
||||
|
|
@ -1840,8 +1883,15 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
|||
line.baseline += oy;
|
||||
line.bottom += oy;
|
||||
line.shift_x(ox);
|
||||
if lines_ltr {
|
||||
// The line's place mirrored across the box: the first line ends up on the left.
|
||||
let d = inner.y0 + inner.y1 - line.top - line.bottom;
|
||||
line.top += d;
|
||||
line.baseline += d;
|
||||
line.bottom += d;
|
||||
}
|
||||
let Some(p) = shaped_paras.get(line.para) else { continue };
|
||||
if let Some((s, bx, by)) = bullet.take()
|
||||
if let Some((s, bx, _)) = bullet.take()
|
||||
&& let Some((_, st)) = p.bullet.as_ref()
|
||||
{
|
||||
let (_, g) = measure_str(&st.face, st.size, &s);
|
||||
|
|
@ -1851,7 +1901,7 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
|||
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 }, by + oy))
|
||||
.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(),
|
||||
cells: vec![],
|
||||
fake_bold: false,
|
||||
|
|
|
|||
|
|
@ -1088,3 +1088,33 @@ fn east_asian_vertical_text_marks_cjk_upright() {
|
|||
assert!(l.runs.iter().all(|r| !r.upright));
|
||||
assert!(is_upright('あ') && is_upright('、') && is_upright('A') && !is_upright('A') && !is_upright('1'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mongolian_lines_run_left_to_right_and_stacked_letters_stand_one_em_apart() {
|
||||
let (p, mut sh) = setup("one\ntwo", 200.0, 400.0);
|
||||
sh.text.as_mut().unwrap().body.vert = Some(deckcraft_model::text::TextDir::Vertical);
|
||||
let v = lay(&p, &sh, 200.0, 400.0);
|
||||
sh.text.as_mut().unwrap().body.vert = Some(deckcraft_model::text::TextDir::MongolianVertical);
|
||||
let m = lay(&p, &sh, 200.0, 400.0);
|
||||
assert_eq!(m.rotation, 90.0);
|
||||
// In layout space (turned a quarter turn) vert puts the first line at the top, which shows on
|
||||
// the right; mongolianVert mirrors the line places, so the first line shows on the left.
|
||||
assert!(v.lines[0].top < v.lines[1].top);
|
||||
assert!(m.lines[0].top > m.lines[1].top);
|
||||
let mid = (m.inner.y0 + m.inner.y1) / 2.0;
|
||||
assert!(((m.lines[0].top + m.lines[0].bottom) / 2.0 - mid + ((v.lines[0].top + v.lines[0].bottom) / 2.0 - mid)).abs() < 0.01);
|
||||
// Stacked: every letter upright, one em apart.
|
||||
let (p, mut sh) = setup("Lil", 200.0, 400.0);
|
||||
let t = sh.text.as_mut().unwrap();
|
||||
t.body.vert = Some(deckcraft_model::text::TextDir::Stacked);
|
||||
t.paragraphs[0].runs[0].props.size = Some(20.0);
|
||||
let s = lay(&p, &sh, 200.0, 400.0);
|
||||
assert!(s.runs.iter().filter(|r| !r.cells.is_empty()).all(|r| r.upright));
|
||||
// Its lines run left to right too.
|
||||
let (p2, mut sh2) = setup("one\ntwo", 200.0, 400.0);
|
||||
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);
|
||||
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:?}");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue