Type: stylistic sets, alternates, titling, figure styles, superscript and subscript

A character style's OpenType features took nine tags. OtFeatures now
also keeps ss01-ss20, salt, titl, pnum, lnum, sups and subs (off by
default, one bit each in `extra`): they are accepted by the type
commands, stored with the style and passed to the shaper, so a font
that has them uses them.

The Affinity import test used ss01 as its example of a feature kept but
not rendered; it now uses cv01 for that (character variants still
aren't rendered) and keeps ss01 as one that is.

Fixes #1607
This commit is contained in:
Gert Pellin authored and bflatastic committed 2026-10-13 13:36:40 +07:00
1 parent 504e719230
commit eb25a85d90
4 files changed
+54 -5

No files matched your search

+6 -1
View File
@@ -230,7 +230,12 @@ pub(crate) fn features_param(p: &Value, cmd: &str) -> Result<Option<Vec<String>>
let Some(v) = p.get("features").filter(|v| !v.is_null()) else { return Ok(None) };
let tags: Vec<&str> = v.as_array().ok_or_else(|| bad(cmd, "features must be a list of tags"))?.iter().filter_map(Value::as_str).collect();
if let Some(t) = tags.iter().find(|t| !vectorcraft_text::OtFeatures::known_tag(t)) {
return Err(bad(cmd, format!("unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh; prefix - to turn off)")));
return Err(bad(
cmd,
format!(
"unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs; prefix - to turn off)"
),
));
}
Ok(Some(vectorcraft_text::OtFeatures::default().with_tags(tags).to_tags()))
}
+3 -1
View File
@@ -159,6 +159,8 @@ fn caps_and_feature_overrides_pass_through_to_editable_type() {
vec![(tag(b"Feat"), F::U32(u32::from_be_bytes(*b"CAP\x01"))), (tag(b"Valu"), F::I32(1))],
vec![(tag(b"Feat"), F::U32(u32::from_be_bytes(*b"liga"))), (tag(b"Valu"), F::I32(0))],
vec![(tag(b"Feat"), F::U32(u32::from_be_bytes(*b"ss01"))), (tag(b"Valu"), F::I32(1))],
// Character variants aren't rendered yet (stylistic sets are, #1607).
vec![(tag(b"Feat"), F::U32(u32::from_be_bytes(*b"cv01"))), (tag(b"Valu"), F::I32(1))],
],
),
)],
@@ -169,7 +171,7 @@ fn caps_and_feature_overrides_pass_through_to_editable_type() {
let t = texts(&loaded.doc)[0];
assert_eq!(t.runs[0].text, "Mixed");
assert!(t.runs[0].style.all_caps);
assert_eq!(t.runs[0].style.features, ["-liga", "ss01"]);
assert_eq!(t.runs[0].style.features, ["-liga", "ss01", "cv01"]);
assert!(loaded.warnings.iter().any(|w| w.contains("retained but not rendered")));
let restored = vectorcraft_format::load(&vectorcraft_format::save_file(&loaded.doc)).unwrap();
assert_eq!(texts(&restored)[0], t);
+44 -2
View File
@@ -41,8 +41,19 @@ pub struct OtFeatures {
pub ordinals: bool,
/// Swashes (`swsh`).
pub swash: bool,
/// The features of [`EXTRA_FEATURES`] turned on, one bit each (off by default): stylistic
/// sets, alternates, titling, proportional and lining figures, superscript and subscript.
pub extra: u32,
}
/// Features beyond the named fields, off unless asked for, each bit `i` of [`OtFeatures::extra`]:
/// stylistic sets `ss01`–`ss20`, stylistic alternates `salt`, titling `titl`, proportional `pnum`
/// and lining `lnum` figures, superscript `sups` and subscript `subs`.
pub const EXTRA_FEATURES: [&str; 26] = [
"ss01", "ss02", "ss03", "ss04", "ss05", "ss06", "ss07", "ss08", "ss09", "ss10", "ss11", "ss12", "ss13", "ss14", "ss15", "ss16", "ss17", "ss18",
"ss19", "ss20", "salt", "titl", "pnum", "lnum", "sups", "subs",
];
impl Default for OtFeatures {
fn default() -> Self {
Self {
@@ -56,6 +67,7 @@ impl Default for OtFeatures {
tabular_figures: false,
ordinals: false,
swash: false,
extra: 0,
}
}
}
@@ -89,13 +101,14 @@ impl OtFeatures {
v.push(if on { tag.to_string() } else { format!("-{tag}") });
}
}
v.extend(EXTRA_FEATURES.iter().enumerate().filter(|(i, _)| self.extra & (1 << i) != 0).map(|(_, t)| t.to_string()));
v
}
/// Is `tag` (optionally prefixed with `-` or `+`) one this set understands?
pub fn known_tag(tag: &str) -> bool {
let t = tag.trim_start_matches(['-', '+']);
matches!(t, "liga" | "calt" | "dlig" | "smcp" | "frac" | "onum" | "tnum" | "ordn" | "swsh")
matches!(t, "liga" | "calt" | "dlig" | "smcp" | "frac" | "onum" | "tnum" | "ordn" | "swsh") || EXTRA_FEATURES.contains(&t)
}
/// This set with `tags` applied on top.
@@ -116,7 +129,15 @@ impl OtFeatures {
"tnum" => o.tabular_figures = on,
"ordn" => o.ordinals = on,
"swsh" => o.swash = on,
_ => {}
_ => {
if let Some(i) = EXTRA_FEATURES.iter().position(|e| *e == t) {
if on {
o.extra |= 1 << i;
} else {
o.extra &= !(1 << i);
}
}
}
}
}
self
@@ -155,6 +176,11 @@ impl OtFeatures {
v.push(f(tag, true));
}
}
for (i, tag) in EXTRA_FEATURES.iter().enumerate() {
if let (true, Ok(t)) = (s.extra & (1 << i) != 0, <[u8; 4]>::try_from(tag.as_bytes())) {
v.push(f(&t, true));
}
}
if self.vertical {
// Not `vrt2`: its glyphs are already turned on their side, and the layout turns Latin
// and digits itself (they would lie upside down, and tate-chu-yoko break).
@@ -230,6 +256,22 @@ pub fn explicit_ligatures<S: AsRef<str>>(features: &[S]) -> bool {
#[cfg(test)]
mod tests {
use super::*;
/// #1607: stylistic sets, alternates, titling, figure styles and superscript/subscript are
/// known, round-trip through the stored tags and reach the shaper.
#[test]
fn extra_features_are_kept_and_shaped() {
for t in ["ss01", "ss20", "-ss07", "salt", "titl", "pnum", "lnum", "sups", "subs"] {
assert!(OtFeatures::known_tag(t), "{t}");
}
assert!(!OtFeatures::known_tag("ss21"));
let o = OtFeatures::from_tags(["ss03", "pnum", "sups", "-liga"]);
assert_eq!(o.to_tags(), vec!["-liga", "ss03", "pnum", "sups"]);
assert_eq!(OtFeatures::from_tags(["ss03", "-ss03"]).to_tags(), Vec::<String>::new(), "turned off again");
let st = CharStyle { features: vec!["ss05".into(), "titl".into()], ..CharStyle::default() };
let tags: Vec<String> = OtFeatures::default().resolve(&st).iter().filter(|f| f.value == 1).map(|f| f.tag.to_string()).collect();
assert!(tags.contains(&"ss05".to_string()) && tags.contains(&"titl".to_string()), "{tags:?}");
}
use crate::{FontDb, layout};
use vectorcraft_doc::TextObject;
use vectorcraft_geom::Point;
+1 -1
View File
@@ -26,7 +26,7 @@ pub mod test_fonts;
pub mod thread;
pub use craft_fonts::{CRAFT_FONTS, CraftFont, WEB_FONTS, WebFont};
pub use features::{LIGATURE_TRACKING_LIMITS, OtFeatures, explicit_ligatures, ligatures_suppressed_by};
pub use features::{EXTRA_FEATURES, LIGATURE_TRACKING_LIMITS, OtFeatures, explicit_ligatures, ligatures_suppressed_by};
pub use fontdb::{
FALLBACK_FAMILY, FontClass, FontDb, FontFace, FontMatch, FontTraits, IcfMargins, PlatformFontFiles, WantedFont, WantedFonts, app_font_dir,
is_font_file, is_suitcase, set_app_font_dir, set_platform_font_files, set_user_font_dirs, style_weight, system_font_dirs, user_font_dirs,