Merge pull request #1082 from imachus/pr/ja-vpal

Type: Proportional Metrics in vertical type — upright glyphs on the font's proportional heights, `vpal` (#966)
This commit is contained in:
BFlat 2026-10-11 12:25:23 +07:00 committed by GitHub
commit 91eeb5610c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 148 additions and 37 deletions

File diff suppressed because one or more lines are too long

View File

@ -96,8 +96,8 @@ pub struct CharStyle {
#[serde(default, rename = "charAlign", skip_serializing_if = "crate::skip::is_default")]
pub char_align: CharAlign,
/// Proportional Metrics (Character panel menu, East Asian options): full-width glyphs take the
/// font's own proportional widths, OpenType `palt` (#966). Vertical type sets them as before
/// for now.
/// font's own proportional widths, OpenType `palt` (#966); in vertical type upright glyphs take
/// its proportional heights, `vpal`.
#[serde(default, rename = "proportionalMetrics", skip_serializing_if = "crate::skip::is_default")]
pub proportional_metrics: bool,
}

View File

@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Character / Paragraph",
[],
None,
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths, `palt`; horizontal type), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)",
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)",
has_doc,
set_format
),

View File

@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Character",
[],
None,
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths, `palt`; horizontal type)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs}",
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs}",
has_doc,
set_range_style
),

View File

@ -167,11 +167,28 @@ impl OtFeatures {
}
/// Does text in style `st` take proportional widths (`palt`)? Proportional Metrics in
/// horizontal type; vertical type (`vpal`) needs vertical shaping, so it sets as before.
/// horizontal type; vertical type takes proportional heights instead
/// ([`Self::proportional_vertical`]).
pub(crate) fn proportional(&self, st: &CharStyle) -> bool {
st.proportional_metrics && !self.vertical
}
/// Does text in style `st` take proportional heights (`vpal`)? Proportional Metrics in
/// vertical type.
pub(crate) fn proportional_vertical(&self, st: &CharStyle) -> bool {
st.proportional_metrics && self.vertical
}
/// 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> {
let mut v = self.resolve(st);
if proportional {
v.push(f(b"vpal", true));
}
v
}
/// The harfrust features for text in style `st` with full-width glyphs on their full widths:
/// [`Self::resolve`] without `palt`.
pub(crate) fn resolve_fixed_width(&self, st: &CharStyle) -> Vec<Feature> {

View File

@ -55,6 +55,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).
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;
}
}
}
v
@ -1178,6 +1186,7 @@ mod wrapping_tests {
inline: None,
lead: 0.0,
proportional: false,
vpal: None,
level: unicode_bidi::Level::ltr(),
})
.collect()

View File

@ -52,12 +52,25 @@ pub(crate) struct SGlyph {
/// Proportional Metrics changed the glyph's advance (`palt`): it has its proportional width,
/// and Japanese composition takes no more space off it (#966).
pub proportional: bool,
/// Vertical type with Proportional Metrics: what `vpal` does to the glyph when it stands
/// upright ([`crate::layout`] applies it).
pub vpal: Option<VAdjust>,
/// Resolved Unicode bidi embedding level (logical source order).
pub level: Level,
/// An inline graphic ([`vectorcraft_doc::TextRun::inline`]) standing in for a glyph.
pub inline: Option<InlineBox>,
}
/// 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).
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct VAdjust {
pub advance: f64,
pub dx: f64,
pub dy: f64,
}
/// Placement of an inline graphic's art, glyph space being points with the pen at the origin on
/// the baseline (y down).
#[derive(Clone, Copy, Debug, PartialEq)]
@ -121,6 +134,7 @@ pub(crate) fn hyphen_glyph(g: &SGlyph) -> SGlyph {
h.ch = '-';
h.inline = None;
h.proportional = false;
h.vpal = None;
h
}
@ -427,6 +441,7 @@ fn inline_glyph(face: &Arc<FontFace>, st: &CharStyle, art: &InlineArt, run: usiz
tcy: None,
lead: 0.0,
proportional: false,
vpal: None,
// U+FFFC is a bidi neutral (ON): its level, resolved from its neighbours, places it in
// RTL text like any other glyph once the line is reordered.
level,
@ -475,11 +490,14 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
let first_char = |byte: usize| text[byte..].chars().next().unwrap_or(' ');
let mut raw: Vec<(u32, u32, i32, i32, i32)> = Vec::with_capacity(text_seg.len()); // gid, cluster, xadv, xoff, yoff
// Proportional Metrics: which glyphs `palt` gave another advance than their full width.
// Proportional Metrics: which glyphs `palt` gave another advance than their full width, and
// what `vpal` does to each in vertical type.
let mut proportional = vec![];
let mut vpal = vec![];
let shaped = face.hb().map(|hb| {
let shaper = face.shaper.shaper(&hb).instance(face.instance.as_ref()).build();
let shape = |feats: &[Feature]| {
// gid, cluster, x and y advance, x and y offset.
let shape = |feats: &[Feature], direction: Direction| {
let mut buf = UnicodeBuffer::new();
for (i, c) in text_seg.char_indices() {
let cl = (range.start + i) as u32;
@ -491,7 +509,7 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
buf.add(c, cl);
}
}
buf.set_direction(if level.is_rtl() { Direction::RightToLeft } else { Direction::LeftToRight });
buf.set_direction(direction);
buf.guess_segment_properties();
// Harfrust keeps this Unicode context for joining/positional shaping, but does not emit
// glyphs for it. This retains context at real style/font segmentation boundaries without
@ -506,18 +524,38 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
gb.glyph_infos()
.iter()
.zip(gb.glyph_positions())
.map(|(info, pos)| (info.glyph_id, info.cluster, pos.x_advance, pos.x_offset, pos.y_offset))
.map(|(info, pos)| (info.glyph_id, info.cluster, pos.x_advance, pos.y_advance, pos.x_offset, pos.y_offset))
.collect::<Vec<_>>()
};
raw = shape(&feats.resolve(st));
let along = if level.is_rtl() { Direction::RightToLeft } else { Direction::LeftToRight };
raw = shape(&feats.resolve(st), along).into_iter().map(|(g, c, xa, _, xo, yo)| (g, c, xa, xo, yo)).collect();
let same_glyphs = |v: &[(u32, u32, i32, i32, i32, i32)]| v.len() == raw.len() && v.iter().zip(&raw).all(|(a, b)| a.0 == b.0);
if feats.proportional(st) {
// `palt` is positioning only: the same glyphs set on their full widths tell which
// advances it changed (kerning and the rest change both alike).
let full = shape(&feats.resolve_fixed_width(st));
if full.len() == raw.len() && full.iter().zip(&raw).all(|(a, b)| a.0 == b.0) {
let full = shape(&feats.resolve_fixed_width(st), along);
if same_glyphs(&full) {
proportional = full.iter().zip(&raw).map(|(a, b)| a.2 != b.2).collect();
}
}
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) {
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 };
(adj != VAdjust::default()).then_some(adj)
})
.collect();
}
}
});
if shaped.is_none() {
// Fallback: nominal glyphs and hmtx advances, no shaping.
@ -573,6 +611,7 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
tcy: None,
lead: 0.0,
proportional: proportional.get(gi).copied().unwrap_or(false),
vpal: vpal.get(gi).copied().flatten(),
level: *level,
inline: None,
});

View File

@ -753,8 +753,8 @@ fn layout_10k_area_text_is_fast() {
/// Proportional Metrics (#966): in horizontal type full-width glyphs take the font's proportional
/// widths (`palt`), and Japanese composition takes nothing more off the punctuation `palt`
/// re-spaced (the flush bracket at a paragraph's start, consecutive punctuation, half width at a
/// line's end); punctuation it leaves full width is trimmed as before. Vertical type sets as
/// before. Needs craft-fonts' Shippori Mincho, whose `palt` covers kana and most brackets but not
/// line's end); punctuation it leaves full width is trimmed as before (vertical type: see
/// `tests_vertical`). Needs craft-fonts' Shippori Mincho, whose `palt` covers kana and most brackets but not
/// 〘〙 (skipped without it).
#[test]
fn proportional_metrics_set_full_width_glyphs_on_their_proportional_widths() {
@ -765,32 +765,29 @@ fn proportional_metrics_set_full_width_glyphs_on_their_proportional_widths() {
}
let st = |on: bool| CharStyle { size: 20.0, font_family: "Shippori Mincho".into(), proportional_metrics: on, ..CharStyle::default() };
// Right-aligned in a frame ten ems wide, so a line's end trim moves the line.
let lay = |text: &str, on: bool, m: Mojikumi, vertical: bool| {
let lay = |text: &str, on: bool, m: Mojikumi| {
let mut t = area(text, st(on), Rect::new(0.0, 0.0, 200.0, 200.0), Justify::Right);
t.vertical = vertical;
t.para.mojikumi = m;
layout(db(), &t)
};
let adv = |l: &TextLayout| l.glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
let x0 = |l: &TextLayout| l.glyphs.iter().map(|g| g.outline.bounding_box().x0).collect::<Vec<_>>();
let near = |a: &[f64], b: &[f64]| a.len() == b.len() && a.iter().zip(b).all(|(a, b)| (a - b).abs() < 0.01);
let off = adv(&lay("「あ漢」", false, Mojikumi::None, false));
let on = adv(&lay("「あ漢」", true, Mojikumi::None, false));
let off = adv(&lay("「あ漢」", false, Mojikumi::None));
let on = adv(&lay("「あ漢」", true, Mojikumi::None));
assert!(off.iter().all(|a| (a - 20.0).abs() < 0.01), "{off:?}");
assert!(on[0] < 15.0 && on[1] < 19.0 && on[3] < 15.0, "kana and brackets narrower: {on:?}");
assert!((on[2] - 20.0).abs() < 0.01, "the kanji keeps its em: {on:?}");
// Re-spaced punctuation: Line-end Punctuation Half Width changes nothing, at the paragraph's
// start (「), between punctuation (」、) or at the line's end (」).
for text in ["「あ」、漢", "漢「あ」"] {
let (half, none) = (lay(text, true, Mojikumi::LineEndHalf, false), lay(text, true, Mojikumi::None, false));
let (half, none) = (lay(text, true, Mojikumi::LineEndHalf), lay(text, true, Mojikumi::None));
assert!(near(&adv(&half), &adv(&none)), "{text}: {:?} {:?}", adv(&half), adv(&none));
assert!(near(&x0(&half), &x0(&none)), "{text}: {:?} {:?}", x0(&half), x0(&none));
}
// Punctuation `palt` leaves full width: flush at the paragraph's start, half width at the end.
let (half, none) = (lay("〘あ〙", true, Mojikumi::LineEndHalf, false), lay("〘あ〙", true, Mojikumi::None, false));
let (half, none) = (lay("〘あ〙", true, Mojikumi::LineEndHalf), lay("〘あ〙", true, Mojikumi::None));
assert!((none.glyphs[0].advance - 20.0).abs() < 0.01 && (half.glyphs[0].advance - 10.0).abs() < 0.01, "{:?}", adv(&half));
let moved = x0(&half)[1] - x0(&none)[1];
assert!((moved - 10.0).abs() < 0.01, "〙's empty half goes past the line's end: あ moved {moved}");
// Vertical type sets as before.
assert!(near(&adv(&lay("「あ漢」", true, Mojikumi::None, true)), &adv(&lay("「あ漢」", false, Mojikumi::None, true))));
}

View File

@ -539,3 +539,46 @@ fn kinsoku_set_decides_whether_a_small_kana_may_start_a_line() {
assert_eq!(stop(Kinsoku::Soft), [0, 1, 1], "vertical {vertical_type}");
}
}
/// Proportional Metrics in vertical type (#966): upright glyphs take the font's proportional
/// heights (`vpal`, from shaping them top to bottom) and move with them, so each glyph's ink stays in
/// its shorter cell. Line-end Punctuation Half Width takes nothing more off the punctuation `vpal`
/// re-spaced, and trims the punctuation it leaves full height (〘〙 in Shippori Mincho) as before.
/// Off, the column is as before, and a font without vertical metrics (the bundled Source Sans 3)
/// keeps today's cells. Needs craft-fonts' Shippori Mincho (skipped without it).
#[test]
fn proportional_metrics_set_upright_glyphs_on_their_proportional_heights() {
use vectorcraft_doc::Mojikumi;
let lay = |text: &str, font: &str, on: bool, m: Mojikumi| {
let mut t = vertical_styled(text, CharStyle { size: 20.0, font_family: font.into(), proportional_metrics: on, ..CharStyle::default() });
t.para.mojikumi = m;
layout(FontDb::global(), &t)
};
let adv = |l: &TextLayout| l.glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
let y0 = |l: &TextLayout| l.glyphs.iter().map(|g| g.outline.bounding_box().y0).collect::<Vec<_>>();
let near = |a: &[f64], b: &[f64]| a.len() == b.len() && a.iter().zip(b).all(|(a, b)| (a - b).abs() < 0.01);
// Without vertical metrics the cells stay as they are.
let (on, off) = (lay("§§", "Source Sans 3", true, Mojikumi::None), lay("§§", "Source Sans 3", false, Mojikumi::None));
assert!(near(&adv(&on), &adv(&off)) && near(&y0(&on), &y0(&off)), "{:?} {:?}", adv(&on), adv(&off));
if FontDb::global().face("Shippori Mincho", "Regular").is_none_or(|f| f.family != "Shippori Mincho") {
eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout to run it)");
return;
}
let text = "「あ」、ッ漢。";
let off = lay(text, "Shippori Mincho", false, Mojikumi::None);
assert!(off.glyphs.iter().all(|g| (g.advance - 20.0).abs() < 0.01), "off: an em each, as before: {:?}", adv(&off));
let on = lay(text, "Shippori Mincho", true, Mojikumi::None);
assert!(on.glyphs[0].advance < 12.0 && on.glyphs[4].advance < 16.0, "「 and ッ shorter: {:?}", adv(&on));
assert!((on.glyphs[5].advance - 20.0).abs() < 0.01, "the kanji keeps its em: {:?}", adv(&on));
// Each glyph's ink stays in its own cell, so none runs into the next.
for g in &on.glyphs {
let b = g.outline.bounding_box();
assert!(b.y0 > g.origin.y - 0.5 && b.y1 < g.origin.y + g.advance + 0.5, "byte {}: ink {b:?} in [{}, +{}]", g.byte, g.origin.y, g.advance);
}
// Re-spaced punctuation: Line-end Punctuation Half Width changes nothing.
let half = lay(text, "Shippori Mincho", true, Mojikumi::LineEndHalf);
assert!(near(&adv(&half), &adv(&on)) && near(&y0(&half), &y0(&on)), "{:?} {:?}", adv(&half), adv(&on));
// Punctuation `vpal` leaves full height: set flush at the paragraph's start, as before.
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));
}

View File

@ -655,7 +655,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
}
});
});
// Proportional Metrics: full-width glyphs on the font's proportional widths (`palt`).
// Proportional Metrics: full-width glyphs on the font's proportional widths (`palt`, `vpal`).
let proportional = s.as_ref().is_some_and(|s| s.proportional_metrics);
if menu_item(ui, tl!("Proportional Metrics"), has, proportional) {
format(app, json!({"proportionalMetrics": !proportional}));

View File

@ -1,6 +1,6 @@
# Gaps
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred) · **Target:** Adobe Illustrator 2026 (30.x)
Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from
switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make
@ -96,8 +96,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
## G4 Advanced type
- **Missing:** hyphenation and justification options, Optical Margin Alignment (type core); CJK vertical
composition (ruby, mojikumi sets and dialog, proportional vertical metrics #966, kinsoku settings #633, manual
tate-chu-yoko), Middle Eastern features beyond bidi, variable font axes, spell check with an open dictionary,
composition (ruby, mojikumi sets and dialog, kinsoku settings #633, manual tate-chu-yoko), Middle Eastern
features beyond bidi, variable font axes, spell check with an open dictionary,
Touch Type, Retype, Snap to Glyph; optional preservation of font sizes during group scaling (#1034 enhancement).
Itemized in [type-parity.md](type-parity.md).
- **Impact:** professional typography and Japanese/Chinese publishing.
@ -203,6 +203,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | G4: proportional vertical metrics (#966) done; estimate unchanged |
| 2026-10-11 | minor | G11: #893 fixed (merged regions of crossing lines left seams); #937 still open, estimate unchanged |
| 2026-10-11 | minor | #1034: corrected scaled type controls; optional group font-size preservation remains separate. Estimates unchanged at this rounding. |
| 2026-10-11 | minor | G9: legacy `.ai` without the prolog opens with its layers (#1027) |

View File

@ -2669,10 +2669,12 @@ without ideographs uses its em box. One undo step.
`text.setFormat {proportionalMetrics: true}` (the selected type, or `ids`) and `text.setRangeStyle {id, start, end,
proportionalMetrics}` (a range) set full-width glyphs on the proportional widths the font gives them with the
OpenType `palt` feature; glyphs the font doesn't re-space keep their full width. Line-end Punctuation Half Width then takes nothing more off the punctuation `palt` 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. Horizontal type only for now: vertical type sets as before.
One undo step.
OpenType `palt` feature; glyphs the font doesn't re-space keep their full width. In vertical type, upright glyphs take
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.
```json
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"proportionalMetrics":true}}}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
# Type parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (document-point character controls after object/group scaling, #1034) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Proportional Metrics in vertical type, `vpal`, #966; document-point character controls after object/group scaling, #1034) · **Target:** Adobe Illustrator 2026 (30.x)
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
[target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the
@ -43,12 +43,14 @@ against Illustrator's public documentation; **estimated** unless noted.
| Middle Eastern: Character Direction, digit types, kashidas, Middle Eastern composers, split caret | missing | |
| CJK: input methods (marked text, candidate window) | done | |
| CJK: kinsoku (Hard, Soft, None), Line-end Punctuation Half Width, tate-chu-yoko (auto) | done / partial | custom kinsoku sets, the Mojikumi Settings dialog and the other mojikumi sets missing |
| CJK: ruby, proportional vertical metrics (`vpal`/`palt`), manual tate-chu-yoko | missing | #966, #633 |
| CJK: Proportional Metrics (`palt` in horizontal type, `vpal` in vertical type) | done | #966 |
| CJK: ruby, manual tate-chu-yoko | missing | #633 |
| Inline symbols in text | beyond Illustrator | not scored |
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | Proportional Metrics done: `palt` in horizontal type, `vpal` in vertical type (#966, #1082); ruby and manual tate-chu-yoko still missing |
| 2026-10-10 | minor | #1034: transformed character controls and regression coverage; preserving group font sizes remains a separate enhancement. Scores/hours unchanged at this rounding. |
| 2026-10-10 | major | First checklist, split out of the parity table's Type core and Type advanced rows |