mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #4786 from mockydevs/fix/lam-alef-review-followup
Type: lam-alef highlight follow-up: optional features, all marks, exact ligature test (#394)
This commit is contained in:
Generated
+1
@@ -6383,6 +6383,7 @@ dependencies = [
|
||||
"psd",
|
||||
"serde_json",
|
||||
"tiff",
|
||||
"unicode-general-category",
|
||||
"vectorcraft-brush",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
|
||||
@@ -27,6 +27,8 @@ weezl = "0.2"
|
||||
# PNG rows read one at a time (MIT OR Apache-2.0; already used by the image crate's PNG reader).
|
||||
png = "0.18"
|
||||
log = { workspace = true }
|
||||
# Combining marks inside a lam-alef cluster (MIT OR Apache-2.0; already used by vectorcraft-text).
|
||||
unicode-general-category = "1"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
vello_cpu = { workspace = true, features = ["multithreading"] }
|
||||
|
||||
@@ -136,17 +136,49 @@ fn lam_alef_is_not_an_alternate() {
|
||||
/// feature. Built in code, so it runs whatever fonts the machine has.
|
||||
#[test]
|
||||
fn lam_alef_in_liga_is_not_an_alternate() {
|
||||
use vectorcraft_text::test_fonts::{lam_alef_family, lam_alef_font};
|
||||
use vectorcraft_text::test_fonts::{lam_alef_family, lam_alef_font, source_sans_gid};
|
||||
let _adding = crate::tests_fontchange::adding_fonts();
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let gid = |c| u32::from(source_sans_gid(c).unwrap());
|
||||
// Text, the ligature glyph and its cluster (byte, length): lam + alef, lam + alef with hamza
|
||||
// above, the pair after a beh, and the pair with a fatha on the lam (the ligature skips it).
|
||||
let cases = [
|
||||
("\u{0644}\u{0627}", 'H', (0, 4)),
|
||||
("\u{0644}\u{0623}", 'K', (0, 4)),
|
||||
("\u{0628}\u{0644}\u{0627}", 'H', (2, 4)),
|
||||
("\u{0644}\u{064E}\u{0627}", 'H', (0, 6)),
|
||||
];
|
||||
for feature in [b"liga", b"rlig"] {
|
||||
db.add_font(lam_alef_font(feature).unwrap());
|
||||
let style = vectorcraft_doc::text::CharStyle { size: 36.0, font_family: lam_alef_family(feature).into(), ..Default::default() };
|
||||
for s in ["\u{0644}\u{0627}", "\u{0644}\u{0623}", "\u{0628}\u{0644}\u{0627}", "\u{0644}\u{064E}\u{0627}"] {
|
||||
let family = lam_alef_family(feature);
|
||||
let style = vectorcraft_doc::text::CharStyle { size: 36.0, font_family: family.into(), ..Default::default() };
|
||||
for (s, ligature, (byte, len)) in cases {
|
||||
let t = text(s, style.clone());
|
||||
// The font's ligature glyph stands for the pair.
|
||||
assert!(glyphs(&t).iter().any(|g| g.1 > 2), "{}: {s} is shaped to one ligature glyph", lam_alef_family(feature));
|
||||
assert_eq!(alternates(&t), 0, "{}: {s}", lam_alef_family(feature));
|
||||
// The font's ligature glyph stands for the pair, its cluster lam to alef.
|
||||
let shaped = glyphs(&t);
|
||||
assert!(shaped.contains(&(byte, len, gid(ligature))), "{family}: {s} is shaped to {ligature} over {byte}..{}: {shaped:?}", byte + len);
|
||||
assert_eq!(alternates(&t), 0, "{family}: {s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #394: an optional feature that changes the lam-alef ligature (here `salt`) is highlighted:
|
||||
/// only the required ligature itself is left clear, whichever feature the font files it under.
|
||||
#[test]
|
||||
fn an_optional_feature_on_the_lam_alef_ligature_is_an_alternate() {
|
||||
use vectorcraft_text::test_fonts::{LAM_ALEF_SALT, lam_alef_family, lam_alef_font, source_sans_gid};
|
||||
let _adding = crate::tests_fontchange::adding_fonts();
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let salted = u32::from(source_sans_gid(LAM_ALEF_SALT).unwrap());
|
||||
for feature in [b"liga", b"rlig"] {
|
||||
db.add_font(lam_alef_font(feature).unwrap());
|
||||
let family = lam_alef_family(feature);
|
||||
let style = vectorcraft_doc::text::CharStyle { size: 36.0, font_family: family.into(), features: vec!["salt".into()], ..Default::default() };
|
||||
for (s, byte) in [("\u{0644}\u{0627}", 0), ("\u{0628}\u{0644}\u{0627}", 2)] {
|
||||
let t = text(s, style.clone());
|
||||
let shaped = glyphs(&t);
|
||||
assert!(shaped.contains(&(byte, 4, salted)), "{family}: salt turns the ligature into {LAM_ALEF_SALT}: {shaped:?}");
|
||||
assert_eq!(alternates(&t), 1, "{family}: {s}: the salted ligature, and nothing else");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,11 +186,36 @@ fn lam_alef_in_liga_is_not_an_alternate() {
|
||||
/// Only a lam followed by an alef (marks between or after allowed) is the required lam-alef.
|
||||
#[test]
|
||||
fn only_lam_alef_clusters_are_the_required_ligature() {
|
||||
for s in ["\u{0644}\u{0627}", "\u{0644}\u{0622}", "\u{0644}\u{0623}", "\u{0644}\u{0625}", "\u{0644}\u{064E}\u{0627}", "\u{0644}\u{0627}\u{064B}"]
|
||||
{
|
||||
// Marks of the Arabic block, Arabic Extended-A (U+08E4, U+08F0), Extended-B (U+0898) and
|
||||
// Extended-C (U+10EFD), and Quranic marks (U+0610, U+06ED).
|
||||
for s in [
|
||||
"\u{0644}\u{0627}",
|
||||
"\u{0644}\u{0622}",
|
||||
"\u{0644}\u{0623}",
|
||||
"\u{0644}\u{0625}",
|
||||
"\u{0644}\u{064E}\u{0627}",
|
||||
"\u{0644}\u{0627}\u{064B}",
|
||||
"\u{0644}\u{08E4}\u{0627}",
|
||||
"\u{0644}\u{08F0}\u{0627}",
|
||||
"\u{0644}\u{0898}\u{0627}",
|
||||
"\u{0644}\u{0627}\u{10EFD}",
|
||||
"\u{0644}\u{0610}\u{06ED}\u{0627}",
|
||||
] {
|
||||
assert!(crate::text_geom::is_lam_alef(s), "{s:?}");
|
||||
}
|
||||
for s in ["", "\u{0644}", "\u{0627}\u{0644}", "\u{0644}\u{0628}", "\u{0628}\u{0644}\u{0627}", "\u{0644}\u{0627}\u{0627}", "fi", "\u{064E}"] {
|
||||
// Not marks: tatweel (U+0640) and the number mark above (U+08E2, a format character).
|
||||
for s in [
|
||||
"",
|
||||
"\u{0644}",
|
||||
"\u{0627}\u{0644}",
|
||||
"\u{0644}\u{0628}",
|
||||
"\u{0628}\u{0644}\u{0627}",
|
||||
"\u{0644}\u{0627}\u{0627}",
|
||||
"fi",
|
||||
"\u{064E}",
|
||||
"\u{0644}\u{0640}\u{0627}",
|
||||
"\u{0644}\u{08E2}\u{0627}",
|
||||
] {
|
||||
assert!(!crate::text_geom::is_lam_alef(s), "{s:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,17 +48,28 @@ impl TextGeom {
|
||||
/// The boxes of the glyphs of `t` (the text this geometry is of) that optional OpenType features
|
||||
/// chose (Preferences › Highlight Alternate Glyphs): candidates the text's required shaping
|
||||
/// alone doesn't give ([`required_shaping`]). Arabic letters joining, Indic conjuncts and the
|
||||
/// lam-alef ligature are what the script needs, not alternates (the lam-alef ligature also when
|
||||
/// a font files it under `liga`: [`is_lam_alef`]). Measured once.
|
||||
/// lam-alef ligature are what the script needs, not alternates. A font may file the lam-alef
|
||||
/// ligature under `liga`, which required shaping turns off, so a lam-alef cluster
|
||||
/// ([`is_lam_alef`]) is also compared with its shaping with `liga` left on: an optional feature
|
||||
/// that changes its glyph beyond that (a stylistic alternate…) is still highlighted. Measured
|
||||
/// once.
|
||||
pub(crate) fn alternate_glyphs(&self, t: &TextObject) -> &BezPath {
|
||||
self.alternate_glyphs.get_or_init(|| {
|
||||
let mut out = BezPath::new();
|
||||
if self.alternate_candidates.is_empty() {
|
||||
return out;
|
||||
}
|
||||
let required = required_shaping(t);
|
||||
let required = required_shaping(t, &["-liga", "-calt"]);
|
||||
let plain = t.plain_text();
|
||||
let in_lam_alef = |c: &AlternateCandidate| plain.get(c.byte..c.byte.saturating_add(c.len)).is_some_and(is_lam_alef);
|
||||
// Laid out only when the text has a lam-alef candidate.
|
||||
let with_liga = self.alternate_candidates.iter().any(in_lam_alef).then(|| required_shaping(t, &["-calt"]));
|
||||
let gives = |map: &std::collections::HashMap<(usize, usize), Vec<(u32, u32)>>, c: &AlternateCandidate| {
|
||||
map.get(&(c.byte, c.len)).is_some_and(|glyphs| glyphs.contains(&(c.font_id, c.gid)))
|
||||
};
|
||||
for c in &self.alternate_candidates {
|
||||
if !required.get(&(c.byte, c.len)).is_some_and(|glyphs| glyphs.contains(&(c.font_id, c.gid))) {
|
||||
let lam_alef_ligature = || in_lam_alef(c) && with_liga.as_ref().is_some_and(|m| gives(m, c));
|
||||
if !gives(&required, c) && !lam_alef_ligature() {
|
||||
out.extend(c.cell.iter());
|
||||
}
|
||||
}
|
||||
@@ -86,12 +97,14 @@ pub(crate) fn is_opentype_alternate(nominal_gid: u32, shaped_gid: u32, cluster_c
|
||||
}
|
||||
|
||||
/// Whether `cluster` is a lam followed by an alef (plain, with madda, or with hamza above or below),
|
||||
/// with any Arabic marks between or after them: the lam-alef ligature, which the Unicode
|
||||
/// Standard's Arabic chapter treats as obligatory, not a stylistic choice. Fonts made before
|
||||
/// `rlig` was common put it in `liga`, which [`required_shaping`] turns off, so it is required
|
||||
/// by its characters whatever feature the font files it under.
|
||||
/// with any combining marks (Unicode general category M: the Arabic harakat, Quranic marks and
|
||||
/// those of Arabic Extended-A, -B and -C) between or after them: the lam-alef ligature, which the
|
||||
/// Unicode Standard's Arabic chapter treats as obligatory, not a stylistic choice. Fonts made
|
||||
/// before `rlig` was common put it in `liga`, which [`required_shaping`] turns off, so
|
||||
/// [`TextGeom::alternate_glyphs`] also accepts its glyph from shaping with `liga` on.
|
||||
pub(crate) fn is_lam_alef(cluster: &str) -> bool {
|
||||
let is_mark = |c: &char| matches!(c, '\u{0610}'..='\u{061A}' | '\u{064B}'..='\u{065F}' | '\u{0670}' | '\u{06D6}'..='\u{06DC}' | '\u{06DF}'..='\u{06E4}' | '\u{06E7}' | '\u{06E8}' | '\u{06EA}'..='\u{06ED}');
|
||||
use unicode_general_category::{GeneralCategory as G, get_general_category};
|
||||
let is_mark = |c: &char| matches!(get_general_category(*c), G::NonspacingMark | G::SpacingMark | G::EnclosingMark);
|
||||
let mut letters = cluster.chars().filter(|c| !is_mark(c));
|
||||
matches!((letters.next(), letters.next(), letters.next()), (Some('\u{0644}'), Some('\u{0622}' | '\u{0623}' | '\u{0625}' | '\u{0627}'), None))
|
||||
}
|
||||
@@ -108,14 +121,16 @@ pub(crate) struct AlternateCandidate {
|
||||
}
|
||||
|
||||
/// The glyphs `t` is shaped to by required shaping alone, by cluster (`byte`, `len`) → `(font,
|
||||
/// glyph)`: laid out again with every optional feature off (standard and contextual ligatures and
|
||||
/// alternates, and the features its characters turn on: stylistic sets, swashes, discretionary
|
||||
/// ligatures, figure styles…). What is left is what the script needs: `ccmp`, `locl`, `rlig`,
|
||||
/// Arabic's `init`, `medi`, `fina` and `isol`, the Indic and other complex scripts' required sets.
|
||||
fn required_shaping(t: &TextObject) -> std::collections::HashMap<(usize, usize), Vec<(u32, u32)>> {
|
||||
/// glyph)`: laid out again with every optional feature off (with `features` `-liga` and `-calt`:
|
||||
/// standard and contextual ligatures and alternates, and the features its characters turn on:
|
||||
/// stylistic sets, swashes, discretionary ligatures, figure styles…). What is left is what the
|
||||
/// script needs: `ccmp`, `locl`, `rlig`, Arabic's `init`, `medi`, `fina` and `isol`, the Indic
|
||||
/// and other complex scripts' required sets. With `features` `-calt` alone, standard ligatures
|
||||
/// stay on (an older font's lam-alef).
|
||||
fn required_shaping(t: &TextObject, features: &[&str]) -> std::collections::HashMap<(usize, usize), Vec<(u32, u32)>> {
|
||||
let mut plain = t.clone();
|
||||
for run in &mut plain.runs {
|
||||
run.style.features = vec!["-liga".into(), "-calt".into()];
|
||||
run.style.features = features.iter().map(|f| (*f).into()).collect();
|
||||
}
|
||||
let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &plain);
|
||||
let mut glyphs: std::collections::HashMap<(usize, usize), Vec<(u32, u32)>> = std::collections::HashMap::new();
|
||||
@@ -193,7 +208,7 @@ pub(crate) fn text_geom_snapped(t: &TextObject, snap: Option<Affine>) -> TextGeo
|
||||
let cluster_chars = cluster.chars().count();
|
||||
let Some(first) = cluster.chars().next() else { continue };
|
||||
let nominal = db.face_by_id(g.font_id).map(|f| f.glyph_for(first)).unwrap_or(0);
|
||||
if is_opentype_alternate(nominal, g.gid, cluster_chars) && !is_lam_alef(cluster) {
|
||||
if is_opentype_alternate(nominal, g.gid, cluster_chars) {
|
||||
alternate_candidates.push(AlternateCandidate { byte: g.byte, len: g.len, font_id: g.font_id, gid: g.gid, cell });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,21 +352,29 @@ pub fn lam_alef_family(feature: &[u8; 4]) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The glyph [`lam_alef_font`]'s `salt` gives its lam + alef ligature (H): M.
|
||||
pub const LAM_ALEF_SALT: char = 'M';
|
||||
|
||||
/// A test-only Arabic cmap over bundled Source Sans 3: lam uses l, alef I, alef with hamza above
|
||||
/// J and beh b. The `GSUB` table's `arab` script has one ligature lookup, under `feature`, that
|
||||
/// joins lam + alef into H and lam + alef with hamza above into K. Older fonts put the mandatory
|
||||
/// lam-alef ligature in `liga` rather than `rlig`; this builds either, without an OS font. `None`
|
||||
/// when it can't be built.
|
||||
/// J, beh b and fatha the combining acute (a mark in the font's `GDEF`). The `GSUB` table's `arab`
|
||||
/// script has one ligature lookup, under `feature`, that skips marks and joins lam + alef into H
|
||||
/// and lam + alef with hamza above into K, and a `salt` lookup that turns H into
|
||||
/// [`LAM_ALEF_SALT`]. Older fonts put the mandatory lam-alef ligature in `liga` rather than
|
||||
/// `rlig`; this builds either, without an OS font. `None` when it can't be built.
|
||||
pub fn lam_alef_font(feature: &[u8; 4]) -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::cmap::Cmap;
|
||||
use write_fonts::tables::gsub::{Gsub, Ligature, LigatureSet, LigatureSubstFormat1, SubstitutionLookup, SubstitutionLookupList};
|
||||
use write_fonts::tables::layout::{CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, Script, ScriptList, ScriptRecord};
|
||||
use write_fonts::tables::gsub::{
|
||||
Gsub, Ligature, LigatureSet, LigatureSubstFormat1, SingleSubstFormat2, SubstitutionLookup, SubstitutionLookupList,
|
||||
};
|
||||
use write_fonts::tables::layout::{
|
||||
CoverageFormat1, Feature, FeatureList, FeatureRecord, LangSys, Lookup, LookupFlag, Script, ScriptList, ScriptRecord,
|
||||
};
|
||||
use write_fonts::types::GlyphId16;
|
||||
|
||||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||||
let charmap = font.charmap();
|
||||
let mapping = [('\u{0644}', 'l'), ('\u{0627}', 'I'), ('\u{0623}', 'J'), ('\u{0628}', 'b')];
|
||||
let mapping = [('\u{0644}', 'l'), ('\u{0627}', 'I'), ('\u{0623}', 'J'), ('\u{0628}', 'b'), ('\u{064E}', '\u{0301}')];
|
||||
let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
|
||||
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
|
||||
let family = lam_alef_family(feature);
|
||||
@@ -380,9 +388,17 @@ pub fn lam_alef_font(feature: &[u8; 4]) -> Option<Vec<u8>> {
|
||||
let coverage = CoverageFormat1::new(vec![gid('l')?]);
|
||||
let ligatures = vec![Ligature::new(gid('H')?, vec![gid('I')?]), Ligature::new(gid('K')?, vec![gid('J')?])];
|
||||
let subst = LigatureSubstFormat1::new(coverage.into(), vec![LigatureSet::new(ligatures)]);
|
||||
let features = FeatureList::new(vec![FeatureRecord::new(Tag::new(feature), Feature::new(None, vec![0]))]);
|
||||
let lookups = SubstitutionLookupList::new(vec![SubstitutionLookup::Ligature(Lookup::new(Default::default(), vec![subst]))]);
|
||||
let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"arab"), Script::new(Some(LangSys::new(vec![0])), vec![]))]);
|
||||
let salt = SingleSubstFormat2::new(CoverageFormat1::new(vec![gid('H')?]).into(), vec![gid(LAM_ALEF_SALT)?]);
|
||||
// Features sorted by tag, as OpenType requires: `liga` / `rlig`, then `salt`.
|
||||
let features = FeatureList::new(vec![
|
||||
FeatureRecord::new(Tag::new(feature), Feature::new(None, vec![0])),
|
||||
FeatureRecord::new(Tag::new(b"salt"), Feature::new(None, vec![1])),
|
||||
]);
|
||||
let lookups = SubstitutionLookupList::new(vec![
|
||||
SubstitutionLookup::Ligature(Lookup::new(LookupFlag::IGNORE_MARKS, vec![subst])),
|
||||
SubstitutionLookup::Single(Lookup::new(Default::default(), vec![salt.into()])),
|
||||
]);
|
||||
let scripts = ScriptList::new(vec![ScriptRecord::new(Tag::new(b"arab"), Script::new(Some(LangSys::new(vec![0, 1])), vec![]))]);
|
||||
builder.add_table(&Gsub::new(scripts, features, lookups)).ok()?;
|
||||
for r in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Type parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Highlight Alternate Glyphs leaves the required lam-alef ligature unhighlighted also in older Arabic fonts (such as legacy Arial) that put it in `liga` rather than `rlig` (#394); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Highlight Alternate Glyphs: an optional feature (such as `salt`) that changes a lam-alef ligature is highlighted again, only the required ligature staying clear, and a lam-alef with any combining mark (Arabic Extended-A/B/C included) counts as the ligature (#394); scores unchanged) · **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
|
||||
@@ -28,7 +28,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Character and Paragraph panels, styles with overrides | done | Character-style Auto-leading resets, partial definitions, local leading overrides, override markers, Undo/Redo and native serialization have regression coverage; differing paragraph alignments leave the shared alignment buttons unselected (headless regressions, full integration CI and visual review pass); mixed-value display for other paragraph fields remains missing |
|
||||
| Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. |
|
||||
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
|
||||
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference (#394, off by default) paints pink behind OpenType substitutions on screen when on (required shaping such as Arabic joining forms and the lam-alef ligature, including a lam-alef older fonts file under `liga`, is not highlighted); stylistic sets ss01–ss20, stylistic and titling alternates, proportional and lining figures, superscript and subscript are character-style features (`text.setStyle {features}`, #1607) |
|
||||
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference (#394, off by default) paints pink behind OpenType substitutions on screen when on (required shaping such as Arabic joining forms and the lam-alef ligature, including a lam-alef older fonts file under `liga`, is not highlighted; an optional feature that changes the ligature is); stylistic sets ss01–ss20, stylistic and titling alternates, proportional and lining figures, superscript and subscript are character-style features (`text.setStyle {features}`, #1607) |
|
||||
| Font menu: samples, live preview, filters, favourites, recent families; font matching by any name; Find Font | done | #3515: recently applied families appear first, once each; missing families are omitted and the existing Number of Recent Fonts setting applies; #339; script filter: Japanese, Latin, Thai; the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux) |
|
||||
| Missing glyphs set in an installed font made for their script (Thai, Lao, Khmer, Myanmar first; then any font that has them) | done | Thai: Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai, TLWG |
|
||||
| Full font-name matching (OpenType name ID 4) | done | #1258: loaded/cataloged faces and Missing Fonts searches; localized names and variable default faces; Windows font-service QA pending |
|
||||
@@ -71,6 +71,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | Highlight Alternate Glyphs: an optional feature (such as `salt`) that changes a lam-alef ligature is highlighted again, only the required ligature staying clear, and a lam-alef with any combining mark (Arabic Extended-A/B/C included) counts as the ligature (#394); scores unchanged |
|
||||
| 2026-10-30 | minor | Highlight Alternate Glyphs leaves the required lam-alef ligature unhighlighted also in older Arabic fonts (such as legacy Arial) that put it in `liga` rather than `rlig` (#394); scores unchanged |
|
||||
| 2026-10-30 | minor | Character panel menu › Show Snap to Glyph Options shows the Snap to Glyph section (Baseline, x-height, Glyph Bounds, Proximity Guides, Angular Guides, Anchor Point; `view.snapToGlyph.options`) (#4232); scores unchanged |
|
||||
| 2026-10-29 | minor | Font fallback keeps a letter and its combining marks in one face; Vietnamese stacked diacritics (Ể, Ứ, Ự) attach in precomposed, decomposed and keyboard forms (#4491); scores and hours unchanged |
|
||||
|
||||
Reference in New Issue
Block a user