mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 05:55:32 +00:00
Type: glyph forms and width forms follow the OpenType rules
- A form other than the default, set with text.setRangeStyle or text.setFormat, takes away the characters' glyph choice, which would otherwise keep drawing its own glyph. "default" keeps it. - With a width form (fwid, hwid, twid, qwid), shaping leaves out palt and turns kern and apkn off, and SVG output writes font-kerning: none and no palt, as the OpenType feature list asks. proportionalMetrics and kerning keep their values. - The Glyphs panel menu counts a width form as available when a face has the feature in GSUB or GPOS. - At a caret, the menu's check marks and its no-op check read all the text of the object, which is what the command formats. Choosing the current form again is not skipped when a character has a glyph choice.
This commit is contained in:
@@ -298,6 +298,15 @@ impl GlyphForm {
|
||||
GlyphForm::QuarterWidth => b"qwid",
|
||||
})
|
||||
}
|
||||
|
||||
/// Is this one of the monospaced width forms (`fwid`, `hwid`, `twid`, `qwid`)? A font may
|
||||
/// draw them with other glyphs (`GSUB`) or with other widths for the same glyphs (`GPOS`), and
|
||||
/// they set the characters on a fixed width, so Proportional Metrics (`palt`) is not applied
|
||||
/// with them.
|
||||
/// <https://learn.microsoft.com/en-us/typography/opentype/spec/features_fj#tag-fwid>
|
||||
pub fn is_width(self) -> bool {
|
||||
matches!(self, GlyphForm::FullWidth | GlyphForm::HalfWidth | GlyphForm::ThirdWidth | GlyphForm::QuarterWidth)
|
||||
}
|
||||
}
|
||||
|
||||
/// A glyph chosen for one character ([`CharStyle::glyph_alternate`]): the glyph id and the face it
|
||||
@@ -785,6 +794,15 @@ impl CharStyle {
|
||||
self.position = self.position.in_range();
|
||||
self.small_caps = self.small_caps.map(small_caps_in_range);
|
||||
}
|
||||
/// Sets the East Asian glyph form ([`CharStyle::glyph_form`]). A form other than the default
|
||||
/// also takes away the character's glyph choice ([`CharStyle::glyph_alternate`]), which would
|
||||
/// otherwise keep drawing its own glyph; the default form keeps it.
|
||||
pub fn set_glyph_form(&mut self, form: GlyphForm) {
|
||||
self.glyph_form = form;
|
||||
if form != GlyphForm::Default {
|
||||
self.glyph_alternate = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Document {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1974,6 +1974,38 @@ fn glyph_form_is_one_character_attribute_saved_only_when_set() {
|
||||
assert!(runs(&s).iter().all(|r| r.style.glyph_form == GlyphForm::Default));
|
||||
}
|
||||
|
||||
/// A glyph choice (`text.setGlyphAlternate`) would keep drawing its own glyph over a glyph form,
|
||||
/// so setting a form other than "default" takes it away, in `text.setRangeStyle` and in
|
||||
/// `text.setFormat`; "default" (Revert To Default Forms) and calls without `glyphForm` keep it.
|
||||
#[test]
|
||||
fn a_glyph_form_takes_away_the_characters_glyph_choice() {
|
||||
use vectorcraft_doc::{GlyphChoice, GlyphForm};
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "亜唖"})).unwrap()["id"].as_u64().unwrap();
|
||||
let choice = GlyphChoice { ch: '唖', gid: 42, font: "Test-Regular".into(), version: "1".into() };
|
||||
if let Some(NodeKind::Text(t)) = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).node_mut(NodeId(id)).map(|n| &mut n.kind) {
|
||||
vectorcraft_text::edit::style_range(&mut t.runs, 3, 6, |st| st.glyph_alternate = Some(choice.clone()));
|
||||
}
|
||||
let state = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.iter().map(|r| (r.text.clone(), r.style.glyph_form, r.style.glyph_alternate.is_some())).collect::<Vec<_>>(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "size": 30})).unwrap();
|
||||
assert_eq!(state(&s), [("亜".into(), GlyphForm::Default, false), ("唖".into(), GlyphForm::Default, true)], "no glyphForm: kept");
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "glyphForm": "default"})).unwrap();
|
||||
assert_eq!(state(&s)[1], ("唖".into(), GlyphForm::Default, true), "default: kept");
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "start": 3, "end": 6, "glyphForm": "jis78"})).unwrap();
|
||||
assert_eq!(state(&s), [("亜".into(), GlyphForm::Default, false), ("唖".into(), GlyphForm::Jis78, false)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(state(&s)[1], ("唖".into(), GlyphForm::Default, true), "one undo step brings both back");
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
s.execute("text.setFormat", &json!({"glyphForm": "default"})).unwrap();
|
||||
assert!(state(&s).iter().any(|r| r.2), "default: kept");
|
||||
s.execute("text.setFormat", &json!({"glyphForm": "traditional"})).unwrap();
|
||||
assert!(state(&s).iter().all(|r| r.1 == GlyphForm::Traditional && !r.2), "{:?}", state(&s));
|
||||
}
|
||||
|
||||
/// No Break is a character attribute, saved only when on.
|
||||
#[test]
|
||||
fn no_break_is_a_character_attribute_saved_only_when_on() {
|
||||
|
||||
+25
-4
@@ -168,8 +168,8 @@ pub(crate) fn type_props(st: &CharStyle, len: &dyn Fn(f64) -> String) -> Props {
|
||||
if spacing != 0.0 {
|
||||
p.push(("letter-spacing", len(spacing / 1000.0 * st.size * st.v_scale / 100.0)));
|
||||
}
|
||||
if st.kerning.is_some() {
|
||||
// Manual kerning replaces the font's pair kerning.
|
||||
if st.kerning.is_some() || st.glyph_form.is_width() {
|
||||
// Manual kerning replaces the font's pair kerning, and a monospaced width form turns it off.
|
||||
p.push(("font-kerning", "none".into()));
|
||||
}
|
||||
let mut v: Vec<String> = st
|
||||
@@ -190,8 +190,9 @@ pub(crate) fn type_props(st: &CharStyle, len: &dyn Fn(f64) -> String) -> Props {
|
||||
.filter(|(tag, _)| tag.len() == 4 && tag.bytes().all(|b| b.is_ascii_alphanumeric()))
|
||||
.map(|(tag, value)| format!("\"{tag}\" {value}"))
|
||||
.collect();
|
||||
// Proportional Metrics: `palt` (SVG writes vertical type as outlines).
|
||||
if st.proportional_metrics {
|
||||
// Proportional Metrics: `palt` (SVG writes vertical type as outlines), not with a monospaced
|
||||
// width form, which sets the characters on a fixed width.
|
||||
if st.proportional_metrics && !st.glyph_form.is_width() {
|
||||
v.push("\"palt\" 1".into());
|
||||
}
|
||||
// The East Asian glyph form: its one feature.
|
||||
@@ -876,4 +877,24 @@ mod tests {
|
||||
assert_eq!(css_string("w\r\"</style>"), r#""w\d \"\3c /style\3e ""#);
|
||||
assert_eq!(css_string("wght"), "\"wght\"");
|
||||
}
|
||||
|
||||
/// A monospaced width form sets the characters on a fixed width: Proportional Metrics' `palt`
|
||||
/// is not written with it and the font's pair kerning is turned off, and with the other forms
|
||||
/// `palt` is written and kerning stays.
|
||||
#[test]
|
||||
fn width_forms_leave_out_proportional_metrics() {
|
||||
use vectorcraft_doc::GlyphForm;
|
||||
let prop = |form, key: &str| {
|
||||
let st = CharStyle { proportional_metrics: true, glyph_form: form, ..CharStyle::default() };
|
||||
type_props(&st, &|v| format!("{v}px")).into_iter().find(|(k, _)| *k == key).map(|(_, v)| v)
|
||||
};
|
||||
assert_eq!(prop(GlyphForm::Default, "font-feature-settings").as_deref(), Some("\"palt\" 1"));
|
||||
assert_eq!(prop(GlyphForm::Jis78, "font-feature-settings").as_deref(), Some("\"palt\" 1, \"jp78\" 1"));
|
||||
assert_eq!(prop(GlyphForm::Jis78, "font-kerning"), None);
|
||||
for form in [GlyphForm::FullWidth, GlyphForm::HalfWidth, GlyphForm::ThirdWidth, GlyphForm::QuarterWidth] {
|
||||
let tag = std::str::from_utf8(form.feature().unwrap()).unwrap();
|
||||
assert_eq!(prop(form, "font-feature-settings"), Some(format!("\"{tag}\" 1")), "{form:?}");
|
||||
assert_eq!(prop(form, "font-kerning").as_deref(), Some("none"), "{form:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,8 @@ impl OtFeatures {
|
||||
let own = self.with_tags(listed.iter().map(String::as_str));
|
||||
let s = &own;
|
||||
let mut v = Vec::with_capacity(8);
|
||||
if st.kerning.is_some() {
|
||||
// A monospaced width form turns the font's pair kerning off as well.
|
||||
if st.kerning.is_some() || st.glyph_form.is_width() {
|
||||
v.push(f(b"kern", false));
|
||||
}
|
||||
let liga = s.ligatures && (!ligatures_suppressed_by(st.tracking) || explicit_ligatures(listed));
|
||||
@@ -332,15 +333,20 @@ impl OtFeatures {
|
||||
// Kerning for the glyphs `palt` makes proportional (OpenType `apkn`), with automatic
|
||||
// kerning only.
|
||||
v.push(f(b"apkn", st.kerning.is_none()));
|
||||
} else if st.glyph_form.is_width() {
|
||||
// Listed as off, not left out: in runs shaped together with proportional text, a
|
||||
// feature left out is set back to the shaper's default, which for `apkn` is on.
|
||||
v.push(f(b"apkn", false));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Does text in style `st` take proportional widths (`palt`)? Proportional Metrics in
|
||||
/// horizontal type; vertical type takes proportional heights instead
|
||||
/// horizontal type, except with a monospaced width form ([`vectorcraft_doc::GlyphForm::is_width`]),
|
||||
/// which sets the characters on a fixed width; vertical type takes proportional heights instead
|
||||
/// ([`Self::proportional_vertical`]).
|
||||
pub(crate) fn proportional(&self, st: &CharStyle) -> bool {
|
||||
st.proportional_metrics && !self.vertical
|
||||
st.proportional_metrics && !self.vertical && !st.glyph_form.is_width()
|
||||
}
|
||||
|
||||
/// Does text in style `st` take proportional heights (`vpal`)? Proportional Metrics in
|
||||
@@ -595,4 +601,44 @@ mod tests {
|
||||
assert!(serbian.iter().zip(&russian).all(|(s, r)| s != r), "every letter in its Serbian form");
|
||||
assert_eq!(outlines("sr-Latn"), serbian);
|
||||
}
|
||||
|
||||
/// A monospaced width form (`fwid`, `hwid`, `twid`, `qwid`) and Proportional Metrics' `palt`
|
||||
/// are mutually exclusive: with a width form `palt` is left out and the font's pair kerning
|
||||
/// (`kern`, and `apkn` for `palt`'s glyphs) is turned off, with automatic or manual kerning,
|
||||
/// and with the other forms they stay.
|
||||
/// <https://learn.microsoft.com/en-us/typography/opentype/spec/features_fj#tag-fwid>
|
||||
#[test]
|
||||
fn width_forms_turn_off_proportional_metrics() {
|
||||
use vectorcraft_doc::GlyphForm;
|
||||
let tags = |form, kerning, value| {
|
||||
let st = CharStyle { proportional_metrics: true, glyph_form: form, kerning, ..CharStyle::default() };
|
||||
OtFeatures::default().resolve(&st).iter().filter(|f| f.value == value).map(|f| f.tag.to_string()).collect::<Vec<_>>()
|
||||
};
|
||||
for form in [GlyphForm::Default, GlyphForm::Traditional, GlyphForm::Jis78] {
|
||||
assert!(tags(form, None, 1).contains(&"palt".to_string()), "{form:?}: {:?}", tags(form, None, 1));
|
||||
assert!(!tags(form, None, 0).contains(&"kern".to_string()), "{form:?}: {:?}", tags(form, None, 0));
|
||||
}
|
||||
for form in [GlyphForm::FullWidth, GlyphForm::HalfWidth, GlyphForm::ThirdWidth, GlyphForm::QuarterWidth] {
|
||||
for kerning in [None, Some(0.0)] {
|
||||
let on = tags(form, kerning, 1);
|
||||
assert!(!on.contains(&"palt".to_string()) && !on.contains(&"apkn".to_string()), "{form:?}: {on:?}");
|
||||
assert!(on.iter().any(|t| t.as_bytes() == form.feature().unwrap()), "{form:?}: {on:?}");
|
||||
// Listed as off, so that runs shaped together with proportional text keep them off.
|
||||
let off = tags(form, kerning, 0);
|
||||
assert!(off.contains(&"kern".to_string()) && off.contains(&"apkn".to_string()), "{form:?} {kerning:?}: {off:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A face's `GPOS` features are found apart from its `GSUB` ones: Source Sans 3 kerns with
|
||||
/// `GPOS` and makes ligatures with `GSUB`.
|
||||
#[test]
|
||||
fn positioning_and_substitution_features_are_found_in_their_own_tables() {
|
||||
let face = FontDb::global().face("Source Sans 3", "Regular").unwrap();
|
||||
assert!(face.has_positioning_feature(*b"kern"));
|
||||
assert!(!face.has_substitution_feature(*b"kern"));
|
||||
assert!(face.has_substitution_feature(*b"liga"));
|
||||
assert!(!face.has_positioning_feature(*b"liga"));
|
||||
assert!(!face.has_positioning_feature(*b"fwid"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,17 @@ fn ligature_glyphs(t: &LigatureSubstFormat1<'_>, room: usize) -> Vec<u32> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Does `features` (a `GSUB` or `GPOS` feature list) have feature `tag` with at least one lookup?
|
||||
/// At most [`MAX_FEATURES`] feature records are read.
|
||||
fn has_feature(features: &skrifa::raw::tables::layout::FeatureList<'_>, tag: [u8; 4]) -> bool {
|
||||
features
|
||||
.feature_records()
|
||||
.iter()
|
||||
.take(MAX_FEATURES)
|
||||
.filter(|r| r.feature_tag().to_be_bytes() == tag)
|
||||
.any(|r| r.feature(features.offset_data()).is_ok_and(|f| !f.lookup_list_indices().is_empty()))
|
||||
}
|
||||
|
||||
impl GsubIndex {
|
||||
/// Read `face`'s `GSUB` table. Empty when the face has none or it cannot be read; glyph ids past
|
||||
/// the face's glyph count are left out; the caps above bound what a damaged table makes it read.
|
||||
@@ -309,13 +320,15 @@ impl FontFace {
|
||||
/// cannot be read.
|
||||
pub fn has_substitution_feature(&self, tag: [u8; 4]) -> bool {
|
||||
let Some(font) = self.skrifa() else { return false };
|
||||
let Some(features) = font.gsub().ok().and_then(|g| g.feature_list().ok()) else { return false };
|
||||
features
|
||||
.feature_records()
|
||||
.iter()
|
||||
.take(MAX_FEATURES)
|
||||
.filter(|r| r.feature_tag().to_be_bytes() == tag)
|
||||
.any(|r| r.feature(features.offset_data()).is_ok_and(|f| !f.lookup_list_indices().is_empty()))
|
||||
font.gsub().ok().and_then(|g| g.feature_list().ok()).is_some_and(|features| has_feature(&features, tag))
|
||||
}
|
||||
|
||||
/// Does the face's `GPOS` table have feature `tag` with at least one lookup? At most
|
||||
/// [`MAX_FEATURES`] feature records are read; false when the face has no `GPOS` table or it
|
||||
/// cannot be read.
|
||||
pub fn has_positioning_feature(&self, tag: [u8; 4]) -> bool {
|
||||
let Some(font) = self.skrifa() else { return false };
|
||||
font.gpos().ok().and_then(|g| g.feature_list().ok()).is_some_and(|features| has_feature(&features, tag))
|
||||
}
|
||||
|
||||
/// What the features `aalt`, `salt`, `ss01` to `ss20`, `swsh`, `cv01` to `cv99`, `hist`,
|
||||
|
||||
@@ -1535,7 +1535,8 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
char_level.set(false);
|
||||
*chars = crate::harakat::own_chars(raw, &by_char, range.end, text, |gid| classes.get(skrifa::GlyphId::new(gid)) == 3);
|
||||
}
|
||||
if feats.proportional(st) {
|
||||
// Runs shaped together may differ in their glyph form, and a width form turns `palt` off.
|
||||
if feats.proportional(st) || runs.get(source_runs.clone()).unwrap_or_default().iter().any(|(_, s)| feats.proportional(s)) {
|
||||
// `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(&with_more(&|s| feats.resolve_fixed_width(s)), along);
|
||||
|
||||
@@ -978,6 +978,35 @@ fn selection_faces(app: &VectorcraftApp, ui: &Ui) -> Vec<u32> {
|
||||
ids
|
||||
}
|
||||
|
||||
/// The value `f` gives every character [`glyph_form_items`] acts on: the Type tool's selected
|
||||
/// range, all the text of the object at a caret (the menu formats all of it there), else every
|
||||
/// frame of the selected stories. `None` where they differ, or with no type.
|
||||
fn selection_shared<T: PartialEq>(app: &VectorcraftApp, f: impl Fn(&CharStyle) -> T) -> Option<T> {
|
||||
match super::character::text_editing(app) {
|
||||
Some((id, a, b)) if b <= a && super::character::text_story(app).is_none() => {
|
||||
let NodeKind::Text(t) = &app.session.active()?.doc.node(id)?.kind else { return None };
|
||||
let mut acc = super::character::Shared::None;
|
||||
for r in &t.runs {
|
||||
acc.add(f(&r.style));
|
||||
}
|
||||
acc.value()
|
||||
}
|
||||
_ => super::character::shared(app, f),
|
||||
}
|
||||
}
|
||||
|
||||
/// The East Asian glyph form every character [`glyph_form_items`] acts on has ([`selection_shared`]).
|
||||
fn selection_glyph_form(app: &VectorcraftApp) -> Option<GlyphForm> {
|
||||
selection_shared(app, |s| s.glyph_form)
|
||||
}
|
||||
|
||||
/// Does choosing `form` change the characters [`glyph_form_items`] acts on? Not when they all have
|
||||
/// it already, unless one of them has a glyph choice, which a form other than the default takes
|
||||
/// away.
|
||||
fn glyph_form_changes(app: &VectorcraftApp, form: GlyphForm) -> bool {
|
||||
selection_glyph_form(app) != Some(form) || (form != GlyphForm::Default && selection_shared(app, |s| s.glyph_alternate.is_none()) != Some(true))
|
||||
}
|
||||
|
||||
/// The East Asian glyph forms of the panel menu, with Show East Asian Options: each draws the
|
||||
/// selected characters (all the text of the selected type objects without a Type tool range) in
|
||||
/// one form of their font, and is checked when they all have it; a form none of the faces that
|
||||
@@ -989,10 +1018,12 @@ fn glyph_form_items(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let has = super::character::text_style(app).is_some();
|
||||
let db = FontDb::global();
|
||||
let faces: Vec<_> = selection_faces(app, ui).into_iter().filter_map(|id| db.face_by_id(id)).collect();
|
||||
let current = super::character::shared(app, |s| s.glyph_form);
|
||||
let current = selection_glyph_form(app);
|
||||
for form in GlyphForm::ALL {
|
||||
// A width form may be drawn with other widths for the same glyphs (`GPOS`) instead of
|
||||
// other glyphs.
|
||||
let enabled = match form.feature() {
|
||||
Some(tag) => faces.iter().any(|f| f.has_substitution_feature(*tag)),
|
||||
Some(tag) => faces.iter().any(|f| f.has_substitution_feature(*tag) || (form.is_width() && f.has_positioning_feature(*tag))),
|
||||
None => has,
|
||||
};
|
||||
let checked = form != GlyphForm::Default && current == Some(form);
|
||||
@@ -1000,7 +1031,7 @@ fn glyph_form_items(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.separator();
|
||||
}
|
||||
// Choosing the form every selected character already has changes nothing.
|
||||
if menu_item(ui, tl!(glyph_form_label(form)), enabled, checked) && current != Some(form) {
|
||||
if menu_item(ui, tl!(glyph_form_label(form)), enabled, checked) && glyph_form_changes(app, form) {
|
||||
super::character::format(app, json!({"glyphForm": form.id()}));
|
||||
}
|
||||
}
|
||||
@@ -1261,6 +1292,51 @@ mod tests {
|
||||
assert!(forms(&app).iter().all(|(_, f)| *f == GlyphForm::Default), "{:?}", forms(&app));
|
||||
}
|
||||
|
||||
/// At a caret the glyph form items act on all the text of the object, so they are checked, and
|
||||
/// skipped as changing nothing, by every character's form, not the caret's: with the caret in
|
||||
/// text of the default form and other text in JIS 78 forms, Revert To Default Forms reverts
|
||||
/// that text.
|
||||
#[test]
|
||||
fn glyph_form_items_at_a_caret_follow_all_the_text() {
|
||||
let (mut app, id) = editing("亜唖");
|
||||
app.session.prefs.show_east_asian_options = true;
|
||||
let ctx = egui::Context::default();
|
||||
let node = NodeId(id.as_u64().unwrap_or(0));
|
||||
let forms = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(node).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.iter().map(|r| r.style.glyph_form).collect::<Vec<_>>(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 3, "glyphForm": "jis78"})).unwrap();
|
||||
select(&mut app, 6, 6);
|
||||
assert_eq!(selection_glyph_form(&app), None, "the caret's form is not every character's");
|
||||
choose(&ctx, &mut app, "Revert To Default Forms");
|
||||
assert!(forms(&app).iter().all(|f| *f == GlyphForm::Default), "{:?}", forms(&app));
|
||||
app.run("text.setRangeStyle", json!({"id": id, "glyphForm": "jis78"})).unwrap();
|
||||
select(&mut app, 6, 6);
|
||||
assert_eq!(selection_glyph_form(&app), Some(GlyphForm::Jis78));
|
||||
}
|
||||
|
||||
/// Choosing the form every character already has is skipped as changing nothing, unless a
|
||||
/// character has a glyph choice: a form other than the default takes it away, and Revert To
|
||||
/// Default Forms keeps it.
|
||||
#[test]
|
||||
fn a_glyph_choice_lets_the_current_form_be_chosen_again() {
|
||||
use vectorcraft_text::test_fonts::{alternates_family, alternates_font, unencoded_glyphs};
|
||||
FontDb::global().add_font(alternates_font(false).expect("the test font builds"));
|
||||
let [u1, ..] = unencoded_glyphs().expect("unencoded glyphs");
|
||||
let (mut app, id) = editing("xay");
|
||||
app.run("text.setRangeStyle", json!({"id": id, "font": alternates_family(false), "glyphForm": "jis78"})).unwrap();
|
||||
select(&mut app, 3, 3);
|
||||
assert!(!glyph_form_changes(&app, GlyphForm::Jis78), "every character has it");
|
||||
assert!(glyph_form_changes(&app, GlyphForm::Default));
|
||||
app.run("text.setGlyphAlternate", json!({"id": id, "start": 1, "gid": u1})).unwrap();
|
||||
select(&mut app, 3, 3);
|
||||
assert!(glyph_form_changes(&app, GlyphForm::Jis78), "it takes away the glyph choice");
|
||||
app.run("text.setRangeStyle", json!({"id": id, "glyphForm": "default"})).unwrap();
|
||||
select(&mut app, 3, 3);
|
||||
assert!(!glyph_form_changes(&app, GlyphForm::Default), "Revert To Default Forms keeps the glyph choice");
|
||||
}
|
||||
|
||||
/// Show › Alternates for Current Selection lists the selected character's alternates from the
|
||||
/// font of its text, whatever font the panel browses; a double-click draws the character with
|
||||
/// one. A character without alternates, no selected character and a missing font each show a
|
||||
|
||||
+14
-7
@@ -9126,13 +9126,20 @@ as outlines.
|
||||
`fwid`, `hwid`, `twid` or `qwid` ([feature list](https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist)),
|
||||
or `"default"`, the font's default glyphs. A character holds one form at a time, so setting one replaces another. The
|
||||
text keeps its characters, and a character the font has no glyph of that form for keeps its own. Runs that differ only
|
||||
in their form are shaped as one piece of text. The form is saved only when it is not `"default"`, character styles
|
||||
carry it, PDF draws the glyphs the canvas draws, and live SVG text writes it in `font-feature-settings`. Another value
|
||||
is refused with `` `glyphForm` must be one of "traditional", … ``. One undo step, and a `text.setFormat` call that
|
||||
gives the form the characters already have records none. With Show East Asian Options on, the Glyphs panel menu lists
|
||||
the ten forms and Revert To Default Forms: a form is checked when every selected character has it, choosing it again
|
||||
does nothing, and it is disabled when none of the faces that draw the selected text, fallback faces included, has a
|
||||
feature for it.
|
||||
in their form are shaped as one piece of text. A form other than `"default"` also takes away the characters' glyph
|
||||
choice (`text.setGlyphAlternate`), which would otherwise keep drawing its own glyph; `"default"` keeps it. The four
|
||||
width forms set the characters on a fixed width, so Proportional Metrics (`palt`) is not applied with them and the
|
||||
font's pair kerning (`kern`, `apkn`) is turned off, in shaping and in SVG output (`font-kerning: none`);
|
||||
`proportionalMetrics` and `kerning` keep their values. The form is saved only when it is not `"default"`, character
|
||||
styles carry it, PDF draws the glyphs the canvas draws, and live SVG text writes it in `font-feature-settings`. Another
|
||||
value is refused with `` `glyphForm` must be one of "traditional", … ``. One undo step, and a `text.setFormat` call
|
||||
that gives the form the characters already have records none. With Show East Asian Options on, the Glyphs panel menu
|
||||
lists the ten forms and Revert To Default Forms. They act on the Type tool's selected range, all the text of the object
|
||||
at a caret, or all the text of the selected type objects. A form is checked when every one of those characters has
|
||||
it, and choosing it again does nothing unless one of them has a glyph choice, which the form takes away. A form is
|
||||
disabled when none of the faces that draw the selected text, fallback faces included, has a feature for it: a `GSUB` feature, or for the width
|
||||
forms a `GSUB` or `GPOS` feature
|
||||
([fwid](https://learn.microsoft.com/en-us/typography/opentype/spec/features_fj#tag-fwid)).
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.setRangeStyle","params":{"id":12,"start":0,"end":3,"glyphForm":"jis78"}}}
|
||||
|
||||
@@ -1447,7 +1447,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The Paragraph panel's Mojikumi Set lists the five predefined sets the help page names (None, YakumonoHankaku, GyomatsuYakumonoHankaku, GyomatsuYakumonoZenkaku and YakumonoZenkaku), then Line-end Punctuation Half Width, and `text.setFormat {mojikumi}` takes each of them. YakumonoHankaku sets every bracket, comma and full stop half width and takes a quarter em off each side of a middle dot; GyomatsuYakumonoHankaku keeps punctuation full width inside a line, sets an opening bracket flush at a line's start and a closing mark half width at its end, and takes the quarter em after a middle dot off at a line's end; GyomatsuYakumonoZenkaku keeps punctuation full width and adds a quarter em between Japanese and Latin text; YakumonoZenkaku sets characters as None does. These widths are VectorCraft's own rules, from the help page's one-line description of each set. Both composers and vertical type measure each line as it is set, the sets save with the document and paragraph styles, and a `.ai` save reports the new sets as left out (`crates/text/src/tests/vertical.rs`, `crates/ui-egui/src/panels/paragraph/tests.rs`). Scores and estimates are unchanged.
|
||||
Interface text in characters the interface's fonts lack, drawn in a bundled Japanese, Georgian or Devanagari face or in an installed font that covers them, sits on the baseline of the Latin text beside it, monospace text included (#5816; `crates/ui-egui/src/ui_fonts.rs`, `crates/ui-egui/src/theme.rs`).
|
||||
Character panel menu › Standard Vertical Roman Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {standardVerticalRoman}`, off by default) stands the selected Latin letters, digits and other half-width characters upright one below the other in vertical type instead of rotating them ([formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); spaces keep their width, tate-chu-yoko blocks stay across the column, and Character Style Options gives it its own checkbox beside Character Alignment.
|
||||
The Glyphs panel menu (with the East Asian options) offers Traditional, Expert, JIS 04, JIS 90, JIS 78 and JIS 83 Forms, Monospaced Full-Width, Half-Width, Third-Width and Quarter-Width Forms, and Revert To Default Forms ([formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); each draws the selected characters with the font's OpenType feature for that form (`trad`, `expt`, `jp04`, `jp90`, `jp78`, `jp83`, `fwid`, `hwid`, `twid`, `qwid`), one form at a time, and a form none of the faces that draw the selected text has a feature for is disabled. `text.setFormat` and `text.setRangeStyle {glyphForm}` set it; the native file, character styles, the canvas, PDF and SVG (`font-feature-settings`) keep it (`tests::typo::glyph_forms_draw_the_fonts_east_asian_forms`, `typecmd::area_tests::glyph_form_is_one_character_attribute_saved_only_when_set`, `panels::glyphs` tests).
|
||||
The Glyphs panel menu (with the East Asian options) offers Traditional, Expert, JIS 04, JIS 90, JIS 78 and JIS 83 Forms, Monospaced Full-Width, Half-Width, Third-Width and Quarter-Width Forms, and Revert To Default Forms ([formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); each draws the selected characters with the font's OpenType feature for that form (`trad`, `expt`, `jp04`, `jp90`, `jp78`, `jp83`, `fwid`, `hwid`, `twid`, `qwid`), one form at a time, and a form none of the faces that draw the selected text has a feature for (in `GSUB`, or for the width forms `GSUB` or `GPOS`) is disabled. A form other than the default takes away a character's glyph choice, and with a width form Proportional Metrics' `palt` is not applied and the font's pair kerning is off. `text.setFormat` and `text.setRangeStyle {glyphForm}` set it; the native file, character styles, the canvas, PDF and SVG (`font-feature-settings`) keep it (`tests::typo::glyph_forms_draw_the_fonts_east_asian_forms`, `typecmd::area_tests::glyph_form_is_one_character_attribute_saved_only_when_set`, `typecmd::area_tests::a_glyph_form_takes_away_the_characters_glyph_choice`, `features::tests::width_forms_turn_off_proportional_metrics`, `panels::glyphs` tests).
|
||||
Character panel menu › Tate-chu-yoko Settings… (with the East Asian options; `ui.tateChuYokoSettings`, `text.setFormat` or `text.setRangeStyle {tcyUpDown, tcyLeftRight}`, in points, 0 by default) moves a marked tate-chu-yoko block in vertical type up by a positive Up/Down and right by a positive Left/Right ([format East Asian characters in vertical text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/format-east-asian-characters-in-vertical-text.html), [formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); the block moves as one piece with its first character's values and keeps its one em down the column, on a vertical path it moves toward the path's start and to the path's left, automatic digit blocks do not move, Reset Panel sets both back to 0, and character styles and the native file keep them.
|
||||
A file whose character styles hold a Tate-chu-yoko Up/Down or Left/Right offset past the coordinate limit (`MAX_COORD`) either way opens with the nearer end, as the type's own offsets do; an offset within it stays as saved.
|
||||
Character panel › Insert Aki Left and Insert Aki Right (with the East Asian options; `text.setFormat` or `text.setRangeStyle {akiLeft, akiRight}`, thousandths of an em from 0 to 1000, 0 by default) add space before and after each selected character, measured as tracking is, and kept as set through justification ([the help page](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); before and after follow the reading order in vertical and right-to-left type, character styles carry both values (Character Style Options › Advanced Character Formats edits them), and a file holding a value outside the range opens with the nearer end.
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| CJK: East Asian Type units (Q, Ha) | done | Preferences › Units › East Asian Type (`unitsAsianType`, Points by default) offers every unit and Q and Ha, a quarter of a millimetre each ([Scripting Guide](https://ai-scripting.docsforadobe.dev/scripting/measurementUnits/): 0.709 pt per Q; [InDesign's unit notation](https://helpx.adobe.com/in/indesign/desktop/layout-and-grid-tools/rulers-and-measure-tools/change-ruler-measurement-units.html): `q`, `h`). The public pages do not say which fields follow it, so no field uses it yet and that waits for a source. Evidence: `q_and_ha_are_a_quarter_millimetre`, `east_asian_type_unit_*` |
|
||||
| CJK: manual tate-chu-yoko | done | Character panel menu › Tate-chu-yoko (`text.setFormat` / `text.setRangeStyle {tateChuYoko}`), with Show East Asian Options: marked characters stand side by side across one em of a vertical column, squeezed to the em when wider; two or three half-width digits still do so without the mark; horizontal type keeps the mark and does not move them. Tate-chu-yoko Settings… (`ui.tateChuYokoSettings`, `{tcyUpDown, tcyLeftRight}` in points) moves a marked block up or down and right or left as one piece. Evidence: `tests_vertical::manual_tate_chu_yoko_sets_marked_characters_across_one_em`, `tests_vertical::tate_chu_yoko_settings_move_a_marked_block_up_down_left_and_right`, `typecmd::tate_chu_yoko_is_a_character_attribute_saved_only_when_on`, `typecmd::tate_chu_yoko_settings_are_character_attributes_saved_only_when_set`, `dialogs::tate_chu_yoko::tests` |
|
||||
| CJK: Standard Vertical Roman Alignment | done | Character panel menu › Standard Vertical Roman Alignment (`text.setFormat` / `text.setRangeStyle {standardVerticalRoman}`), with Show East Asian Options, off by default: in vertical type the marked half-width characters (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. Character Style Options has its own checkbox for it. Evidence: `tests_vertical::standard_vertical_roman_alignment_stands_half_width_characters_upright`, `typecmd::standard_vertical_roman_is_a_character_attribute_saved_only_when_on` |
|
||||
| CJK: East Asian glyph forms | done | Glyphs panel menu › Traditional Forms, Expert Forms, JIS 04, JIS 90, JIS 78 and JIS 83 Forms, Monospaced Full-Width, Half-Width, Third-Width and Quarter-Width Forms, and Revert To Default Forms, with Show East Asian Options ([Format East Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)): the selected characters are drawn with the font's OpenType feature for that form (`trad`, `expt`, `jp04`, `jp90`, `jp78`, `jp83`, `fwid`, `hwid`, `twid`, `qwid`; [feature list](https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist)), one form at a time (`text.setFormat` / `text.setRangeStyle {glyphForm}`); a form none of the faces that draw the selected text (fallback faces included) has a feature for is disabled, and Revert To Default Forms draws the default glyphs. Saved natively only when set, carried by character styles, drawn on the canvas and in PDF, and written to SVG as `font-feature-settings`. Character Style Options has no field for it. Evidence: `tests::typo::glyph_forms_draw_the_fonts_east_asian_forms`, `typecmd::area_tests::glyph_form_is_one_character_attribute_saved_only_when_set`, `panels::glyphs::tests::east_asian_glyph_forms_in_the_panel_menu_set_the_selected_characters_form` |
|
||||
| CJK: East Asian glyph forms | done | Glyphs panel menu › Traditional Forms, Expert Forms, JIS 04, JIS 90, JIS 78 and JIS 83 Forms, Monospaced Full-Width, Half-Width, Third-Width and Quarter-Width Forms, and Revert To Default Forms, with Show East Asian Options ([Format East Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)): the selected characters are drawn with the font's OpenType feature for that form (`trad`, `expt`, `jp04`, `jp90`, `jp78`, `jp83`, `fwid`, `hwid`, `twid`, `qwid`; [feature list](https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist)), one form at a time (`text.setFormat` / `text.setRangeStyle {glyphForm}`); a form none of the faces that draw the selected text (fallback faces included) has a feature for (in `GSUB`, or for the width forms `GSUB` or `GPOS`) is disabled, and Revert To Default Forms draws the default glyphs. A form other than the default takes away a character's glyph choice, and with a width form Proportional Metrics' `palt` is not applied and the font's pair kerning is off. Saved natively only when set, carried by character styles, drawn on the canvas and in PDF, and written to SVG as `font-feature-settings`. Character Style Options has no field for it. Evidence: `tests::typo::glyph_forms_draw_the_fonts_east_asian_forms`, `typecmd::area_tests::glyph_form_is_one_character_attribute_saved_only_when_set`, `typecmd::area_tests::a_glyph_form_takes_away_the_characters_glyph_choice`, `features::tests::width_forms_turn_off_proportional_metrics`, `panels::glyphs::tests::east_asian_glyph_forms_in_the_panel_menu_set_the_selected_characters_form`, `panels::glyphs::tests::glyph_form_items_at_a_caret_follow_all_the_text`, `panels::glyphs::tests::a_glyph_choice_lets_the_current_form_be_chosen_again` |
|
||||
| CJK: warichu | done | Character panel menu › Warichu and Warichu Settings… (`text.setFormat` / `text.setRangeStyle {warichu, warichuLines, warichuGap, warichuScale, warichuAlign}`, `ui.warichuSettings`), with Show East Asian Options ([Format East Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)): marked characters set at a percentage of their size and stacked on 1 to 6 lines inside their line, one above the other in horizontal type and side by side in vertical type, with a line gap and eight alignments; a block stays whole on one line. Saved natively, carried by character styles, drawn on the canvas and in every export (SVG, PDF and `.ai` write such type as outlines). Type on a path sets the characters at their own size. Evidence: `tests::warichu`, `dialogs::warichu::tests`, `typecmd::area_tests::warichu_is_a_character_attribute_set_saved_and_exported` |
|
||||
| CJK: composite fonts | partial | Type › Composite Fonts… (`type.compositeFonts.set`), with Show East Asian Options: a font made of a font per class of character (Kanji, Kana, Punctuation, Symbols, Roman, Numerals), each with its own style, size and baseline in percent of the type's size, Kana, Roman and Numerals with horizontal and vertical scale, and Kana with Scale at Center; New, Delete Font, Import… and Export… (`.vccomposite`, `type.compositeFonts.import` and `.export`) ([Creating composite fonts](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347)). Composite fonts are listed first in the font menus, saved with the document, and their type is laid out per class; SVG and legacy `.ai` write it as outlines. Not yet: custom character sets, the Units menu, Show Sample. Evidence: `text::composite::tests::characters_fall_in_their_classes`, `tests::composite::composite_type_draws_each_class_in_its_font`, `compositefonts::tests`, `dialogs::composite_fonts::tests` |
|
||||
| CJK: ruby | partial | ruby (ふりがな) for live text (`type.ruby`): a reading over a byte range, above (right of the column in vertical type) or below, start/centre/end alignment, size, overhang or squeeze; saved natively, laid out with its line growing, drawn on the canvas, in SVG and PDF (as outlines where ruby is present), in EPS, EMF/WMF and legacy `.ai` (legacy `.ai` writes type with ruby as outlines), and kept by Create Outlines, Expand Appearance and flattening, each glyph where the layout puts it (`typecmd::ruby_tests::create_outlines_keeps_the_ruby_where_it_is_laid_out`) | #633 |
|
||||
|
||||
Reference in New Issue
Block a user