Type: Proportional Metrics bring their kerning — `apkn` with `palt`, `vkrn` and `vapk` with `vpal` (#966)
The follow-up #966 left room for. With automatic kerning, horizontal type with Proportional Metrics also applies `apkn`, and vertical type applies `vkrn` and `vapk` along with `vpal`. Manual kerning turns them off, as it turns off `kern`. Vertical kerning used to cancel out: both top-to-bottom shapings had harfrust's default `vkrn`. The baseline now has neither `vpal` nor the kerning, so their difference carries both. A third shaping (`vpal` alone) tells which advances `vpal` itself changed, and mojikumi steps aside only for those. Vertical type without Proportional Metrics isn't kerned, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
68c3cd56f7
commit
876f8e283c
|
|
@ -162,6 +162,9 @@ impl OtFeatures {
|
|||
}
|
||||
if self.proportional(st) {
|
||||
v.push(f(b"palt", true));
|
||||
// Kerning for the glyphs `palt` makes proportional (OpenType `apkn`), with automatic
|
||||
// kerning only.
|
||||
v.push(f(b"apkn", st.kerning.is_none()));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
|
@ -180,20 +183,24 @@ impl OtFeatures {
|
|||
}
|
||||
|
||||
/// The harfrust features for vertical text in style `st` shaped top to bottom: [`Self::resolve`],
|
||||
/// with `vpal` when `proportional`.
|
||||
pub(crate) fn resolve_vertical(&self, st: &CharStyle, proportional: bool) -> Vec<Feature> {
|
||||
/// with `vpal` when `proportional`, and vertical kerning (`vkrn`, and `vapk` for the glyphs
|
||||
/// `vpal` makes proportional) when `kerning` and the style's kerning is automatic.
|
||||
pub(crate) fn resolve_vertical(&self, st: &CharStyle, proportional: bool, kerning: bool) -> Vec<Feature> {
|
||||
let mut v = self.resolve(st);
|
||||
if proportional {
|
||||
v.push(f(b"vpal", true));
|
||||
}
|
||||
let kern = kerning && st.kerning.is_none();
|
||||
v.push(f(b"vkrn", kern));
|
||||
v.push(f(b"vapk", kern));
|
||||
v
|
||||
}
|
||||
|
||||
/// The harfrust features for text in style `st` with full-width glyphs on their full widths:
|
||||
/// [`Self::resolve`] without `palt`.
|
||||
/// [`Self::resolve`] without `palt` (and its kerning, `apkn`).
|
||||
pub(crate) fn resolve_fixed_width(&self, st: &CharStyle) -> Vec<Feature> {
|
||||
let mut v = self.resolve(st);
|
||||
v.retain(|x| x.tag != Tag::new(b"palt"));
|
||||
v.retain(|x| x.tag != Tag::new(b"palt") && x.tag != Tag::new(b"apkn"));
|
||||
v
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,13 +56,14 @@ impl Ctx<'_> {
|
|||
let extra = upright_cell(g) - g.face.advance(g.gid) * g.sx;
|
||||
g.dx += extra * 0.5;
|
||||
g.adv += extra;
|
||||
// Proportional Metrics: `vpal` moves it and changes its length down the column,
|
||||
// in fonts with vertical metrics (else the cell stays as it is, #966).
|
||||
// Proportional Metrics: `vpal` (and the vertical kerning with it) moves it and
|
||||
// changes its length down the column, in fonts with vertical metrics (else the cell
|
||||
// stays as it is, #966).
|
||||
if let Some(a) = g.vpal.filter(|_| g.tcy.is_none() && g.face.vertical_glyph(g.gid).is_some()) {
|
||||
g.dx += a.dx;
|
||||
g.dy += a.dy;
|
||||
g.adv += a.advance;
|
||||
g.proportional = a.advance != 0.0;
|
||||
g.proportional = a.proportional;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,14 +64,16 @@ pub(crate) struct SGlyph {
|
|||
pub word_end: bool,
|
||||
}
|
||||
|
||||
/// How `vpal` moves an upright glyph in vertical type and changes its length down the column, in
|
||||
/// points: `advance` down the column, `dx` and `dy` in glyph space (y down; `dy` is down the column
|
||||
/// once the glyph stands upright).
|
||||
/// How `vpal` and the vertical kerning with it move an upright glyph in vertical type and change
|
||||
/// its length down the column, in points: `advance` down the column, `dx` and `dy` in glyph space
|
||||
/// (y down; `dy` is down the column once the glyph stands upright). `proportional`: `vpal` itself
|
||||
/// changed the advance.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub(crate) struct VAdjust {
|
||||
pub advance: f64,
|
||||
pub dx: f64,
|
||||
pub dy: f64,
|
||||
pub proportional: bool,
|
||||
}
|
||||
|
||||
/// Placement of an inline graphic's art, glyph space being points with the pen at the origin on
|
||||
|
|
@ -584,17 +586,24 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
|||
}
|
||||
if feats.proportional_vertical(st) {
|
||||
// `vpal` adjusts heights, which only top-to-bottom shaping applies: the same glyphs set
|
||||
// top to bottom with and without it tell how it moves each and changes its advance down
|
||||
// the column (y up, advances negative).
|
||||
let (with, without) =
|
||||
(shape(&feats.resolve_vertical(st, true), Direction::TopToBottom), shape(&feats.resolve_vertical(st, false), Direction::TopToBottom));
|
||||
if same_glyphs(&with) && same_glyphs(&without) {
|
||||
// top to bottom with it (and the vertical kerning that goes with it, `vkrn` and `vapk`)
|
||||
// and without either tell how it moves each and changes its advance down the column
|
||||
// (y up, advances negative). Which advances `vpal` itself changed comes from setting
|
||||
// them with it alone.
|
||||
let ttb = |proportional, kerning| shape(&feats.resolve_vertical(st, proportional, kerning), Direction::TopToBottom);
|
||||
let (with, without, alone) = (ttb(true, true), ttb(false, false), ttb(true, false));
|
||||
if same_glyphs(&with) && same_glyphs(&without) && same_glyphs(&alone) {
|
||||
vpal = with
|
||||
.iter()
|
||||
.zip(&without)
|
||||
.map(|(a, b)| {
|
||||
let adj =
|
||||
VAdjust { advance: f64::from(b.3 - a.3) * k * vs, dx: f64::from(a.4 - b.4) * k * hs, dy: -f64::from(a.5 - b.5) * k * vs };
|
||||
.zip(&alone)
|
||||
.map(|((a, b), c)| {
|
||||
let adj = VAdjust {
|
||||
advance: f64::from(b.3 - a.3) * k * vs,
|
||||
dx: f64::from(a.4 - b.4) * k * hs,
|
||||
dy: -f64::from(a.5 - b.5) * k * vs,
|
||||
proportional: c.3 != b.3,
|
||||
};
|
||||
(adj != VAdjust::default()).then_some(adj)
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
|
|
@ -582,3 +582,37 @@ fn proportional_metrics_set_upright_glyphs_on_their_proportional_heights() {
|
|||
let (half, none) = (lay("〘あ〙", "Shippori Mincho", true, Mojikumi::LineEndHalf), lay("〘あ〙", "Shippori Mincho", true, Mojikumi::None));
|
||||
assert!((none.glyphs[0].advance - 20.0).abs() < 0.01 && (half.glyphs[0].advance - 10.0).abs() < 0.01, "{:?}", adv(&half));
|
||||
}
|
||||
|
||||
/// Proportional Metrics turns on the kerning that goes with it (#966): `vkrn` and `vapk` in
|
||||
/// vertical type with `vpal`, `apkn` with `palt`, only while the style's kerning is automatic.
|
||||
/// Without Proportional Metrics vertical type isn't kerned, as before. Needs craft-fonts' Noto Sans
|
||||
/// CJK SC, which has `vpal` and `vkrn` (skipped without it; it isn't a document font, so it's loaded
|
||||
/// here).
|
||||
#[test]
|
||||
fn proportional_metrics_kern_the_glyphs_they_respace() {
|
||||
let tags = |v: Vec<harfrust::Feature>| v.iter().map(|f| (f.tag.to_be_bytes(), f.value)).collect::<Vec<_>>();
|
||||
let st = |pm: bool, kerning: Option<f64>| CharStyle { size: 20.0, proportional_metrics: pm, kerning, ..CharStyle::default() };
|
||||
let h = crate::OtFeatures::default();
|
||||
assert!(tags(h.resolve(&st(true, None))).contains(&(*b"apkn", 1)));
|
||||
assert!(tags(h.resolve(&st(true, Some(0.0)))).contains(&(*b"apkn", 0)), "manual kerning: none");
|
||||
assert!(!tags(h.resolve_fixed_width(&st(true, None))).iter().any(|(t, _)| t == b"apkn" || t == b"palt"));
|
||||
let v = crate::OtFeatures { vertical: true, ..Default::default() };
|
||||
let with = tags(v.resolve_vertical(&st(true, None), true, true));
|
||||
assert!(with.contains(&(*b"vpal", 1)) && with.contains(&(*b"vkrn", 1)) && with.contains(&(*b"vapk", 1)), "{with:?}");
|
||||
assert!(tags(v.resolve_vertical(&st(true, Some(0.0)), true, true)).contains(&(*b"vkrn", 0)), "manual kerning: none");
|
||||
assert!(tags(v.resolve_vertical(&st(true, None), false, false)).contains(&(*b"vkrn", 0)));
|
||||
|
||||
let Some(noto) = crate::CRAFT_FONTS.iter().find(|f| f.family == "Noto Sans CJK SC") 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![]);
|
||||
db.add_font(noto.bytes.to_vec());
|
||||
let lay = |pm: bool, kerning: Option<f64>| {
|
||||
let s = CharStyle { font_family: "Noto Sans CJK SC".into(), ..st(pm, kerning) };
|
||||
layout(&db, &vertical_styled("「カタカナ」とトップ、ヤッホー。", s)).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
|
||||
};
|
||||
let (auto, manual) = (lay(true, None), lay(true, Some(0.0)));
|
||||
assert!(auto.iter().zip(&manual).any(|(a, b)| (a - b).abs() > 0.01), "kerned: {auto:?} {manual:?}");
|
||||
assert!(lay(false, None).iter().all(|a| (a - 20.0).abs() < 0.01), "off: an em each, as before");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2697,8 +2697,9 @@ OpenType `palt` feature; glyphs the font doesn't re-space keep their full width.
|
|||
the proportional heights of the font's `vpal` feature (the glyphs are shaped top to bottom for it) and move with them;
|
||||
a font without vertical metrics keeps its cells. Line-end Punctuation Half Width then takes nothing more off the
|
||||
punctuation `palt` or `vpal` re-spaced (the opening bracket at a line's start, consecutive punctuation, the closing
|
||||
mark at a line's end); punctuation left full width is trimmed as before. Off by default and saved only when on. One
|
||||
undo step.
|
||||
mark at a line's end); punctuation left full width is trimmed as before. With automatic kerning the font's kerning
|
||||
for the re-spaced glyphs comes too: `apkn` with `palt`, and `vkrn` and `vapk` with `vpal` (vertical type is otherwise
|
||||
not kerned). Off by default and saved only when on. One undo step.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"proportionalMetrics":true}}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue