mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Merge pull request #344 from imachus/pr/ja-justify
Type: justified Japanese lines spread between their characters
This commit is contained in:
@@ -8,7 +8,7 @@ use vectorcraft_doc::{CharStyle, Justify, ParaStyle, PathEffect, TextKind, TextO
|
||||
use crate::composer::{Breakpoint, compose};
|
||||
use crate::fontdb::FontDb;
|
||||
use crate::hyphen::hyphen_points;
|
||||
use crate::shape::{SGlyph, Tcy, cap_x_heights, hyphen_glyph, no_line_end, no_line_start, shape_range, style_metrics};
|
||||
use crate::shape::{SGlyph, Tcy, cap_x_heights, hyphen_glyph, is_cjk, no_line_end, no_line_start, shape_range, style_metrics};
|
||||
use crate::{Composer, FirstBaseline, LayoutOptions, LineInfo, OtFeatures, PositionedGlyph, TextLayout};
|
||||
|
||||
const EPS: f64 = 1e-6;
|
||||
@@ -846,10 +846,24 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
Justify::JustifyAll => (0, true),
|
||||
};
|
||||
let justify = justify && regions.is_some();
|
||||
let (mut per_space, mut per_gap) = (0.0, 0.0);
|
||||
let (mut per_space, mut per_gap, mut per_cjk) = (0.0, 0.0, 0.0);
|
||||
let spaces = sg[i..trimmed].iter().filter(|g| g.is_space()).count();
|
||||
// Japanese (and Chinese) lines are justified between their characters (JLREQ 3.8): the
|
||||
// gaps next to a CJK character, not inside a Latin word or a tate-chu-yoko block.
|
||||
let cjk_gap = |j: usize| {
|
||||
j + 1 < trimmed
|
||||
&& sg
|
||||
.get(j)
|
||||
.zip(sg.get(j + 1))
|
||||
.is_some_and(|(a, b)| !a.is_space() && !b.is_space() && !b.continues_tcy() && a.ch != '\t' && (is_cjk(a.ch) || is_cjk(b.ch)))
|
||||
};
|
||||
let cjk_gaps = (i..trimmed).filter(|&j| cjk_gap(j)).count();
|
||||
if justify && (width - w).abs() > EPS {
|
||||
if spaces > 0 {
|
||||
if cjk_gaps > 0 && width > w {
|
||||
// Spread over the CJK gaps and the word spaces alike.
|
||||
let per = (width - w) / (cjk_gaps + spaces) as f64;
|
||||
(per_space, per_cjk) = (per, per);
|
||||
} else if spaces > 0 {
|
||||
// Composed lines may shrink word spaces (never below zero).
|
||||
per_space =
|
||||
((width - w) / spaces as f64).max(-sg[i..trimmed].iter().filter(|g| g.is_space()).map(|g| g.adv).fold(f64::MAX, f64::min));
|
||||
@@ -888,6 +902,10 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
} else if j < trimmed {
|
||||
if g.is_space() {
|
||||
adv += per_space;
|
||||
} else if per_cjk != 0.0 {
|
||||
if cjk_gap(j) {
|
||||
adv += per_cjk;
|
||||
}
|
||||
} else if j + 1 < trimmed && sg.get(j + 1).is_some_and(|next| !next.continues_tcy()) {
|
||||
// Between glyphs, never inside a tate-chu-yoko block (one cell).
|
||||
adv += per_gap;
|
||||
|
||||
@@ -137,7 +137,7 @@ pub(crate) fn no_line_end(c: char) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn is_cjk(c: char) -> bool {
|
||||
pub(crate) fn is_cjk(c: char) -> bool {
|
||||
matches!(c as u32, 0x2E80..=0x9FFF | 0xAC00..=0xD7AF | 0xF900..=0xFAFF | 0xFF00..=0xFFEF | 0x20000..=0x2FFFF)
|
||||
}
|
||||
|
||||
|
||||
@@ -491,3 +491,34 @@ fn vertical_area_type_wraps_into_columns_inside_the_frame() {
|
||||
assert!(l.glyphs.iter().all(|g| g.origin.x >= 0.0 && g.origin.x <= 100.0 && g.origin.y >= 0.0 && g.origin.y <= 65.0));
|
||||
assert!(l.glyphs[l.lines[1].glyph_start].origin.x < l.glyphs[0].origin.x);
|
||||
}
|
||||
|
||||
/// Justified Japanese lines (no word spaces) spread the leftover room over the gaps between their
|
||||
/// characters (JLREQ 3.8), not inside a Latin word; the last line of a paragraph stays set solid.
|
||||
#[test]
|
||||
fn justified_japanese_lines_spread_between_characters() {
|
||||
// The ideographs' advance in the fonts at hand, measured; five fit a line, 10 pt left over.
|
||||
let measure = layout(db(), &point("一", style(20.0)));
|
||||
let a = measure.glyphs[0].advance;
|
||||
let width = 5.0 * a + 10.0;
|
||||
let t = area("一二三四五六七", style(20.0), Rect::new(0.0, 0.0, width, 400.0), Justify::JustifyLeft);
|
||||
let l = layout(db(), &t);
|
||||
let first = &l.glyphs[l.lines[0].glyph_start..l.lines[0].glyph_end];
|
||||
assert_eq!(first.len(), 5);
|
||||
for w in first.windows(2) {
|
||||
assert!((w[1].origin.x - w[0].origin.x - (a + 2.5)).abs() < 0.01, "10 pt over 4 gaps");
|
||||
}
|
||||
let end = first.last().map(|g| g.origin.x + a).unwrap();
|
||||
assert!((end - width).abs() < 0.01, "the line reaches the frame's edge: {end} vs {width}");
|
||||
// The last line keeps its natural spacing.
|
||||
let last = &l.glyphs[l.lines[1].glyph_start..l.lines[1].glyph_end];
|
||||
assert!((last[1].origin.x - last[0].origin.x - a).abs() < 0.01);
|
||||
// A Latin word inside a Japanese line keeps its letters together; the gaps around it share.
|
||||
let t = area("一二ABC三四五六七八九十一二三", style(20.0), Rect::new(0.0, 0.0, 8.0 * a + 10.0, 400.0), Justify::JustifyLeft);
|
||||
let l = layout(db(), &t);
|
||||
let g = &l.glyphs[l.lines[0].glyph_start..l.lines[0].glyph_end];
|
||||
let at = g.iter().position(|g| g.byte == "一二".len()).unwrap();
|
||||
let solid =
|
||||
|k: usize| (g[k + 1].origin.x - g[k].origin.x - layout(db(), &point(&"ABC"[k - at..=k - at], style(20.0))).glyphs[0].advance).abs() < 0.01;
|
||||
assert!(solid(at) && solid(at + 1), "A–B–C set solid");
|
||||
assert!(g[at].origin.x - g[at - 1].origin.x > a + 0.1, "二–A takes its share");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user