//! Vertical type (bundled fonts: `§` and `×` stand upright like CJK, letters lie on their side; //! Japanese text only with craft-fonts' faces, skipped without them). use super::*; use kurbo::Shape; use vectorcraft_doc::{CharStyle, Justify, TextKind}; use vectorcraft_geom::PathData; fn vertical(text: &str) -> TextObject { vertical_styled(text, CharStyle { size: 20.0, ..CharStyle::default() }) } fn vertical_styled(text: &str, style: CharStyle) -> TextObject { let mut t = TextObject::point(Point::ZERO, text, style); t.xf = Affine::IDENTITY; t.vertical = true; t } fn ink(l: &TextLayout, i: usize) -> Rect { l.glyphs[i].outline.bounding_box() } #[test] fn columns_run_down_and_follow_each_other_to_the_left() { let l = layout(FontDb::global(), &vertical("§§\n§")); assert!(l.vertical); let (a, b, c) = (ink(&l, 0), ink(&l, 1), ink(&l, 2)); assert!(b.center().y > a.center().y + 15.0, "down the column: {a:?} {b:?}"); assert!((a.center().x - b.center().x).abs() < 0.5, "one column"); assert!(c.center().x < a.center().x - 15.0, "the next column is to the left: {c:?}"); // Point type: the anchor is on the first column's centre line. assert!(a.center().x.abs() < 4.0, "{a:?}"); assert!(l.bounds.contains(a.center()) && l.bounds.contains(c.center())); } #[test] fn upright_marks_stand_and_letters_lie_on_their_side() { let h = layout(FontDb::global(), &TextObject::point(Point::ZERO, "§l", CharStyle { size: 20.0, ..CharStyle::default() })); let v = layout(FontDb::global(), &vertical("§l")); let (hs, hl, vs, vl) = (ink(&h, 0), ink(&h, 1), ink(&v, 0), ink(&v, 1)); assert!((vs.width() - hs.width()).abs() < 0.01 && (vs.height() - hs.height()).abs() < 0.01, "§ upright: {hs:?} {vs:?}"); assert!((vl.width() - hl.height()).abs() < 0.01 && (vl.height() - hl.width()).abs() < 0.01, "l turned: {hl:?} {vl:?}"); assert!(vl.width() > vl.height(), "a tall l lies across the column"); } #[test] fn caret_and_selection_turn_with_the_columns() { let l = layout(FontDb::global(), &vertical("§§§")); let (top, bottom) = caret_position(&l, "§".len()); assert!((top.y - bottom.y).abs() < 1e-6 && (top.x - bottom.x).abs() > 10.0, "a horizontal caret across the column: {top:?} {bottom:?}"); let between = (ink(&l, 0).center().y + ink(&l, 1).center().y) / 2.0; assert!((top.y - between).abs() < 3.0, "between the first two marks: {top:?} {between} {:?} {:?}", ink(&l, 0), ink(&l, 1)); let q = selection_quads(&l, 0, "§§".len()); let r = Rect::from_points(q[0][0], q[0][2]); assert!(r.height() > r.width() && r.contains(ink(&l, 0).center()) && r.contains(ink(&l, 1).center()) && !r.contains(ink(&l, 2).center())); // Clicking a mark finds it. let c = ink(&l, 2).center(); assert_eq!(hit_byte(&l, c + Vec2::new(0.0, 6.0)), "§§§".len()); assert_eq!(hit_byte(&l, c - Vec2::new(0.0, 6.0)), "§§".len()); } #[test] fn area_type_starts_at_the_right_edge_and_wraps_to_the_left() { let mut t = vertical(&"§".repeat(12)); t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 100.0, 105.0).to_path(0.1)) }; let l = layout(FontDb::global(), &t); let first = ink(&l, 0); assert!(first.x1 <= 100.0 + 1e-6 && first.x1 > 80.0 && first.y0 >= 0.0, "top right: {first:?}"); let cols: Vec = l.glyphs.iter().map(|g| (g.outline.bounding_box().center().x * 10.0).round()).collect(); let mut distinct = cols.clone(); distinct.dedup(); assert!(distinct.len() >= 2 && distinct.windows(2).all(|w| w[1] < w[0]), "columns right to left: {distinct:?}"); assert!(l.glyphs.iter().all(|g| g.outline.bounding_box().y1 <= 105.0 + 1e-6), "inside the frame"); } /// `baselines`: one per line that holds characters, along it for horizontal type and down the /// column's centre line for vertical type (where point type's anchor is); none on a path. #[test] fn baselines_run_along_lines_and_down_column_centres() { let h = layout( FontDb::global(), &TextObject::point( Point::ZERO, "§§ §", CharStyle { size: 20.0, ..CharStyle::default() }, ), ); let hb = h.baselines(); assert_eq!(hb.len(), 2, "the empty line has none: {hb:?}"); assert!(hb[0].0 == Point::ZERO && hb[0].1.y == 0.0 && hb[0].1.x > 15.0, "the first baseline runs from the anchor: {hb:?}"); assert!(hb[1].0.y > 40.0 && hb[1].0.y == hb[1].1.y && hb[1].1.x > hb[1].0.x, "the third line's, lower: {hb:?}"); let v = layout( FontDb::global(), &vertical( "§§ §", ), ); let vb = v.baselines(); let (a, c) = (ink(&v, 0), ink(&v, 2)); assert_eq!(vb.len(), 2, "{vb:?}"); assert!(vb[0].0.x.abs() < 4.0 && vb[0].0.x == vb[0].1.x && vb[0].1.y > vb[0].0.y + 30.0, "down the first column's centre: {vb:?}"); assert!((vb[0].0.x - a.center().x).abs() < 4.0 && (vb[1].0.x - c.center().x).abs() < 4.0, "through the glyphs: {vb:?} {a:?} {c:?}"); let mut on_path = vertical("ab"); on_path.kind = TextKind::OnPath { path: PathData::from_bezpath(&kurbo::Line::new((0.0, 0.0), (100.0, 0.0)).to_path(0.1)), start: 0.0, end: None }; assert!(layout(FontDb::global(), &on_path).baselines().is_empty()); } #[test] fn type_on_a_path_stays_horizontal() { let mut t = vertical("ab"); t.kind = TextKind::OnPath { path: PathData::from_bezpath(&kurbo::Line::new((0.0, 0.0), (100.0, 0.0)).to_path(0.1)), start: 0.0, end: None }; assert!(!layout(FontDb::global(), &t).vertical); } #[test] fn three_digits_stand_across_the_column_squeezed_into_one_em() { let l = layout(FontDb::global(), &vertical("§100§")); let block: Vec = (1..4).map(|i| ink(&l, i)).collect(); assert!(block.windows(2).all(|w| w[1].center().x > w[0].center().x + 2.0 && (w[1].center().y - w[0].center().y).abs() < 1.0), "{block:?}"); let span = block[0].union(block[2]); assert!(span.width() <= 20.0 + 1e-6, "squeezed into the 20 pt em: {span:?}"); assert!(ink(&l, 4).center().y - ink(&l, 0).center().y < 45.0, "one em of the column"); } #[test] fn two_digits_stand_across_the_column_and_longer_numbers_lie_on_their_side() { let l = layout(FontDb::global(), &vertical("§12§2026")); let (one, two) = (ink(&l, 1), ink(&l, 2)); assert!((one.center().y - two.center().y).abs() < 1.0 && two.center().x > one.center().x + 3.0, "12 side by side: {one:?} {two:?}"); assert!(one.height() > one.width(), "upright digits are taller than wide: {one:?}"); let (before, after) = (ink(&l, 0), ink(&l, 3)); assert!(after.center().y - before.center().y < 45.0, "the block takes one em (20 pt) of the column"); let year: Vec = (4..8).map(|i| ink(&l, i)).collect(); assert!(year.windows(2).all(|w| w[1].center().y > w[0].center().y + 5.0), "2026 runs down the column on its side"); } /// Each line's text, from the layout's line ranges. fn line_texts(t: &TextObject, l: &TextLayout) -> Vec { let text = t.plain_text(); l.lines.iter().map(|li| text.get(li.start..li.end).unwrap_or("").trim_end().to_string()).collect() } #[test] fn kinsoku_keeps_closing_marks_off_line_starts_and_opening_brackets_off_line_ends() { for vertical_type in [false, true] { // Five characters fit a line: without kinsoku "。" would start the second line. let text = "あいうえお。かきくけ「こさしすせそ」"; let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() }); t.xf = Affine::IDENTITY; t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 101.0, 400.0).to_path(0.1)) }; if vertical_type { t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 400.0, 101.0).to_path(0.1)) }; t.vertical = true; } let l = layout(FontDb::global(), &t); let lines = line_texts(&t, &l); assert!(lines.len() > 2, "{lines:?}"); for (i, line) in lines.iter().enumerate() { if i > 0 { let first = line.chars().next().unwrap_or(' '); assert!(!crate::shape::no_line_start(first), "line {i} starts with {first}: {lines:?}"); } let last = line.chars().last().unwrap_or(' '); assert!(!crate::shape::no_line_end(last), "line {i} ends with {last}: {lines:?}"); } } } /// Each glyph's cell down the (single) column: from its origin over its advance. A glyph that /// doesn't advance (the rest of a tate-chu-yoko block) shares the cell of the glyph before it. fn column_cells(l: &TextLayout) -> Vec<(f64, f64)> { let mut cells: Vec<(f64, f64)> = Vec::with_capacity(l.glyphs.len()); for g in &l.glyphs { let cell = match cells.last() { Some(&prev) if g.advance == 0.0 => prev, _ => (g.origin.y, g.origin.y + g.advance), }; cells.push(cell); } cells } /// One column, cells one after the other, each glyph's ink inside its own cell (within `tol`) /// and no two glyphs' ink overlapping. fn assert_own_cells(l: &TextLayout, text: &str, tol: f64) { let label = |i: usize| &text[l.glyphs[i].byte..l.glyphs[i].byte + l.glyphs[i].len]; let cells = column_cells(l); for (i, g) in l.glyphs.iter().enumerate() { assert!((g.origin.x - l.glyphs[0].origin.x).abs() < 1e-9, "{:?} leaves the column: {g:?}", label(i)); if let Some(next) = l.glyphs.get(i + 1) { assert!((next.origin.y - (g.origin.y + g.advance)).abs() < 1e-9, "{:?} starts where {:?} ends", label(i + 1), label(i)); } if g.outline.elements().is_empty() { continue; } let (r, (top, bottom)) = (ink(l, i), cells[i]); assert!(r.y0 >= top - tol && r.y1 <= bottom + tol, "{:?}: ink {r:?} outside its cell {top}..{bottom}", label(i)); for j in (0..i).filter(|&j| !l.glyphs[j].outline.elements().is_empty()) { let o = ink(l, j); let (ox, oy) = (r.x1.min(o.x1) - r.x0.max(o.x0), r.y1.min(o.y1) - r.y0.max(o.y0)); assert!(ox <= tol || oy <= tol, "{:?} {r:?} overlaps {:?} {o:?}", label(i), label(j)); } } } /// Index of the glyph of `needle`'s first byte in `text`. fn glyph_of(l: &TextLayout, text: &str, needle: &str) -> usize { let b = text.find(needle).unwrap(); l.glyphs.iter().position(|g| g.byte == b).unwrap() } /// Issue #260 with bundled glyphs (`§` stands in for the upright CJK characters). #[test] fn numbers_keep_their_own_cells_down_the_column() { let text = "§§ §§§ 10§22§§ 1§ 100§ 2026§"; let l = layout(FontDb::global(), &vertical(text)); assert_own_cells(&l, text, 0.25); // Tate-chu-yoko: one em of the column, centred on the column's centre line (x = 0), the next // character right after it. for block in ["10§", "22§", "100§"] { let first = glyph_of(&l, text, block); let digits = block.len() - '§'.len_utf8(); let after = first + digits; let g = &l.glyphs[first]; assert!((g.advance - 20.0).abs() < 1e-9, "{block}: {g:?}"); assert!(l.glyphs[first + 1..after].iter().all(|d| d.advance == 0.0), "{block}"); assert!((l.glyphs[after].origin.y - (g.origin.y + 20.0)).abs() < 1e-9, "{block}: the next character follows the block's em"); let inks = (first..after).map(|i| ink(&l, i)).reduce(|a, b| a.union(b)).unwrap(); assert!(inks.center().x.abs() < 1.0, "{block} centred on the column: {inks:?}"); assert!((inks.center().y - (g.origin.y + 10.0)).abs() < 1.5, "{block} centred in its em: {inks:?} {g:?}"); } // A single digit and longer numbers lie on their side, advancing by their own length. let one = glyph_of(&l, text, "1§"); let r = ink(&l, one); assert!(r.width() > r.height() && l.glyphs[one].advance < 20.0, "1 on its side: {r:?}"); let year = glyph_of(&l, text, "2026"); let len: f64 = l.glyphs[year..year + 4].iter().map(|g| g.advance).sum(); assert!(len > 30.0 && (l.glyphs[year + 4].origin.y - l.glyphs[year].origin.y - len).abs() < 1e-9, "2026 takes its length: {len}"); // Half-width spaces take their own (narrow) width. for (i, g) in l.glyphs.iter().enumerate().filter(|(_, g)| text[g.byte..].starts_with(' ')) { assert!(g.advance > 2.0 && g.advance < 10.0, "space {i}: {g:?}"); } } #[test] fn justified_columns_keep_tate_chu_yoko_blocks_whole_and_marks_centred() { let text = "§10§§"; let mut t = vertical(text); t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 40.0, 200.0).to_path(0.1)) }; t.para.justify = Justify::JustifyAll; let l = layout(FontDb::global(), &t); assert_eq!(l.lines.len(), 1); // 200 pt of column, four 20 pt cells: three gaps of 40 pt between them, none inside "10". let (one, zero) = (ink(&l, 1), ink(&l, 2)); assert!((one.center().y - zero.center().y).abs() < 0.5 && zero.x0 > one.x1, "10 side by side: {one:?} {zero:?}"); let marks: Vec = [0, 3, 4].iter().map(|&i| ink(&l, i)).collect(); assert!((marks[1].center().y - marks[0].center().y - 120.0).abs() < 1e-6, "{marks:?}"); assert!((marks[2].center().y - marks[1].center().y - 60.0).abs() < 1e-6, "{marks:?}"); assert!(marks.iter().all(|m| (m.center().x - marks[0].center().x).abs() < 1e-6), "upright marks stay on the centre line: {marks:?}"); assert!(marks[2].y1 <= 200.0 + 1e-6, "inside the frame: {marks:?}"); } #[test] fn tracking_spaces_upright_marks_without_moving_them_off_the_centre_line() { let plain = layout(FontDb::global(), &vertical("§§")); let tracked = layout(FontDb::global(), &vertical_styled("§§", CharStyle { size: 20.0, tracking: 200.0, ..CharStyle::default() })); for i in 0..2 { assert!((ink(&tracked, i).center().x - ink(&plain, i).center().x).abs() < 1e-6, "{:?} {:?}", ink(&tracked, i), ink(&plain, i)); } let step = ink(&tracked, 1).center().y - ink(&tracked, 0).center().y; assert!((step - 24.0).abs() < 1e-6, "one em plus 200/1000 em of tracking: {step}"); } /// Issue #260 as reported, with a Japanese face of craft-fonts (whose own digits are proportional). #[test] fn japanese_numbers_keep_their_own_cells_down_the_column() { let Some(face) = crate::craft_fonts::japanese_ui_fonts(false).into_iter().next() else { eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout to run it)"); return; }; let db = FontDb::with_font_dirs(vec![]); let text = "はただ商店 秋のセール 10月22日まで"; let t = vertical_styled(text, CharStyle { size: 28.0, font_family: face.family.into(), font_style: face.style.into(), ..CharStyle::default() }); let l = layout(&db, &t); assert!(l.glyphs.iter().filter(|g| !text[g.byte..].starts_with(' ')).all(|g| g.gid != 0), "real glyphs, no tofu"); assert_own_cells(&l, text, 0.5); for block in ["10月", "22日"] { let first = glyph_of(&l, text, block); let g = &l.glyphs[first]; assert!((g.advance - 28.0).abs() < 1e-9 && l.glyphs[first + 1].advance == 0.0, "{block}: one em"); assert!((l.glyphs[first + 2].origin.y - (g.origin.y + 28.0)).abs() < 1e-9, "{block}: the next character follows the em"); let inks = ink(&l, first).union(ink(&l, first + 1)); assert!(inks.center().x.abs() < 2.8, "{block} centred on the column: {inks:?}"); } } /// Upright glyphs take their cell from the font's vertical metrics: the advance down the column, /// and where the glyph hangs from its vertical origin (VORG, or the glyph's top and top side /// bearing). Fonts without them keep one em and the em box centre. #[test] fn upright_glyphs_follow_the_fonts_vertical_metrics() { use crate::test_fonts::{VERTICAL_FAMILY, VERTICAL_TALL, vertical_font}; for vorg in [false, true] { let db = FontDb::with_font_dirs(vec![]); db.add_font(vertical_font(vorg).unwrap()); let text = format!("{VERTICAL_TALL}{VERTICAL_TALL}"); let mut t = vertical(&text); t.runs[0].style.font_family = VERTICAL_FAMILY.into(); let l = layout(&db, &t); let step = l.glyphs[1].origin.y - l.glyphs[0].origin.y; assert!((step - 28.0).abs() < 0.01, "1.4 em down the column at 20 pt (vorg {vorg}): {step}"); // The glyph hangs from its vertical origin, 1.1 em above the baseline: its top is // (1.1 em − its own top) below the top of its cell. let face = db.face(VERTICAL_FAMILY, "Regular").unwrap(); let (_, origin) = face.vertical_glyph(face.glyph_for(VERTICAL_TALL)).unwrap(); assert!((origin - 1100.0).abs() < 1.0, "vorg {vorg}: {origin}"); let top = db.outline(&face, face.glyph_for(VERTICAL_TALL)).bounding_box().y0; // y-down: −yMax let cell_top = l.glyphs[0].origin.y; let expected = cell_top + (1.1 + top / 1000.0) * 20.0; assert!((ink(&l, 0).y0 - expected).abs() < 0.05, "vorg {vorg}: ink top {} vs {expected}", ink(&l, 0).y0); } // The same glyph in the font without vertical metrics: one em, as before. let l = layout(FontDb::global(), &vertical("§§")); assert!((l.glyphs[1].origin.y - l.glyphs[0].origin.y - 20.0).abs() < 0.01); } /// Mojikumi (JLREQ 3.1): with Line-end Punctuation Half Width, a closing mark ending a line is set /// half width, a closing mark followed by punctuation loses the space after it, and an opening /// bracket after another loses the space before it; vertical type the same way down the column. /// Needs a font with full-width Japanese punctuation (skipped without one). #[test] fn mojikumi_halves_line_end_and_consecutive_punctuation() { use vectorcraft_doc::Mojikumi; let lay = |text: &str, m: Mojikumi, vertical_type: bool| { let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() }); t.xf = Affine::IDENTITY; t.vertical = vertical_type; t.para.mojikumi = m; layout(FontDb::global(), &t) }; let solid = lay("一。", Mojikumi::None, false); if (solid.glyphs[1].advance - 20.0).abs() > 2.0 { return; // no font with full-width punctuation here } for vertical_type in [false, true] { let adv = |text: &str, m| lay(text, m, vertical_type).glyphs.iter().map(|g| g.advance).collect::>(); // Line end: 。 half width. assert!((adv("一。", Mojikumi::LineEndHalf)[1] - 10.0).abs() < 0.01, "vertical {vertical_type}"); assert!((adv("一。", Mojikumi::None)[1] - 20.0).abs() < 0.01); // 」 before 「: the closing mark's space goes; 「 keeps its own. let a = adv("一」「二", Mojikumi::LineEndHalf); assert!((a[1] - 10.0).abs() < 0.01 && (a[2] - 20.0).abs() < 0.01, "{a:?}"); // 「「: the second bracket loses the space before it and is drawn half an em earlier. let a = adv("「「一", Mojikumi::LineEndHalf); assert!((a[0] - 20.0).abs() < 0.01 && (a[1] - 10.0).abs() < 0.01, "{a:?}"); let (on, off) = (lay("「「一", Mojikumi::LineEndHalf, vertical_type), lay("「「一", Mojikumi::None, vertical_type)); let along = |r: Rect| if vertical_type { r.y0 } else { r.x0 }; let moved = along(off.glyphs[1].outline.bounding_box()) - along(on.glyphs[1].outline.bounding_box()); assert!((moved - 10.0).abs() < 0.01, "vertical {vertical_type}: the second 「 is drawn half an em earlier ({moved})"); let across = |r: Rect| if vertical_type { r.x0 } else { r.y0 }; let drift = across(off.glyphs[1].outline.bounding_box()) - across(on.glyphs[1].outline.bounding_box()); assert!(drift.abs() < 0.01, "vertical {vertical_type}: and not moved across the line ({drift})"); // Text without punctuation is untouched. assert_eq!(adv("一二", Mojikumi::LineEndHalf), adv("一二", Mojikumi::None)); } } /// Mojikumi (JLREQ 3.2.2): a quarter em between Japanese and Latin letters or digits, either way /// round, horizontal and vertical; none inside a tate-chu-yoko block, none left at a line's end, and /// none with Mojikumi None. Needs a font with full-width Japanese (skipped without one). #[test] fn mojikumi_spaces_japanese_from_latin_by_a_quarter_em() { use vectorcraft_doc::Mojikumi; let lay = |text: &str, m: Mojikumi, vertical_type: bool| { let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() }); t.xf = Affine::IDENTITY; t.vertical = vertical_type; t.para.mojikumi = m; layout(FontDb::global(), &t) }; if (lay("雅", Mojikumi::None, false).glyphs[0].advance - 20.0).abs() > 2.0 { return; // no font with full-width Japanese here } for vertical_type in [false, true] { let extra = |text: &str| -> Vec { let (on, off) = (lay(text, Mojikumi::LineEndHalf, vertical_type), lay(text, Mojikumi::None, vertical_type)); on.glyphs.iter().zip(&off.glyphs).map(|(a, b)| a.advance - b.advance).collect() }; // 雅楽 2026 年: after 楽 and after 6. assert_eq!(extra("雅楽2026年").iter().map(|x| (x * 100.0).round() / 100.0).collect::>(), [0.0, 5.0, 0.0, 0.0, 0.0, 5.0, 0.0]); // Latin first, and nothing after the last character of the line. assert_eq!(extra("AB雅").iter().map(|x| x.round()).collect::>(), [0.0, 5.0, 0.0]); assert_eq!(extra("雅A").iter().map(|x| x.round()).collect::>(), [5.0, 0.0]); // Punctuation takes no Japanese–Latin space (the closing bracket isn't at the line's end). assert!(extra("「A」です").iter().all(|x| x.abs() < 0.01), "{:?}", extra("「A」です")); } // A tate-chu-yoko block is set as a Japanese character, with no space inside or around it. let on = lay("第10回", Mojikumi::LineEndHalf, true); let off = lay("第10回", Mojikumi::None, true); assert!(on.glyphs.iter().zip(&off.glyphs).all(|(a, b)| (a.advance - b.advance).abs() < 0.01)); } /// Mojikumi (JLREQ 3.1.5): with Line-end Punctuation Half Width, an opening bracket starting a /// wrapped line is set flush with the line's start (the space before it goes), giving the line half /// an em more room; at the start of a paragraph it keeps its full width. Horizontal and vertical. /// Needs a font with full-width Japanese punctuation (skipped without one). #[test] fn mojikumi_sets_an_opening_bracket_flush_at_the_start_of_a_wrapped_line() { use vectorcraft_doc::Mojikumi; // 20 pt type in a frame three and a half ems across the lines. let lay = |text: &str, m: Mojikumi, vertical_type: bool| { let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() }); t.xf = Affine::IDENTITY; t.vertical = vertical_type; t.para.mojikumi = m; let frame = if vertical_type { Rect::new(0.0, 0.0, 200.0, 70.0) } else { Rect::new(0.0, 0.0, 70.0, 200.0) }; t.kind = TextKind::Area { frame: PathData::from_bezpath(&frame.to_path(0.1)) }; layout(FontDb::global(), &t) }; if (lay("一「", Mojikumi::None, false).glyphs[1].advance - 20.0).abs() > 2.0 { return; // no font with full-width punctuation here } for vertical_type in [false, true] { let along = |r: Rect| if vertical_type { r.y0 } else { r.x0 }; // 一二三 / 「四五六: the bracket can't end the first line, so it starts the second. let (on, off) = (lay("一二三「四五六", Mojikumi::LineEndHalf, vertical_type), lay("一二三「四五六", Mojikumi::None, vertical_type)); let moved = along(off.glyphs[3].outline.bounding_box()) - along(on.glyphs[3].outline.bounding_box()); assert!((moved - 10.0).abs() < 0.01, "vertical {vertical_type}: 「 is drawn half an em earlier ({moved})"); let across = |r: Rect| if vertical_type { r.x0 } else { r.y0 }; let drift = across(off.glyphs[3].outline.bounding_box()) - across(on.glyphs[3].outline.bounding_box()); assert!(drift.abs() < 0.01, "vertical {vertical_type}: and not moved across the line ({drift})"); assert!((on.glyphs[3].advance - 10.0).abs() < 0.01, "vertical {vertical_type}: {}", on.glyphs[3].advance); // The half em it gave up lets 六 stay on the line. assert_eq!(on.glyphs[6].line, on.glyphs[3].line, "vertical {vertical_type}"); assert_ne!(off.glyphs[6].line, off.glyphs[3].line, "vertical {vertical_type}"); // At the start of a paragraph the bracket keeps its full width. let first = lay("「一」", Mojikumi::LineEndHalf, vertical_type); assert!((first.glyphs[0].advance - 20.0).abs() < 0.01, "vertical {vertical_type}: {}", first.glyphs[0].advance); } } /// Burasagari: in a measure of exactly five ems, a comma that would be the sixth character goes to /// the next line with the character before it (None) or hangs outside the line (Standard, Forced); /// a full stop that is the fifth character stays inside (None, Standard) or hangs while the four /// before it fill the measure (Forced). Closing brackets don't hang. Horizontal and vertical, with /// Line-end Punctuation Half Width (the hanging mark is half width). Needs a font with full-width /// Japanese punctuation (skipped without one). #[test] fn burasagari_hangs_a_comma_or_full_stop_outside_the_line() { use vectorcraft_doc::{Burasagari, Mojikumi}; // 20 pt type, justified, in a frame five ems along the lines. let lay = |text: &str, b: Burasagari, vertical_type: bool| { let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() }); t.xf = Affine::IDENTITY; t.vertical = vertical_type; t.para.mojikumi = Mojikumi::LineEndHalf; t.para.justify = Justify::JustifyLeft; t.para.burasagari = b; let frame = if vertical_type { Rect::new(0.0, 0.0, 200.0, 100.0) } else { Rect::new(0.0, 0.0, 100.0, 200.0) }; t.kind = TextKind::Area { frame: PathData::from_bezpath(&frame.to_path(0.1)) }; layout(FontDb::global(), &t) }; if (lay("一、二", Burasagari::None, false).glyphs[1].advance - 20.0).abs() > 2.0 { return; // no font with full-width punctuation here } let advances = |l: &TextLayout, n: usize| l.glyphs.iter().take(n).map(|g| (g.advance * 100.0).round() / 100.0).collect::>(); for vertical_type in [false, true] { // 、 as the sixth character. let none = lay("一二三四五、六七", Burasagari::None, vertical_type); assert_ne!(none.glyphs[4].line, none.glyphs[0].line, "vertical {vertical_type}: 五、 go to the next line"); for b in [Burasagari::Standard, Burasagari::Forced] { let l = lay("一二三四五、六七", b, vertical_type); assert_eq!(l.glyphs[5].line, l.glyphs[0].line, "vertical {vertical_type} {b:?}: 、 hangs"); assert_eq!(advances(&l, 6), [20.0, 20.0, 20.0, 20.0, 20.0, 10.0], "vertical {vertical_type} {b:?}: five ems inside, half an em outside"); assert_eq!(l.glyphs[6].line, l.glyphs[0].line + 1, "vertical {vertical_type} {b:?}"); } // 。 as the fifth character: it fits. for b in [Burasagari::None, Burasagari::Standard] { let l = lay("一二三四。六七", b, vertical_type); assert_eq!(l.glyphs[5].line, l.glyphs[0].line + 1, "vertical {vertical_type} {b:?}"); // Four ems and a half: the half em left is spread between the five characters. assert_eq!(advances(&l, 5), [22.5, 22.5, 22.5, 22.5, 10.0], "vertical {vertical_type} {b:?}: 。 inside"); } let forced = lay("一二三四。六七", Burasagari::Forced, vertical_type); let a = advances(&forced, 5); assert!((a[..4].iter().sum::() - 100.0).abs() < 0.05, "vertical {vertical_type}: the four fill the measure: {a:?}"); assert_eq!(a[4], 10.0, "vertical {vertical_type}"); assert_eq!(forced.glyphs[4].line, forced.glyphs[0].line); // A closing bracket doesn't hang: 」 as the sixth character goes on with 五. for b in [Burasagari::Standard, Burasagari::Forced] { let l = lay("一二三四五」六七", b, vertical_type); assert_ne!(l.glyphs[4].line, l.glyphs[0].line, "vertical {vertical_type} {b:?}"); } // The full-width comma and full stop hang too. for mark in [',', '.'] { let l = lay(&format!("一二三四五{mark}六七"), Burasagari::Standard, vertical_type); assert_eq!(l.glyphs[5].line, l.glyphs[0].line, "vertical {vertical_type}: {mark} hangs"); } } // Horizontal: the hanging mark starts at the frame's edge. let l = lay("一二三四五、六七", Burasagari::Standard, false); assert!((l.glyphs[5].origin.x - 100.0).abs() < 0.01, "{:?}", l.glyphs[5].origin); }