mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 02:32:22 +00:00
The Paragraph panel's Mojikumi Set lists None, YakumonoHankaku,
GyomatsuYakumonoHankaku, GyomatsuYakumonoZenkaku and YakumonoZenkaku,
then Line-end Punctuation Half Width, and text.setFormat {mojikumi}
takes yakumonoHankaku, gyomatsuYakumonoHankaku, gyomatsuYakumonoZenkaku
and yakumonoZenkaku besides none and lineEndHalf.
The spacing of each set is VectorCraft's own rule, built on the help
page's one-line description of each set:
- YakumonoHankaku sets every bracket, comma and full stop half width,
takes a quarter em off each side of a middle dot, and puts a quarter
em between Japanese and Latin letters or digits.
- GyomatsuYakumonoHankaku keeps punctuation full width inside a line,
sets an opening bracket starting a line flush and a closing mark
ending it half width, and takes the quarter em after a middle dot
ending a line off.
- GyomatsuYakumonoZenkaku keeps punctuation full width and adds the
quarter em between Japanese and Latin text.
- YakumonoZenkaku sets characters as None does.
Both composers and vertical type measure each line as it is set. A
right-to-left paragraph, whose line ends are measured untrimmed, also
draws them untrimmed, and a right-to-left line that ends between a
Japanese and a Latin character is measured without the quarter em the
set adds there. With a Desired glyph scaling other than 100%, the
space a set takes off before an opening bracket or middle dot, and the
Japanese-Latin space recorded at a line's end, scale with the glyph.
The sets save with the document and paragraph styles; new type and
Reset Panel keep Line-end Punctuation Half Width. SVG export lays type
with middle dots out again for its positioned spans. A .ai save
reports a paragraph with one of the new sets as left out.
Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md,
docs/gaps.md.
558 lines
26 KiB
Rust
558 lines
26 KiB
Rust
use super::flow::flow;
|
||
use super::region::Region;
|
||
use super::{Affine, Ctx, Mojikumi, Punct, Range, Rect, SGlyph, Tcy, TextLayout, TextObject, VerticalAlign, continues_cluster, is_cjk, punct};
|
||
|
||
/// System Layout on or Fractional Widths off: each character that takes space advances by a whole
|
||
/// number of points, at least one, so small type does not collapse onto itself. A zero advance (a
|
||
/// tate-chu-yoko continuation, a zero-width mark) stays zero. Right to left, a cluster with manual
|
||
/// kerning moves by the difference the kerning makes to its rounded advance, so the rounding
|
||
/// changes the space before the next character in reading order and not the space after the one
|
||
/// before it. Each side of the Insert Aki is rounded to whole points on its own and added to the
|
||
/// rounded advance, so justification keeps it at that width.
|
||
pub(super) fn whole_character_widths(glyphs: &mut [SGlyph]) {
|
||
let whole = |adv: f64| if adv.is_finite() && adv > 0.0 { adv.round().max(1.0) } else { adv };
|
||
let points = |aki: f64| if aki.is_finite() { aki.round() } else { 0.0 };
|
||
for cluster in glyphs.chunk_by_mut(|a, b| continues_cluster(Some(a), Some(b))) {
|
||
// The cluster's last glyph carries the manual kerning and the Insert Aki in its advance.
|
||
let (adv, shift) = cluster.last().map_or((0.0, 0.0), |g| (g.adv - g.aki, g.kern_shift - g.aki_shift));
|
||
let rounded_shift = Some(whole(adv) - whole(adv - shift)).filter(|s| s.is_finite()).unwrap_or(shift);
|
||
for g in cluster {
|
||
// The space before the character and the space after it are each rounded.
|
||
let aki_shift = points(g.aki_shift);
|
||
let aki = if g.aki == 0.0 { 0.0 } else { aki_shift + points(g.aki - g.aki_shift) };
|
||
g.adv = whole(g.adv - g.aki) + aki;
|
||
g.kern_shift = if shift != 0.0 { rounded_shift } else { g.kern_shift - g.aki_shift } + aki_shift;
|
||
g.aki = aki;
|
||
g.aki_shift = aki_shift;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Tate-chu-yoko: a horizontal block one em of the column long, squeezed to the em when wider.
|
||
///
|
||
/// Automatic: a run of two or three half-width digits (`10`月, `100`年). A single digit and a
|
||
/// longer number (`2026`) stay on their side.
|
||
///
|
||
/// Manual (Character panel menu › Tate-chu-yoko): any characters marked `tateChuYoko`, including
|
||
/// one character, letters and numbers longer than three digits. Marked characters are taken
|
||
/// before the automatic digit rule. A marked block moves by the Tate-chu-yoko Settings Up/Down
|
||
/// and Left/Right of its first character; an automatic block does not move.
|
||
/// <https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html>
|
||
///
|
||
/// With `whole` (System Layout on or Fractional Widths off), each side of a block's Insert Aki is
|
||
/// set in whole points ([`whole_character_widths`]).
|
||
pub(super) fn tate_chu_yoko(g: &mut [SGlyph], whole: bool, style: impl Fn(&SGlyph) -> TcyStyle) {
|
||
let manual: Vec<bool> = g.iter().map(|gl| gl.ch != '\u{2028}' && style(gl).marked).collect();
|
||
let digits: Vec<bool> = g.iter().map(|gl| gl.inline.is_none() && gl.ch.is_ascii_digit()).collect();
|
||
let mut i = 0;
|
||
while i < g.len() {
|
||
if manual.get(i).copied().unwrap_or(false) {
|
||
let mut end = i + 1;
|
||
while manual.get(end).copied().unwrap_or(false) {
|
||
end += 1;
|
||
}
|
||
let first = style(&g[i]);
|
||
place_tcy_block(&mut g[i..end], first.size, (first.up, first.right), whole);
|
||
i = end;
|
||
continue;
|
||
}
|
||
if !digits.get(i).copied().unwrap_or(false) {
|
||
i += 1;
|
||
continue;
|
||
}
|
||
let mut end = i + 1;
|
||
while digits.get(end).copied().unwrap_or(false) && !manual.get(end).copied().unwrap_or(false) {
|
||
end += 1;
|
||
}
|
||
if (2..=3).contains(&(end - i)) {
|
||
let em = style(&g[i]).size;
|
||
place_tcy_block(&mut g[i..end], em, (0.0, 0.0), whole);
|
||
}
|
||
i = end;
|
||
}
|
||
}
|
||
|
||
/// What [`tate_chu_yoko`] reads from a glyph's character style: its size, whether it is marked
|
||
/// Tate-chu-yoko, and its Tate-chu-yoko Settings Up/Down and Left/Right in points.
|
||
pub(super) struct TcyStyle {
|
||
pub size: f64,
|
||
pub marked: bool,
|
||
pub up: f64,
|
||
pub right: f64,
|
||
}
|
||
|
||
/// Lay `g` across one em of the column, moved by `(up, right)` points. The first glyph takes the
|
||
/// em; the rest share its cell. The Insert Aki of the block's first character is space above and
|
||
/// below the block, and the Insert Aki of its other characters is left out: the block is set
|
||
/// down the column that much further, and its last glyph carries the space in its advance. With
|
||
/// `whole`, the space above and the space below are each rounded to whole points.
|
||
fn place_tcy_block(g: &mut [SGlyph], em: f64, (up, right): (f64, f64), whole: bool) {
|
||
let Some(first) = g.first() else { return };
|
||
let (byte, before) = (first.byte, first.aki_shift);
|
||
let aki: f64 = g.iter().filter(|gl| gl.byte == byte).map(|gl| gl.aki).sum();
|
||
let points = |v: f64| {
|
||
if !whole {
|
||
v
|
||
} else if v.is_finite() {
|
||
v.round()
|
||
} else {
|
||
0.0
|
||
}
|
||
};
|
||
let (before, aki) = (points(before), points(before) + points(aki - before));
|
||
let width: f64 = g.iter().map(|gl| gl.adv - gl.aki).sum();
|
||
let squeeze = if width > em && width > 0.0 { em / width } else { 1.0 };
|
||
let last = g.len() - 1;
|
||
let mut ink = 0.0;
|
||
for (k, gl) in g.iter_mut().enumerate() {
|
||
let adv = gl.adv - gl.aki;
|
||
gl.tcy = Some(Tcy { pen: if k == 0 { 0.0 } else { em }, ink, width, squeeze, up: up - before, right });
|
||
gl.kern_shift -= gl.aki_shift;
|
||
gl.aki_shift = 0.0;
|
||
gl.aki = if k == last { aki } else { 0.0 };
|
||
gl.adv = if k == 0 { em } else { 0.0 } + gl.aki;
|
||
ink += adv;
|
||
}
|
||
}
|
||
|
||
/// Half an em of `g`'s size when it is full-width Japanese punctuation of kind `kind` (its
|
||
/// advance an em, give or take a tenth), the space that mojikumi can take off.
|
||
pub(super) fn punct_half(g: &SGlyph, kind: Punct) -> Option<f64> {
|
||
// A glyph Proportional Metrics re-spaced has no empty half left to take off (#966).
|
||
if punct(g.ch)? != kind || g.tcy.is_some() || g.proportional {
|
||
return None;
|
||
}
|
||
let em = g.face.units_per_em();
|
||
((g.face.advance(g.gid) - em).abs() <= em * 0.1).then_some(em * 0.5 * g.sx)
|
||
}
|
||
|
||
/// A quarter em of `g`'s size when it is a full-width Japanese middle dot (・, its advance an em,
|
||
/// give or take a tenth): the space on each side of the dot that a Mojikumi set can take off.
|
||
pub(super) fn dot_quarter(g: &SGlyph) -> Option<f64> {
|
||
if g.ch != '・' || g.tcy.is_some() || g.proportional {
|
||
return None;
|
||
}
|
||
let em = g.face.units_per_em();
|
||
((g.face.advance(g.gid) - em).abs() <= em * 0.1).then_some(em * 0.25 * g.sx)
|
||
}
|
||
|
||
/// What a paragraph's Mojikumi set does to its characters. The five predefined sets other than
|
||
/// Line-end Punctuation Half Width follow VectorCraft's own spacing rules, built on the help
|
||
/// page's one-line description of each set
|
||
/// (<https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/use-mojikumi-for-japanese-text-composition.html>).
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||
pub(super) struct MojikumiRules {
|
||
/// Consecutive punctuation shares one half-em space ([`compress_punctuation`]).
|
||
pub share: bool,
|
||
/// Every opening and closing bracket, comma and full stop is set half width, and a middle dot
|
||
/// loses the quarter em on each side of it ([`halve_punctuation`]).
|
||
pub half: bool,
|
||
/// An opening bracket starting a line is set flush with the line's start.
|
||
pub flush_start: bool,
|
||
/// A closing bracket, comma or full stop ending a line is set half width.
|
||
pub half_end: bool,
|
||
/// A middle dot ending a line loses the quarter em after it.
|
||
pub dot_end: bool,
|
||
/// A quarter em between Japanese and Latin letters or digits ([`space_japanese_and_latin`]),
|
||
/// taken off again at a line's end.
|
||
pub wakan: bool,
|
||
}
|
||
|
||
impl MojikumiRules {
|
||
pub(super) fn of(set: Mojikumi) -> Self {
|
||
let none = Self::default();
|
||
match set {
|
||
Mojikumi::None | Mojikumi::YakumonoZenkaku => none,
|
||
Mojikumi::LineEndHalf => Self { share: true, flush_start: true, half_end: true, wakan: true, ..none },
|
||
Mojikumi::YakumonoHankaku => Self { half: true, wakan: true, ..none },
|
||
Mojikumi::GyomatsuYakumonoHankaku => Self { flush_start: true, half_end: true, dot_end: true, wakan: true, ..none },
|
||
Mojikumi::GyomatsuYakumonoZenkaku => Self { wakan: true, ..none },
|
||
}
|
||
}
|
||
|
||
/// The rules a line's end follows: a right-to-left line keeps its punctuation's width there
|
||
/// and only loses the space between Japanese and Latin text.
|
||
pub(super) fn at_line_end(self, rtl: bool) -> Self {
|
||
if rtl { Self { half_end: false, dot_end: false, ..self } } else { self }
|
||
}
|
||
|
||
/// Whether a line ending at some character can be set narrower than its characters' advances.
|
||
pub(super) fn trims_line_end(&self) -> bool {
|
||
self.half_end || self.dot_end || self.wakan
|
||
}
|
||
}
|
||
|
||
/// YakumonoHankaku: every full-width opening bracket loses the half-em space before it, every
|
||
/// closing bracket, comma and full stop the half-em space after it, and every middle dot a quarter
|
||
/// em on each side. Returns which glyphs lost the space after them (a line ending in one takes
|
||
/// nothing more off).
|
||
pub(super) fn halve_punctuation(sg: &mut [SGlyph]) -> Vec<bool> {
|
||
let mut lost_after = vec![false; sg.len()];
|
||
for (g, lost) in sg.iter_mut().zip(lost_after.iter_mut()) {
|
||
if let Some(h) = punct_half(g, Punct::Closing) {
|
||
g.adv -= h;
|
||
*lost = true;
|
||
} else if let Some(h) = punct_half(g, Punct::Opening) {
|
||
g.adv -= h;
|
||
g.lead += h;
|
||
} else if let Some(q) = dot_quarter(g) {
|
||
g.adv -= 2.0 * q;
|
||
g.lead += q;
|
||
*lost = true;
|
||
}
|
||
}
|
||
lost_after
|
||
}
|
||
|
||
/// Mojikumi (JLREQ 3.1.4): consecutive punctuation shares one half-em space. A closing bracket,
|
||
/// comma or full stop followed by punctuation loses the space after it; an opening bracket after
|
||
/// another loses the space before it. Returns which glyphs lost the space after them (a line
|
||
/// ending in one takes nothing more off).
|
||
pub(super) fn compress_punctuation(sg: &mut [SGlyph]) -> Vec<bool> {
|
||
let mut lost_after = vec![false; sg.len()];
|
||
for j in 1..sg.len() {
|
||
let (before, after) = sg.split_at_mut(j);
|
||
let (Some(a), Some(b)) = (before.last_mut(), after.first_mut()) else { continue };
|
||
if punct(b.ch).is_none() {
|
||
continue;
|
||
}
|
||
if let Some(h) = punct_half(a, Punct::Closing) {
|
||
a.adv -= h;
|
||
if let Some(l) = lost_after.get_mut(j - 1) {
|
||
*l = true;
|
||
}
|
||
} else if punct(a.ch) == Some(Punct::Opening)
|
||
&& let Some(h) = punct_half(b, Punct::Opening)
|
||
{
|
||
b.adv -= h;
|
||
b.lead += h;
|
||
}
|
||
}
|
||
lost_after
|
||
}
|
||
|
||
/// The space between Japanese and Latin letters or digits (JLREQ 3.2.2: a quarter em of the
|
||
/// Japanese characters' size), with the Mojikumi sets that add it ([`MojikumiRules::wakan`]).
|
||
const WAKAN_AKI: f64 = 0.25;
|
||
|
||
/// A Japanese character for the space next to Latin text: kana and kanji (and full-width
|
||
/// letters, and the iteration and abbreviation marks 々 〆 〇), not punctuation or symbols, nor
|
||
/// Hangul (Korean sets a word space instead).
|
||
fn is_japanese_letter(c: char) -> bool {
|
||
if matches!(c, '々' | '〆' | '〇') {
|
||
return true;
|
||
}
|
||
is_cjk(c)
|
||
&& !matches!(
|
||
c as u32,
|
||
0x3000..=0x303F | 0x3130..=0x318F | 0xAC00..=0xD7AF | 0xFF01..=0xFF0F | 0xFF1A..=0xFF20 | 0xFF3B..=0xFF40 | 0xFF5B..=0xFF65 | 0xFFA0..=0xFFDC
|
||
)
|
||
&& c != '・'
|
||
&& punct(c).is_none()
|
||
}
|
||
|
||
impl TextLayout {
|
||
/// Whether a Mojikumi set can set type laid out as this layout differently from Mojikumi
|
||
/// None: `text`, the type's plain text, or one of the layout's list markers holds Japanese
|
||
/// punctuation, a middle dot or a Japanese letter, the only characters the sets space. When it
|
||
/// holds none of them, the type lays out the same with any set.
|
||
pub fn mojikumi_applies(&self, text: &str) -> bool {
|
||
let spaced = |c: char| punct(c).is_some() || c == '・' || is_japanese_letter(c);
|
||
text.chars().any(spaced) || self.markers.iter().any(|m| m.text.chars().any(spaced))
|
||
}
|
||
}
|
||
|
||
/// A Latin letter or digit (or another script's letter), set proportionally.
|
||
fn is_latin_letter(c: char) -> bool {
|
||
c.is_alphanumeric() && !is_cjk(c)
|
||
}
|
||
|
||
/// Mojikumi (JLREQ 3.2.2): a quarter em between a Japanese character and a Latin letter or digit,
|
||
/// either way round, added after the first of the two. Returns what each glyph got after it (taken
|
||
/// off again when the line ends there).
|
||
pub(super) fn space_japanese_and_latin(sg: &mut [SGlyph]) -> Vec<f64> {
|
||
let mut added = vec![0.0; sg.len()];
|
||
for j in 1..sg.len() {
|
||
let (before, after) = sg.split_at_mut(j);
|
||
let (Some(a), Some(b)) = (before.last_mut(), after.first()) else { continue };
|
||
if a.tcy.is_some() || b.tcy.is_some() {
|
||
continue;
|
||
}
|
||
let japanese = if is_japanese_letter(a.ch) && is_latin_letter(b.ch) {
|
||
Some(&*a)
|
||
} else if is_latin_letter(a.ch) && is_japanese_letter(b.ch) {
|
||
Some(b)
|
||
} else {
|
||
None
|
||
};
|
||
if let Some(g) = japanese {
|
||
let aki = WAKAN_AKI * g.face.units_per_em() * g.sx;
|
||
a.adv += aki;
|
||
if let Some(x) = added.get_mut(j - 1) {
|
||
*x = aki;
|
||
}
|
||
}
|
||
}
|
||
added
|
||
}
|
||
|
||
/// The size of `g`'s em, in points.
|
||
pub(super) fn glyph_em(g: &SGlyph) -> f64 {
|
||
g.face.units_per_em() * g.sy
|
||
}
|
||
|
||
/// How far up (line space) Character Alignment `a` moves glyph `g` on a line whose largest em is
|
||
/// `line_em`: the glyph's em box top, centre or bottom onto the line's (the em box running from
|
||
/// its centre less half an em to its centre plus half an em above the baseline), or its ICF's top
|
||
/// or bottom (the em box's less the face's [`crate::IcfMargins`]; across a vertical line, its
|
||
/// right and left). Nothing on the Roman baseline, or for the line's largest characters.
|
||
///
|
||
/// An inline graphic has its run's em (whatever its scale): it counts as one of the line's
|
||
/// largest characters only when its run's size is, and moves with the text of its run (its art
|
||
/// stays centred on that text's cap height).
|
||
pub(super) fn align_shift(g: &SGlyph, a: vectorcraft_doc::CharAlign, line_em: f64, vertical: bool) -> f64 {
|
||
use vectorcraft_doc::CharAlign;
|
||
let icf = || g.face.icf_margins();
|
||
let k = match a {
|
||
CharAlign::RomanBaseline => return 0.0,
|
||
CharAlign::EmBoxTop => 0.5,
|
||
CharAlign::EmBoxCenter => 0.0,
|
||
CharAlign::EmBoxBottom => -0.5,
|
||
CharAlign::IcfTop => 0.5 - if vertical { icf().right } else { icf().top },
|
||
CharAlign::IcfBottom => -0.5 + if vertical { icf().left } else { icf().bottom },
|
||
};
|
||
(g.face.ideographic_centre() + k) * (line_em - glyph_em(g)).max(0.0)
|
||
}
|
||
|
||
/// The length an upright glyph takes down the column before tracking and justification: its
|
||
/// vertical advance (the font's vertical metrics), else its advance, at least one em.
|
||
pub(super) fn upright_cell(g: &SGlyph) -> f64 {
|
||
match g.face.vertical_glyph(g.gid) {
|
||
Some((advance, _)) => advance * g.sy,
|
||
None => (g.face.advance(g.gid) * g.sx).max(g.face.units_per_em() * g.sx),
|
||
}
|
||
}
|
||
|
||
/// An upright glyph's cell in font units, as [`upright_cell`] and [`upright_centre`] place it before
|
||
/// its size: (its length down the column, the height of its top above the baseline). From the
|
||
/// font's vertical metrics, else at least one em long, centred on the ideographic em box.
|
||
pub(super) fn upright_extent(g: &SGlyph) -> (f64, f64) {
|
||
match g.face.vertical_glyph(g.gid) {
|
||
Some((advance, origin)) => (advance, origin),
|
||
None => {
|
||
let em = g.face.units_per_em();
|
||
let cell = g.face.advance(g.gid).max(em);
|
||
(cell, EM_CENTER * em + cell * 0.5)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Height above the baseline (line space) of the centre of an upright glyph's `cell`, the point it
|
||
/// turns about: from the font's vertical metrics, the cell hanging from the glyph's vertical origin;
|
||
/// else the centre of the ideographic em box ([`EM_CENTER`] of the size `em`).
|
||
pub(super) fn upright_centre(g: &SGlyph, cell: f64, em: f64) -> f64 {
|
||
match g.face.vertical_glyph(g.gid) {
|
||
Some((_, origin)) => origin * g.sy - cell * 0.5,
|
||
None => EM_CENTER * em,
|
||
}
|
||
}
|
||
|
||
/// Height of the centre of the ideographic em box above the baseline, in ems (the em box runs
|
||
/// from 0.12 em below the baseline to 0.88 em above it), for fonts without vertical metrics.
|
||
pub(super) const EM_CENTER: f64 = 0.38;
|
||
|
||
/// A quarter turn clockwise (y down), exact: line space → text space for vertical type.
|
||
pub(super) const QUARTER_TURN: Affine = Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0.0]);
|
||
|
||
/// Does the glyph stand upright in vertical type? CJK characters and symbols do (their vertical
|
||
/// forms come from the font's `vert` feature); Latin letters, digits and other characters lie on
|
||
/// their side. Brackets, the long vowel mark and similar marks that need a vertical form lie on
|
||
/// their side when the font has none (Unicode's Vertical_Orientation Tr).
|
||
/// Fullwidth !/? instead keep the nominal glyph upright (Tu fallback, UAX #50 §3.1):
|
||
/// <https://www.unicode.org/reports/tr50/#vo>.
|
||
///
|
||
/// Standard Vertical Roman Alignment (Character panel menu): a marked Latin letter, digit or other
|
||
/// character outside the classes above stands upright instead of lying on its side, except spaces
|
||
/// and control characters (they keep their width down the column) and the glyphs of a
|
||
/// tate-chu-yoko block, which stays across the column.
|
||
/// <https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html>
|
||
pub(super) fn stands_upright(g: &SGlyph) -> bool {
|
||
let c = g.ch as u32;
|
||
let upright = matches!(c,
|
||
0x00A7 | 0x00A9 | 0x00AE | 0x00B1 | 0x00BC..=0x00BE | 0x00D7 | 0x00F7
|
||
| 0x2016 | 0x2020 | 0x2021 | 0x2030 | 0x2031 | 0x203B | 0x203C | 0x2042 | 0x2047..=0x2049 | 0x2051
|
||
| 0x20DD..=0x20E0 | 0x20E2..=0x20E4 | 0x2100..=0x2101 | 0x2103..=0x2109 | 0x210F | 0x2113 | 0x2116 | 0x2117
|
||
| 0x211E..=0x2123 | 0x2125 | 0x2127 | 0x2129 | 0x212E | 0x2135..=0x213F | 0x2145..=0x214A | 0x214C | 0x214D | 0x214F..=0x2189
|
||
| 0x2460..=0x24FF | 0x25A0..=0x27BF | 0x2B12..=0x2B2F | 0x2B50..=0x2B59
|
||
| 0x1100..=0x11FF | 0x2E80..=0x303F | 0x3040..=0xA4CF | 0xA960..=0xA97F | 0xAC00..=0xD7FF
|
||
| 0xE000..=0xFAFF | 0xFE10..=0xFE1F | 0xFE30..=0xFE4F | 0xFE50..=0xFE6F | 0xFF00..=0xFFE7
|
||
| 0x1F000..=0x1FAFF | 0x20000..=0x3FFFF);
|
||
if !upright {
|
||
return g.vertical_roman && g.tcy.is_none() && g.inline.is_none() && !g.ch.is_whitespace() && !g.ch.is_control();
|
||
}
|
||
let needs_vertical_form = matches!(c,
|
||
0x3001 | 0x3002 | 0x3008..=0x3011 | 0x3013..=0x301F | 0x3030 | 0x30A0 | 0x30FC
|
||
| 0xFE50..=0xFE52 | 0xFE59..=0xFE5E | 0xFF08 | 0xFF09 | 0xFF0C | 0xFF0E | 0xFF1A | 0xFF1B
|
||
| 0xFF3B | 0xFF3D | 0xFF3F | 0xFF5B..=0xFF60 | 0xFFE3);
|
||
// Without a vertical alternate the shaped glyph is the nominal one.
|
||
!needs_vertical_form || g.gid != g.face.glyph_for(g.ch)
|
||
}
|
||
|
||
pub(super) fn finish_bounds(out: &mut TextLayout) {
|
||
let xf = out.line_xf;
|
||
let mut b: Option<Rect> = out.ink_bounds();
|
||
let mut add = |r: Rect| b = Some(b.map_or(r, |b| b.union(r)));
|
||
if !out.on_path {
|
||
for l in &out.lines {
|
||
add(xf.transform_rect_bbox(Rect::new(l.x0.min(l.x1), l.baseline - l.ascent, l.x0.max(l.x1), l.baseline + l.descent)));
|
||
}
|
||
}
|
||
out.bounds = b.unwrap_or_default();
|
||
}
|
||
|
||
/// Space left in each cell after flowing top-aligned: (space above the first line's ascent, space
|
||
/// below the last line's descent, number of distinct baselines). Cells without lines are `None`.
|
||
fn cell_space(out: &TextLayout, regions: &[Region]) -> Vec<Option<(f64, f64, usize)>> {
|
||
regions
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(ri, r)| {
|
||
let mut top = f64::INFINITY;
|
||
let mut bottom = f64::NEG_INFINITY;
|
||
let mut baselines: Vec<f64> = vec![];
|
||
for l in out.lines.iter().filter(|l| l.region == ri) {
|
||
top = top.min(l.baseline - l.ascent);
|
||
bottom = bottom.max(l.baseline + l.descent);
|
||
if !baselines.iter().any(|b| (b - l.baseline).abs() < 1e-6) {
|
||
baselines.push(l.baseline);
|
||
}
|
||
}
|
||
if baselines.is_empty() || !top.is_finite() || !bottom.is_finite() {
|
||
return None;
|
||
}
|
||
// Never negative: a full (or overflowing) cell stays where top alignment put it.
|
||
Some(((top - r.top()).max(0.0), (r.bottom() - bottom).max(0.0), baselines.len()))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Area Type Options "Align" other than Top: move each cell's lines down. Rectangular cells
|
||
/// without text wrap shift their lines (centre: half the space left below the last line;
|
||
/// bottom: all of it; justify: the first line stays, each further line gets an equal share of it).
|
||
/// Other frames give lines different widths at different heights, so the text flows again with
|
||
/// each cell's lines started lower (or spaced wider) until it settles, at most eight passes; a
|
||
/// pass that would push text out of the frame is undone and retried with half the step.
|
||
pub(super) fn align_vertically(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, regions: &mut [Region]) {
|
||
let align = cx.opts.vertical_align;
|
||
let plain = regions.iter().all(|r| (r.rect || r.polys.is_empty()) && r.wraps.is_empty());
|
||
if plain {
|
||
let space = cell_space(&cx.out, regions);
|
||
// Each distinct baseline's index within its cell, for justify.
|
||
let mut seen: Vec<Vec<f64>> = vec![vec![]; regions.len()];
|
||
let offs: Vec<f64> = cx
|
||
.out
|
||
.lines
|
||
.iter()
|
||
.map(|l| {
|
||
let Some(Some((_, below, n))) = space.get(l.region).copied() else { return 0.0 };
|
||
match align {
|
||
VerticalAlign::Top => 0.0,
|
||
VerticalAlign::Center => below * 0.5,
|
||
VerticalAlign::Bottom => below,
|
||
VerticalAlign::Justify => {
|
||
let Some(s) = seen.get_mut(l.region) else { return 0.0 };
|
||
let k = s.iter().position(|b| (b - l.baseline).abs() < 1e-6).unwrap_or_else(|| {
|
||
s.push(l.baseline);
|
||
s.len() - 1
|
||
});
|
||
if n > 1 { below * k as f64 / (n - 1) as f64 } else { 0.0 }
|
||
}
|
||
}
|
||
})
|
||
.collect();
|
||
for (l, d) in cx.out.lines.iter_mut().zip(&offs) {
|
||
l.baseline += d;
|
||
}
|
||
for g in &mut cx.out.glyphs {
|
||
let d = offs.get(g.line).copied().unwrap_or(0.0);
|
||
if d != 0.0 {
|
||
let m = Affine::translate((0.0, d));
|
||
g.outline.apply_affine(m);
|
||
g.xf = m * g.xf;
|
||
g.origin.y += d;
|
||
}
|
||
}
|
||
// Inline art moves with its glyph's line.
|
||
for i in &mut cx.out.inlines {
|
||
let d = cx.out.glyphs.get(i.glyph).and_then(|g| offs.get(g.line)).copied().unwrap_or(0.0);
|
||
if d != 0.0 {
|
||
let m = Affine::translate((0.0, d));
|
||
i.xf = m * i.xf;
|
||
i.bounds = m.transform_rect_bbox(i.bounds);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
let laid_out = |out: &TextLayout| out.lines.last().map_or(0, |l| l.end);
|
||
// How much of the measured space a pass takes up: halved after a pass that lost text.
|
||
let mut step = 1.0;
|
||
for _ in 0..8 {
|
||
let space = cell_space(&cx.out, regions);
|
||
let mut moved = false;
|
||
let saved: Vec<(f64, f64)> = regions.iter().map(|r| (r.shift, r.gap)).collect();
|
||
for (r, s) in regions.iter_mut().zip(&space) {
|
||
let Some((above, below, n)) = *s else { continue };
|
||
let (shift, gap) = match align {
|
||
VerticalAlign::Top => (r.shift, r.gap),
|
||
VerticalAlign::Center => (r.shift + (below - above) * 0.5 * step, r.gap),
|
||
VerticalAlign::Bottom => (r.shift + below * step, r.gap),
|
||
VerticalAlign::Justify if n > 1 => (r.shift, r.gap + below * step / (n - 1) as f64),
|
||
VerticalAlign::Justify => (r.shift, r.gap),
|
||
};
|
||
let (shift, gap) = (shift.clamp(0.0, r.cell.height().max(0.0)), gap.clamp(0.0, r.cell.height().max(0.0)));
|
||
if (shift - r.shift).abs() > 0.25 || (gap - r.gap).abs() > 0.01 {
|
||
moved = true;
|
||
}
|
||
r.shift = shift;
|
||
r.gap = gap;
|
||
}
|
||
if !moved {
|
||
return;
|
||
}
|
||
let before = laid_out(&cx.out);
|
||
let prev = (
|
||
std::mem::take(&mut cx.out.glyphs),
|
||
std::mem::take(&mut cx.out.lines),
|
||
std::mem::take(&mut cx.out.inlines),
|
||
std::mem::take(&mut cx.out.markers),
|
||
std::mem::replace(&mut cx.out.overflow, false),
|
||
);
|
||
flow(cx, paras, t, Some(regions));
|
||
if laid_out(&cx.out) < before {
|
||
// Text no longer fits (lines got narrower, or flow around a wrap object): undo the
|
||
// pass and try a smaller step.
|
||
(cx.out.glyphs, cx.out.lines, cx.out.inlines, cx.out.markers, cx.out.overflow) = prev;
|
||
for (r, (shift, gap)) in regions.iter_mut().zip(saved) {
|
||
r.shift = shift;
|
||
r.gap = gap;
|
||
}
|
||
step *= 0.5;
|
||
if step < 0.1 {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[test]
|
||
fn japanese_letters_for_the_latin_space_are_kana_kanji_and_marks_not_hangul_or_punctuation() {
|
||
for c in ['あ', 'カ', '漢', '々', '〆', '〇', 'A'] {
|
||
assert!(is_japanese_letter(c), "{c}");
|
||
}
|
||
// Hangul in every form: syllables, compatibility jamo (ㄱ ㅏ), half-width jamo (ᄀ).
|
||
for c in ['한', 'ㄱ', 'ㅏ', '\u{FFA1}', '。', '「', '・', '、', 'a'] {
|
||
assert!(!is_japanese_letter(c), "{c}");
|
||
}
|
||
}
|