Part of #394: Highlight Alternate Glyphs paints pink behind OpenType substitutions.

This commit is contained in:
Fabio William Conceição
2026-10-17 12:26:13 +07:00
committed by bflatastic
parent 2d0da1472f
commit 8689863fbf
8 changed files with 92 additions and 18 deletions
+42 -14
View File
@@ -123,6 +123,10 @@ pub struct RenderOptions {
pub mask_view: Option<NodeId>, pub mask_view: Option<NodeId>,
/// Screen view: highlight substituted fonts and glyphs as Document Setup asks. /// Screen view: highlight substituted fonts and glyphs as Document Setup asks.
pub highlight_substitutions: bool, pub highlight_substitutions: bool,
/// Screen view: Preferences › Type › Highlight Alternate Glyphs. Pink behind glyphs OpenType
/// features substituted (ligatures, contextual forms), not Document Setup's missing-glyph
/// highlights.
pub highlight_alternate_glyphs: bool,
/// Edge smoothing (raster export option). /// Edge smoothing (raster export option).
pub anti_alias: AntiAlias, pub anti_alias: AntiAlias,
/// Screen view: placed documents draw from cached bitmaps made in the background (see /// Screen view: placed documents draw from cached bitmaps made in the background (see
@@ -227,6 +231,7 @@ impl Default for RenderOptions {
guide: ([0x4a, 0xd8, 0xff], false), guide: ([0x4a, 0xd8, 0xff], false),
mask_view: None, mask_view: None,
highlight_substitutions: false, highlight_substitutions: false,
highlight_alternate_glyphs: false,
anti_alias: AntiAlias::Art, anti_alias: AntiAlias::Art,
progressive_placed: false, progressive_placed: false,
trace_views: false, trace_views: false,
@@ -1789,8 +1794,11 @@ impl Renderer {
} }
if f.opts.highlight_substitutions { if f.opts.highlight_substitutions {
let setup = &f.doc.setup; let setup = &f.doc.setup;
for (on, path) in [(setup.highlight_substituted_fonts, &g.substituted_fonts), (setup.highlight_substituted_glyphs, &g.substituted_glyphs)] for (on, path) in [
{ (setup.highlight_substituted_fonts, &g.substituted_fonts),
(setup.highlight_substituted_glyphs, &g.substituted_glyphs),
(f.opts.highlight_alternate_glyphs, &g.alternate_glyphs),
] {
if on && !path.elements().is_empty() { if on && !path.elements().is_empty() {
ctx.set_transform(xf); ctx.set_transform(xf);
ctx.set_fill_rule(peniko::Fill::NonZero); ctx.set_fill_rule(peniko::Fill::NonZero);
@@ -2218,6 +2226,9 @@ struct TextGeom {
/// font lacks (drawn from a fallback font): Document Setup's substitution highlights. /// font lacks (drawn from a fallback font): Document Setup's substitution highlights.
substituted_fonts: BezPath, substituted_fonts: BezPath,
substituted_glyphs: BezPath, substituted_glyphs: BezPath,
/// Boxes of glyphs OpenType features substituted (ligatures, contextual forms): Preferences ›
/// Highlight Alternate Glyphs.
alternate_glyphs: BezPath,
/// Inline graphics placed by the layout. /// Inline graphics placed by the layout.
inlines: Vec<vectorcraft_text::InlineGlyph>, inlines: Vec<vectorcraft_text::InlineGlyph>,
} }
@@ -2243,12 +2254,18 @@ impl TextGeom {
/// Approximate heap bytes, for the cache's budget. /// Approximate heap bytes, for the cache's budget.
fn bytes(&self) -> usize { fn bytes(&self) -> usize {
size_of::<Self>() size_of::<Self>()
+ [&self.all, &self.substituted_fonts, &self.substituted_glyphs].into_iter().map(lru::path_bytes).sum::<usize>() + [&self.all, &self.substituted_fonts, &self.substituted_glyphs, &self.alternate_glyphs].into_iter().map(lru::path_bytes).sum::<usize>()
+ self.spans.len() * size_of::<(usize, usize, usize)>() + self.spans.len() * size_of::<(usize, usize, usize)>()
+ self.inlines.len() * size_of::<vectorcraft_text::InlineGlyph>() + self.inlines.len() * size_of::<vectorcraft_text::InlineGlyph>()
} }
} }
/// OpenType-substituted form: a ligature (several source characters → one glyph) or a glyph whose
/// id is not the face's cmap mapping for its first character (contextual / stylistic alternate).
pub(crate) fn is_opentype_alternate(nominal_gid: u32, shaped_gid: u32, cluster_chars: usize) -> bool {
shaped_gid != 0 && (cluster_chars > 1 || shaped_gid != nominal_gid)
}
fn text_geom(t: &TextObject) -> TextGeom { fn text_geom(t: &TextObject) -> TextGeom {
text_geom_snapped(t, None) text_geom_snapped(t, None)
} }
@@ -2288,30 +2305,39 @@ fn text_geom_snapped(t: &TextObject, snap: Option<Affine>) -> TextGeom {
None => (true, None), None => (true, None),
}) })
.collect(); .collect();
let (mut substituted_fonts, mut substituted_glyphs) = (BezPath::new(), BezPath::new()); let (mut substituted_fonts, mut substituted_glyphs, mut alternate_glyphs) = (BezPath::new(), BezPath::new(), BezPath::new());
let plain = t.plain_text();
for g in &layout.glyphs { for g in &layout.glyphs {
push(&mut all, g.run, &g.outline); push(&mut all, g.run, &g.outline);
let Some(&(missing, face)) = faces.get(g.run) else { continue }; let Some(&(missing, face)) = faces.get(g.run) else { continue };
let target = if missing {
&mut substituted_fonts
} else if face.is_some_and(|f| f != g.font_id) {
&mut substituted_glyphs
} else {
continue;
};
let Some(line) = layout.lines.get(g.line) else { continue }; let Some(line) = layout.lines.get(g.line) else { continue };
let mut cell = Rect::new(g.origin.x, g.origin.y - line.ascent, g.origin.x + g.advance, g.origin.y + line.descent).to_path(0.1); let mut cell = Rect::new(g.origin.x, g.origin.y - line.ascent, g.origin.x + g.advance, g.origin.y + line.descent).to_path(0.1);
cell.apply_affine(Affine::rotate_about(g.angle, g.origin)); cell.apply_affine(Affine::rotate_about(g.angle, g.origin));
target.extend(cell.iter()); if missing {
substituted_fonts.extend(cell.iter());
continue;
}
if face.is_some_and(|f| f != g.font_id) {
substituted_glyphs.extend(cell.iter());
continue;
}
// Same face: OpenType may have swapped the glyph (liga, calt, …).
let Some(cluster) = plain.get(g.byte..g.byte.saturating_add(g.len)) else { continue };
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) {
alternate_glyphs.extend(cell.iter());
}
} }
// Underline and strikethrough bars, painted as their run's type is (#847). // Underline and strikethrough bars, painted as their run's type is (#847).
for (run, bar) in vectorcraft_text::decorations(&layout, db, t) { for (run, bar) in vectorcraft_text::decorations(&layout, db, t) {
push(&mut all, run, &bar); push(&mut all, run, &bar);
} }
TextGeom { all, spans, bounds: layout.bounds, substituted_fonts, substituted_glyphs, inlines: layout.inlines } TextGeom { all, spans, bounds: layout.bounds, substituted_fonts, substituted_glyphs, alternate_glyphs, inlines: layout.inlines }
} }
/// Document Setup's highlight behind substituted fonts and glyphs (screen only). /// Document Setup's and Preferences › Highlight Alternate Glyphs pink (screen only).
const SUBSTITUTED: peniko::Color = peniko::Color::from_rgba8(255, 120, 190, 110); const SUBSTITUTED: peniko::Color = peniko::Color::from_rgba8(255, 120, 190, 110);
/// The pixel size of `region` rendered at `scale` pixels per point (at least 1×1). /// The pixel size of `region` rendered at `scale` pixels per point (at least 1×1).
pub fn region_pixels(region: Rect, scale: f64) -> (u32, u32) { pub fn region_pixels(region: Rect, scale: f64) -> (u32, u32) {
@@ -2367,6 +2393,8 @@ mod tests;
#[cfg(test)] #[cfg(test)]
mod tests_adjust; mod tests_adjust;
#[cfg(test)] #[cfg(test)]
mod tests_alternate;
#[cfg(test)]
mod tests_blend; mod tests_blend;
#[cfg(test)] #[cfg(test)]
mod tests_charstroke; mod tests_charstroke;
+37
View File
@@ -0,0 +1,37 @@
//! Preferences › Type › Highlight Alternate Glyphs (#394): OpenType substitutions get a pink
//! cell behind them on screen when the preference is on.
use super::*;
#[test]
fn is_opentype_alternate_marks_ligatures_and_forms() {
assert!(is_opentype_alternate(100, 100, 2), "ligature cluster");
assert!(is_opentype_alternate(50, 99, 1), "contextual / stylistic form");
assert!(!is_opentype_alternate(50, 50, 1), "nominal cmap glyph");
assert!(!is_opentype_alternate(50, 0, 2), "missing glyph");
}
#[test]
fn highlight_alternate_glyphs_paints_pink_behind_a_ligature() {
use vectorcraft_doc::text::{CharStyle, TextObject};
use vectorcraft_doc::{Node, NodeKind};
use vectorcraft_geom::Point;
// Source Serif 4 ships with standard ligatures for "fi".
let mut d = Document::new(200.0, 100.0);
let style = CharStyle { size: 48.0, font_family: "Source Serif 4".into(), ..Default::default() };
let mut n = Node::new(NodeId(0), NodeKind::Text(Box::new(TextObject::point(Point::new(20.0, 70.0), "fi", style))));
n.id = d.alloc_id();
let l = d.layers[0].id;
d.insert(Some(l), 0, n).unwrap();
let off = RenderOptions { background: Some([255, 255, 255, 255]), highlight_substitutions: true, highlight_alternate_glyphs: false, ..Default::default() };
let on = RenderOptions { highlight_alternate_glyphs: true, ..off.clone() };
let mut r = Renderer::new();
let without = r.render(&d, 200, 100, Affine::IDENTITY, &off);
let with = r.render(&d, 200, 100, Affine::IDENTITY, &on);
assert_ne!(without.pixels, with.pixels, "the preference changes the frame");
// Pink SUBSTITUTED = (255, 120, 190) at alpha 110, blended over white / ink.
let pinkish = with.pixels.chunks(4).filter(|p| p[0] > 200 && p[1] < 180 && p[2] > 150 && p[2] < 230).count();
assert!(pinkish > 10, "pink highlight cells behind the ligature ({pinkish} pixels)");
}
+3
View File
@@ -302,6 +302,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
hidden: vec![], hidden: vec![],
rot: v.rotation, rot: v.rotation,
anti_alias: app.session.prefs.anti_aliased_artwork, anti_alias: app.session.prefs.anti_aliased_artwork,
highlight_alternate_glyphs: app.session.prefs.highlight_alternate_glyphs,
placed: vectorcraft_render::placed_document::generation(), placed: vectorcraft_render::placed_document::generation(),
pixel, pixel,
smooth_images, smooth_images,
@@ -354,6 +355,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
guide: key.guide, guide: key.guide,
mask_view, mask_view,
highlight_substitutions: true, highlight_substitutions: true,
// Type › Highlight Alternate Glyphs (#394): pink behind OpenType substitutions.
highlight_alternate_glyphs: app.session.prefs.highlight_alternate_glyphs,
// General › Anti-aliased Artwork: off, edges are hard on screen (raster effects and // General › Anti-aliased Artwork: off, edges are hard on screen (raster effects and
// pattern tiles stay smooth), as in Illustrator. // pattern tiles stay smooth), as in Illustrator.
anti_alias: if app.session.prefs.anti_aliased_artwork { vectorcraft_render::AntiAlias::Art } else { vectorcraft_render::AntiAlias::None }, anti_alias: if app.session.prefs.anti_aliased_artwork { vectorcraft_render::AntiAlias::Art } else { vectorcraft_render::AntiAlias::None },
+2
View File
@@ -332,6 +332,8 @@ pub struct CacheKey {
pub rot: f64, pub rot: f64,
/// General › Anti-aliased Artwork. /// General › Anti-aliased Artwork.
pub anti_alias: bool, pub anti_alias: bool,
/// Type › Highlight Alternate Glyphs (#394).
pub highlight_alternate_glyphs: bool,
/// [`vectorcraft_render::placed_document::generation`]: placed documents' bitmaps made since. /// [`vectorcraft_render::placed_document::generation`]: placed documents' bitmaps made since.
pub placed: u64, pub placed: u64,
/// View › Pixel Preview: the document pixels rendered (x0, y0, x1, y1), one per point, shown /// View › Pixel Preview: the document pixels rendered (x0, y0, x1, y1), one per point, shown
+1 -1
View File
@@ -193,8 +193,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
## G12 Preferences and panels that don't act yet ## G12 Preferences and panels that don't act yet
- **Missing:** ~14 Preferences options stored but not acted on (#394: Open
- **Done:** quick Image Trace presets reveal Threshold and Advanced controls (Paths, Corners, Noise); Properties can reopen them. Other context-specific Properties gaps remain. - **Done:** quick Image Trace presets reveal Threshold and Advanced controls (Paths, Corners, Noise); Properties can reopen them. Other context-specific Properties gaps remain.
- **Missing:** ~15 Preferences options stored but not acted on (#394: Highlight Alternate Glyphs, Open - **Missing:** ~15 Preferences options stored but not acted on (#394: Highlight Alternate Glyphs, Open
Documents As Tabs, Real-time Drawing, hyphenation language, devices…), Properties panel per Documents As Tabs, Real-time Drawing, hyphenation language, devices…), Properties panel per
context, context menus beyond the canvas, Layers panel row menus and Collect for Export, SVG Interactivity. context, context menus beyond the canvas, Layers panel row menus and Collect for Export, SVG Interactivity.
+4 -2
View File
@@ -1974,9 +1974,11 @@ their own Anti-aliasing option),
`scrubNumericFields` (on: a horizontal drag on a numeric field's label steps the field, one undo step per drag; the `scrubNumericFields` (on: a horizontal drag on a numeric field's label steps the field, one undo step per drag; the
control channel's `ui.drag` scrubs), `showHomeScreen` (on by default: the Home screen while no document is open; control channel's `ui.drag` scrubs), `showHomeScreen` (on by default: the Home screen while no document is open;
off, an empty window, and the Home button or `app.home` still shows the screen), `autoCollapseIconPanels` (off by off, an empty window, and the Home button or `app.home` still shows the screen), `autoCollapseIconPanels` (off by
default: on, a click away from a panel popped out of the icon column puts it away) and `displayPrintSize` (off by default: on, a click away from a panel popped out of the icon column puts it away), `displayPrintSize` (off by
default: View › Actual Size / `view.actualSize` is one document point per screen point; on, one document inch fills default: View › Actual Size / `view.actualSize` is one document point per screen point; on, one document inch fills
96 screen points, an inch at the system's reference density whatever the display scaling). `blackOnScreen` 96 screen points, an inch at the system's reference density whatever the display scaling). `highlightAlternateGlyphs` (on by default: the canvas paints a pink cell behind glyphs OpenType features
substituted — ligatures, contextual forms — on screen; Document Setup's Highlight Substituted Fonts / Glyphs stay
separate). `blackOnScreen`
(Appearance of Black › On Screen, `accurate` by default) sets how the canvas shows pure CMYK black, black ink alone (Appearance of Black › On Screen, `accurate` by default) sets how the canvas shows pure CMYK black, black ink alone
at 100%: `accurate` shows it through the CMYK profile, a dark grey, and `rich` shows it as RGB black. Rich blacks, at 100%: `accurate` shows it through the CMYK profile, a dark grey, and `rich` shows it as RGB black. Rich blacks,
black tints and images show the same either way, and exports are unchanged. `blackOutput` (Appearance of Black › black tints and images show the same either way, and exports are unchanged. `blackOutput` (Appearance of Black ›
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -25,7 +25,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 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. | | 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. | | 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 unused (#394); 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) paints pink behind OpenType substitutions on screen; 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; font matching by any name; Find Font | done | #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) | | Font menu: samples, live preview, filters, favourites; font matching by any name; Find Font | done | #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 | | 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 | | 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 |