mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
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:
1 parent
504e719230
commit
eb25a85d90
4 files changed
+54
-5
No files matched your search
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user