mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:10:01 -06:00
Type: Standard Vertical Roman Alignment stands half-width characters upright in vertical type
Character panel menu > Standard Vertical Roman Alignment, with Show East Asian Options, is now a check item. It sets a new character attribute, `standardVerticalRoman` (text.setFormat, text.setRangeStyle), off by default and saved only when on. In vertical type the marked Latin letters, digits and other characters that would lie on their side stand upright one below the other, each in its upright cell. Spaces keep their width and tate-chu-yoko blocks stay across the column. Horizontal type keeps the mark and does not change. A combining mark shaped as its own glyph turns with its letter, in Latin and CJK upright type alike. Character Style Options labels the Character Alignment row "Character Alignment" and gives Standard Vertical Roman Alignment its own checkbox. Reset Panel turns the attribute off. PDF and SVG export place the glyphs from the layout and need no change.
This commit is contained in:
+40
-12
@@ -41,6 +41,12 @@ use vertical::{stands_upright, tate_chu_yoko, upright_cell, upright_centre, whol
|
||||
|
||||
const EPS: f64 = 1e-6;
|
||||
|
||||
/// Is glyph `g` a later glyph of the cluster of glyph `prev` before it (a combining mark the shaper
|
||||
/// set as a glyph of its own)? A list marker's glyphs stand for no character and are not.
|
||||
fn continues_cluster(prev: Option<&SGlyph>, g: Option<&SGlyph>) -> bool {
|
||||
prev.zip(g).is_some_and(|(a, b)| a.len > 0 && b.len > 0 && a.byte == b.byte)
|
||||
}
|
||||
|
||||
struct Ctx<'a> {
|
||||
on_path: bool,
|
||||
db: &'a FontDb,
|
||||
@@ -58,6 +64,10 @@ struct Ctx<'a> {
|
||||
system_layout: bool,
|
||||
/// Character panel menu › Fractional Widths. Off, positive advances are whole points.
|
||||
fractional_widths: bool,
|
||||
/// Vertical type: the upright glyph [`Self::emit`] placed last, as (its byte, the index the
|
||||
/// next glyph of its cluster would take, the turn it got, the line-space x that next glyph is
|
||||
/// set after), so that a combining mark shaped into the same cluster turns with it.
|
||||
upright_cluster: Option<(usize, usize, Affine, f64)>,
|
||||
out: TextLayout,
|
||||
}
|
||||
|
||||
@@ -108,8 +118,11 @@ impl Ctx<'_> {
|
||||
});
|
||||
// An upright glyph advances down the column by its vertical advance (the font's vertical
|
||||
// metrics; without them at least one em, as CJK fonts' is), centred across it: a narrow
|
||||
// mark like § must not overlap its neighbours.
|
||||
for g in v.iter_mut().filter(|g| g.adv > 0.0 && stands_upright(g)) {
|
||||
// mark like § must not overlap its neighbours. A combining mark shaped into the cluster
|
||||
// of the glyph before it takes no cell of its own: it turns with that glyph (`emit`).
|
||||
for i in 0..v.len() {
|
||||
let continues = continues_cluster(i.checked_sub(1).and_then(|p| v.get(p)), v.get(i));
|
||||
let Some(g) = v.get_mut(i).filter(|g| g.adv > 0.0 && !continues && stands_upright(g)) else { continue };
|
||||
let extra = upright_cell(g) - g.face.advance(g.gid) * g.sx;
|
||||
g.dx += extra * 0.5;
|
||||
g.adv += extra;
|
||||
@@ -181,6 +194,8 @@ impl Ctx<'_> {
|
||||
}
|
||||
|
||||
fn emit(&mut self, g: &SGlyph, pre: Affine, origin: Point, angle: f64, advance: f64, line: usize) {
|
||||
// Only the glyph right after an upright glyph can continue its cluster.
|
||||
let cluster = self.upright_cluster.take();
|
||||
// Control characters (tabs) and soft hyphens draw nothing (fonts map them to .notdef).
|
||||
let blank = g.is_soft_hyphen() || g.ch.is_control() || g.ch == '\u{2028}';
|
||||
// A colour glyph draws its art (COLR layers, an SVG document) or its image (a bitmap, or
|
||||
@@ -235,16 +250,29 @@ impl Ctx<'_> {
|
||||
let squeeze = Affine::translate((centre.x, 0.0)) * Affine::scale_non_uniform(t.squeeze, 1.0) * Affine::translate((-centre.x, 0.0));
|
||||
m = Affine::rotate_about(-std::f64::consts::FRAC_PI_2, centre) * squeeze * across * m;
|
||||
} else if self.vertical && stands_upright(g) {
|
||||
// Turned about the centre of its cell, which the column's centre line runs through: the
|
||||
// middle of its own cell, not of its advance (tracking and justification add space
|
||||
// after the cell, and must not push the glyph off the centre line).
|
||||
let em = self.style_at(g.byte).size;
|
||||
let cell = if g.adv > 0.0 { upright_cell(g) } else { advance };
|
||||
// A leading space taken off moves it up the column after the turn (moving the
|
||||
// turning point instead would move it across the column too).
|
||||
m = Affine::translate((-g.lead, 0.0))
|
||||
* Affine::rotate_about(-std::f64::consts::FRAC_PI_2, Point::new(origin.x + cell * 0.5, origin.y - upright_centre(g, cell, em)))
|
||||
* m;
|
||||
let next = self.out.glyphs.len();
|
||||
let after_base = self.out.glyphs.last().is_some_and(|p| p.byte == g.byte && p.line == line && p.len > 0);
|
||||
if let Some((_, _, turn, after)) = cluster.filter(|c| c.0 == g.byte && c.1 == next && after_base && g.len > 0) {
|
||||
// A later glyph of an upright glyph's cluster: set after it as the shaper placed
|
||||
// it, and turned with it.
|
||||
m = turn * Affine::translate((after - origin.x, 0.0)) * m;
|
||||
self.upright_cluster = Some((g.byte, next + 1, turn, after));
|
||||
} else {
|
||||
// Turned about the centre of its cell, which the column's centre line runs through:
|
||||
// the middle of its own cell, not of its advance (tracking and justification add
|
||||
// space after the cell, and must not push the glyph off the centre line).
|
||||
let em = self.style_at(g.byte).size;
|
||||
let cell = if g.adv > 0.0 { upright_cell(g) } else { advance };
|
||||
// A leading space taken off moves it up the column after the turn (moving the
|
||||
// turning point instead would move it across the column too).
|
||||
let turn = Affine::translate((-g.lead, 0.0))
|
||||
* Affine::rotate_about(-std::f64::consts::FRAC_PI_2, Point::new(origin.x + cell * 0.5, origin.y - upright_centre(g, cell, em)));
|
||||
m = turn * m;
|
||||
// Its own advance across, from where the cell centres it: where the shaper set the
|
||||
// next glyph of its cluster.
|
||||
let across = g.face.advance(g.gid) * g.sx;
|
||||
self.upright_cluster = (g.len > 0).then_some((g.byte, next + 1, turn, origin.x + (cell - across) * 0.5 + across));
|
||||
}
|
||||
}
|
||||
let outline = if src.elements().is_empty() || blank {
|
||||
BezPath::new()
|
||||
|
||||
@@ -228,6 +228,7 @@ fn layout_once(db: &FontDb, t: &TextObject, opts: &LayoutOptions) -> TextLayout
|
||||
vertical,
|
||||
system_layout: t.system_layout,
|
||||
fractional_widths: t.fractional_widths,
|
||||
upright_cluster: None,
|
||||
out: TextLayout::default(),
|
||||
};
|
||||
// Vertical type: line space turned a quarter turn clockwise (lines become columns, each next
|
||||
|
||||
@@ -218,6 +218,12 @@ pub(super) const QUARTER_TURN: Affine = Affine::new([0.0, 1.0, -1.0, 0.0, 0.0, 0
|
||||
/// 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,
|
||||
@@ -230,7 +236,7 @@ pub(super) fn stands_upright(g: &SGlyph) -> bool {
|
||||
| 0xE000..=0xFAFF | 0xFE10..=0xFE1F | 0xFE30..=0xFE4F | 0xFE50..=0xFE6F | 0xFF00..=0xFFE7
|
||||
| 0x1F000..=0x1FAFF | 0x20000..=0x3FFFF);
|
||||
if !upright {
|
||||
return false;
|
||||
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
|
||||
|
||||
@@ -36,6 +36,7 @@ fn synthetic_glyphs(specs: &[(usize, usize, f64, char)]) -> Vec<SGlyph> {
|
||||
vpal: None,
|
||||
level: unicode_bidi::Level::ltr(),
|
||||
no_break: false,
|
||||
vertical_roman: false,
|
||||
cursive: false,
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -69,6 +69,9 @@ pub(crate) struct SGlyph {
|
||||
/// No Break (Character panel menu): keep this glyph on the same line as the neighbouring
|
||||
/// glyphs that have it.
|
||||
pub no_break: bool,
|
||||
/// Standard Vertical Roman Alignment (Character panel menu): in vertical type this glyph
|
||||
/// stands upright when it would otherwise lie on its side ([`crate::layout`]).
|
||||
pub vertical_roman: bool,
|
||||
/// The glyph is of a script whose letters join (Arabic, Syriac, N'Ko, Mongolian, Adlam…),
|
||||
/// decided for the whole segment it was shaped in ([`joins`]): on a curved path it is bent
|
||||
/// along the path so its joins meet.
|
||||
@@ -713,6 +716,7 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
char_direction: _,
|
||||
proportional_metrics,
|
||||
tate_chu_yoko,
|
||||
standard_vertical_roman: _,
|
||||
digits,
|
||||
no_break,
|
||||
language,
|
||||
@@ -828,6 +832,7 @@ fn inline_glyph(face: &Arc<FontFace>, st: &CharStyle, art: &InlineArt, run: usiz
|
||||
inline: Some(InlineBox { art: drawn, xf }),
|
||||
word_end: false,
|
||||
no_break: st.no_break,
|
||||
vertical_roman: false,
|
||||
cursive: false,
|
||||
}
|
||||
}
|
||||
@@ -1137,6 +1142,7 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
inline: None,
|
||||
word_end: false,
|
||||
no_break: gst.no_break,
|
||||
vertical_roman: gst.standard_vertical_roman,
|
||||
cursive,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -379,6 +379,60 @@ fn manual_tate_chu_yoko_leaves_unmarked_digits_and_stays_on_one_line() {
|
||||
assert!(wrapped.lines.len() > 1, "unmarked 2026 is longer than the column");
|
||||
}
|
||||
|
||||
/// Character panel menu › Standard Vertical Roman Alignment: marked Latin letters and digits stand
|
||||
/// upright one below the other, each in a cell of one em (the bundled font has no vertical
|
||||
/// metrics), centred on the column. A space keeps its width, two digits stay tate-chu-yoko,
|
||||
/// unmarked letters lie on their side, and horizontal type is unchanged.
|
||||
#[test]
|
||||
fn standard_vertical_roman_alignment_stands_half_width_characters_upright() {
|
||||
let plain = CharStyle { size: 20.0, ..CharStyle::default() };
|
||||
let marked = CharStyle { standard_vertical_roman: true, ..plain.clone() };
|
||||
let h = layout(FontDb::global(), &TextObject::point(Point::ZERO, "l", plain.clone()));
|
||||
let side = layout(FontDb::global(), &vertical_styled("lA l", plain.clone()));
|
||||
let up = layout(FontDb::global(), &vertical_styled("lA l", marked.clone()));
|
||||
let (hl, sl, ul) = (ink(&h, 0), ink(&side, 0), ink(&up, 0));
|
||||
assert!(sl.width() > sl.height(), "unmarked l lies across the column: {sl:?}");
|
||||
assert!((ul.width() - hl.width()).abs() < 0.01 && (ul.height() - hl.height()).abs() < 0.01, "marked l upright: {hl:?} {ul:?}");
|
||||
assert!(ul.height() > ul.width());
|
||||
for i in [0, 1, 3] {
|
||||
assert!((up.glyphs[i].advance - 20.0).abs() < 1e-6, "one em down the column: {:?}", up.glyphs[i].advance);
|
||||
assert!(ink(&up, i).center().x.abs() < 1.5, "centred on the column: {:?}", ink(&up, i));
|
||||
assert!(side.glyphs[i].advance < 20.0, "unmarked advance: {:?}", side.glyphs[i].advance);
|
||||
}
|
||||
assert!(ink(&up, 1).center().y > ink(&up, 0).center().y + 15.0, "one below the other");
|
||||
assert!((up.glyphs[2].advance - side.glyphs[2].advance).abs() < 1e-6, "the space keeps its width");
|
||||
|
||||
let digits = layout(FontDb::global(), &vertical_styled("10", marked.clone()));
|
||||
assert!((digits.glyphs[0].advance - 20.0).abs() < 1e-6 && digits.glyphs[1].advance == 0.0, "two digits stay tate-chu-yoko");
|
||||
let (a, b) = (ink(&digits, 0), ink(&digits, 1));
|
||||
assert!((a.center().y - b.center().y).abs() < 1.0 && b.center().x > a.center().x, "side by side: {a:?} {b:?}");
|
||||
|
||||
let mut horizontal = TextObject::point(Point::ZERO, "lA", marked);
|
||||
horizontal.xf = Affine::IDENTITY;
|
||||
let row = layout(FontDb::global(), &horizontal);
|
||||
let plain_row = layout(FontDb::global(), &TextObject::point(Point::ZERO, "lA", plain));
|
||||
assert_eq!(row.glyphs.iter().map(|g| g.advance).collect::<Vec<_>>(), plain_row.glyphs.iter().map(|g| g.advance).collect::<Vec<_>>());
|
||||
assert!((ink(&row, 0).height() - hl.height()).abs() < 0.01, "horizontal type stays as it is");
|
||||
}
|
||||
|
||||
/// A combining mark shaped as a glyph of its own turns upright with its letter and stays over it,
|
||||
/// as in horizontal type, and takes no cell of its own down the column.
|
||||
#[test]
|
||||
fn standard_vertical_roman_alignment_keeps_combining_marks_on_their_letter() {
|
||||
let plain = CharStyle { size: 20.0, ..CharStyle::default() };
|
||||
let text = "q\u{301}x";
|
||||
let h = layout(FontDb::global(), &TextObject::point(Point::ZERO, text, plain.clone()));
|
||||
assert_eq!(h.glyphs.len(), 3, "the bundled font sets the acute as a glyph of its own");
|
||||
let up = layout(FontDb::global(), &vertical_styled(text, CharStyle { standard_vertical_roman: true, ..plain }));
|
||||
assert_eq!(up.glyphs.len(), 3);
|
||||
let (hq, ha, q, a, x) = (ink(&h, 0), ink(&h, 1), ink(&up, 0), ink(&up, 1), ink(&up, 2));
|
||||
let offset = |base: Rect, mark: Rect| (mark.center().x - base.center().x, mark.center().y - base.center().y);
|
||||
let (want, got) = (offset(hq, ha), offset(q, a));
|
||||
assert!((want.0 - got.0).abs() < 0.01 && (want.1 - got.1).abs() < 0.01, "the acute sits on the q as in a row: {want:?} {got:?}");
|
||||
assert!((up.glyphs[0].advance - 20.0).abs() < 1e-6 && up.glyphs[1].advance == 0.0, "one cell for the cluster");
|
||||
assert!(x.center().y > q.center().y + 10.0, "the next letter below: {q:?} {x:?}");
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
|
||||
Reference in New Issue
Block a user