mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 20:23:35 +00:00
Resolve GDEF contour-point ligature carets
This commit is contained in:
@@ -3,6 +3,61 @@ use super::names::{localized, name};
|
||||
use super::traits::{FaceKeys, FontTraits};
|
||||
use super::{Arc, GlyphId, HashMap, Location, LocationRef, MetadataProvider, Size, StringId};
|
||||
|
||||
/// Unhinted TrueType contour points in glyph order, including transformed composite components.
|
||||
/// A cap and depth bound keep damaged fonts from making caret lookup unbounded.
|
||||
fn outline_points(font: &skrifa::FontRef<'_>, gid: GlyphId, depth: usize) -> Option<Vec<(f64, f64)>> {
|
||||
use skrifa::raw::TableProvider;
|
||||
use skrifa::raw::tables::glyf::{Anchor, CompositeGlyphFlags, Glyph};
|
||||
|
||||
const MAX_DEPTH: usize = 16;
|
||||
const MAX_POINTS: usize = 65_536;
|
||||
if depth >= MAX_DEPTH {
|
||||
return None;
|
||||
}
|
||||
let glyph = font.loca(None).ok()?.get(gid, &font.glyf().ok()?)?.into_glyph()?;
|
||||
match glyph {
|
||||
Glyph::Simple(simple) => {
|
||||
if simple.num_points() > MAX_POINTS {
|
||||
return None;
|
||||
}
|
||||
let points: Vec<_> = simple.points().map(|p| (f64::from(p.x), f64::from(p.y))).collect();
|
||||
(points.len() == simple.num_points()).then_some(points)
|
||||
}
|
||||
Glyph::Composite(composite) => {
|
||||
let mut points = Vec::new();
|
||||
for component in composite.components() {
|
||||
let child = outline_points(font, GlyphId::new(component.glyph.to_u32()), depth + 1)?;
|
||||
let t = component.transform;
|
||||
let xx = f64::from(t.xx.to_f32());
|
||||
let xy = f64::from(t.xy.to_f32());
|
||||
let yx = f64::from(t.yx.to_f32());
|
||||
let yy = f64::from(t.yy.to_f32());
|
||||
let transformed: Vec<_> = child.into_iter().map(|(x, y)| (xx * x + xy * y, yx * x + yy * y)).collect();
|
||||
let (dx, dy) = match component.anchor {
|
||||
Anchor::Offset { x, y } => {
|
||||
let (x, y) = (f64::from(x), f64::from(y));
|
||||
if component.flags.contains(CompositeGlyphFlags::SCALED_COMPONENT_OFFSET) {
|
||||
(xx * x + xy * y, yx * x + yy * y)
|
||||
} else {
|
||||
(x, y)
|
||||
}
|
||||
}
|
||||
Anchor::Point { base, component } => {
|
||||
let (bx, by) = *points.get(usize::from(base))?;
|
||||
let (cx, cy) = *transformed.get(usize::from(component))?;
|
||||
(bx - cx, by - cy)
|
||||
}
|
||||
};
|
||||
if transformed.len() > MAX_POINTS.saturating_sub(points.len()) {
|
||||
return None;
|
||||
}
|
||||
points.extend(transformed.into_iter().map(|(x, y)| (x + dx, y + dy)));
|
||||
}
|
||||
Some(points)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The family used when a requested family is unknown (Illustrator's Myriad Pro analogue).
|
||||
pub const FALLBACK_FAMILY: &str = "Source Sans 3";
|
||||
|
||||
@@ -244,11 +299,11 @@ impl FontFace {
|
||||
/// <https://learn.microsoft.com/en-us/typography/opentype/spec/gdef>), one between each two
|
||||
/// characters the glyph stands for, each an x in horizontal text and a y (up from the baseline)
|
||||
/// in vertical text. A coordinate with a variation index (format 3 in a variable font) takes its
|
||||
/// delta from the `GDEF` item variation store at the face's location; a device table's
|
||||
/// adjustments for pixel sizes are not applied. `None` when the font lists no carets for the
|
||||
/// glyph, lists a number other than `count`, gives one by a contour point, or gives one a
|
||||
/// variation index whose delta can't be read.
|
||||
fn ligature_caret_coordinates(&self, gid: u32, count: usize) -> Option<Vec<f64>> {
|
||||
/// delta from the `GDEF` item variation store at the face's location. Format 2 uses the
|
||||
/// unhinted outline point at the font's default variation location. Device-table adjustments
|
||||
/// for pixel sizes are not applied. `None` when the font lists no carets for the glyph, lists
|
||||
/// a number other than `count`, or gives a coordinate that cannot be resolved.
|
||||
fn ligature_caret_coordinates(&self, gid: u32, count: usize, vertical: bool) -> Option<Vec<f64>> {
|
||||
use skrifa::raw::TableProvider;
|
||||
use skrifa::raw::tables::gdef::CaretValue;
|
||||
use skrifa::raw::tables::layout::DeviceOrVariationIndex;
|
||||
@@ -280,17 +335,25 @@ impl FontFace {
|
||||
}
|
||||
}
|
||||
}
|
||||
CaretValue::Format2(_) => None,
|
||||
CaretValue::Format2(v) => {
|
||||
// Raw glyf points have no variation deltas applied. Do not use one at a
|
||||
// non-default location, where it could put the caret in the wrong place.
|
||||
if self.location.coords().iter().any(|c| c.to_f32() != 0.0) {
|
||||
return None;
|
||||
}
|
||||
let point = outline_points(&font, GlyphId::new(gid), 0)?.get(usize::from(v.caret_value_point_index())).copied()?;
|
||||
Some(if vertical { point.1 } else { point.0 })
|
||||
}
|
||||
})
|
||||
.collect::<Option<Vec<f64>>>()?;
|
||||
(!carets.is_empty()).then_some(carets)
|
||||
}
|
||||
/// The caret positions inside ligature glyph `gid` set across, in font units from its left
|
||||
/// edge, left to right: the x coordinates of the font's ligature caret list. `None` when the
|
||||
/// font lists no carets for the glyph, lists a number other than `count`, gives one by a contour
|
||||
/// point, or gives positions that are not increasing inside the glyph's advance.
|
||||
/// font lists no carets for the glyph, lists a number other than `count`, or gives positions
|
||||
/// that are not increasing inside the glyph's advance.
|
||||
pub fn ligature_carets(&self, gid: u32, count: usize) -> Option<Vec<f64>> {
|
||||
let carets = self.ligature_caret_coordinates(gid, count)?;
|
||||
let carets = self.ligature_caret_coordinates(gid, count, false)?;
|
||||
let advance = self.advance(gid);
|
||||
let inside = carets.iter().all(|x| *x > 0.0 && *x < advance) && carets.windows(2).all(|w| w[0] < w[1]);
|
||||
inside.then_some(carets)
|
||||
@@ -298,10 +361,10 @@ impl FontFace {
|
||||
/// The caret positions inside ligature glyph `gid` standing upright in vertical type, in font
|
||||
/// units down from the top of its cell, top to bottom: the y coordinates of the font's ligature
|
||||
/// caret list, for a cell `cell` long whose top is `top` above the baseline. `None` when the
|
||||
/// font lists no carets for the glyph, lists a number other than `count`, gives one by a contour
|
||||
/// point, or gives positions that are not increasing inside the cell.
|
||||
/// font lists no carets for the glyph, lists a number other than `count`, or gives positions
|
||||
/// that are not increasing inside the cell.
|
||||
pub fn vertical_ligature_carets(&self, gid: u32, count: usize, top: f64, cell: f64) -> Option<Vec<f64>> {
|
||||
let carets = self.ligature_caret_coordinates(gid, count)?;
|
||||
let carets = self.ligature_caret_coordinates(gid, count, true)?;
|
||||
let inside = carets.iter().all(|y| *y < top && *y > top - cell) && carets.windows(2).all(|w| w[0] < w[1]);
|
||||
inside.then(|| carets.iter().rev().map(|y| top - y).collect())
|
||||
}
|
||||
|
||||
@@ -19,6 +19,49 @@ fn db_with(font: Option<Vec<u8>>) -> FontDb {
|
||||
db
|
||||
}
|
||||
|
||||
/// Put a contour-point caret on the existing test ligature without changing its outline.
|
||||
fn contour_caret_font(point_index: u16) -> Vec<u8> {
|
||||
use write_fonts::FontBuilder;
|
||||
use write_fonts::tables::gdef::{CaretValue, Gdef, LigCaretList, LigGlyph};
|
||||
use write_fonts::tables::layout::CoverageFormat1 as WriteCoverage;
|
||||
use write_fonts::types::{GlyphId16, Tag};
|
||||
|
||||
let base = ligature_caret_font(None).expect("test font builds");
|
||||
let font = skrifa::FontRef::new(&base).expect("test font parses");
|
||||
let gid = source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph");
|
||||
let mut builder = FontBuilder::new();
|
||||
let list = LigCaretList::new(WriteCoverage::new(vec![GlyphId16::new(gid)]).into(), vec![LigGlyph::new(vec![CaretValue::format_2(point_index)])]);
|
||||
builder.add_table(&Gdef::new(None, None, Some(list), None)).expect("GDEF builds");
|
||||
for record in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&record.tag().to_be_bytes());
|
||||
if !builder.contains(tag) {
|
||||
builder.add_raw(tag, font.table_data(record.tag()).expect("base table reads").as_bytes());
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_contour_point_caret_uses_its_unhinted_x_and_y() {
|
||||
use skrifa::raw::TableProvider;
|
||||
use skrifa::raw::tables::glyf::Glyph;
|
||||
|
||||
let base = ligature_caret_font(None).expect("test font builds");
|
||||
let font = skrifa::FontRef::new(&base).expect("test font parses");
|
||||
let gid = u32::from(source_sans_gid(LIGATURE_CARET_GLYPH).expect("Source Sans 3 has the glyph"));
|
||||
let outline = font.loca(None).expect("loca reads").get(skrifa::GlyphId::new(gid), &font.glyf().expect("glyf reads")).expect("glyph reads");
|
||||
let Glyph::Simple(simple) = outline.into_glyph().expect("glyph has an outline") else { panic!("W is simple") };
|
||||
let (point_index, point) =
|
||||
simple.points().enumerate().find(|(_, p)| p.x > 0 && p.x < 500 && p.y > -500 && p.y < 1000).expect("W has an interior contour point");
|
||||
let db = db_with(Some(contour_caret_font(u16::try_from(point_index).unwrap())));
|
||||
let face = db.face(LIGATURE_CARET_FAMILY, "Regular").unwrap();
|
||||
assert_eq!(face.ligature_carets(gid, 1), Some(vec![f64::from(point.x)]));
|
||||
assert_eq!(face.vertical_ligature_carets(gid, 1, 1000.0, 1500.0), Some(vec![1000.0 - f64::from(point.y)]));
|
||||
|
||||
let bad = db_with(Some(contour_caret_font(u16::MAX)));
|
||||
assert_eq!(bad.face(LIGATURE_CARET_FAMILY, "Regular").unwrap().ligature_carets(gid, 1), None);
|
||||
}
|
||||
|
||||
fn point(text: &str, family: &str) -> TextObject {
|
||||
TextObject::point(Point::ZERO, text, CharStyle { font_family: family.into(), size: 100.0, ..CharStyle::default() })
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (ligature contour-point carets; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
Ligature caret stops use a font's GDEF contour-point positions in horizontal and vertical text. They use unhinted outlines at the default variation location; invalid point indices use the existing evenly divided fallback. Scores and estimates are unchanged.
|
||||
|
||||
Creating a gradient from solid or empty paint uses the document's color model for its default stops, including drawing its vector with the Gradient tool. Explicit `keepModel: true` retains RGB defaults, and existing gradients keep their own colors. Scores and estimates are unchanged.
|
||||
|
||||
**Feature evidence last updated:** 2026-11-03.
|
||||
@@ -21,6 +23,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-03 | minor | Ligature caret stops use GDEF contour-point positions in horizontal and vertical type at the font's default variation location; invalid point indices keep the evenly divided fallback. Scores and estimates unchanged. |
|
||||
| 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. |
|
||||
| 2026-11-03 | minor | Type Tools add-on: Text Tweaker (a VectorCraft addition, #4924): evidence added; scores unchanged. |
|
||||
| 2026-11-03 | minor | Transform previews valid typed values while numeric fields remain focused. Effect dialogs use the object, group, or layer targeted in Appearance. |
|
||||
@@ -775,7 +778,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
While a drag rotates the view, a dial at the center of the canvas shows the angle.
|
||||
- **Editing type:** double-clicking type with a selection tool (Selection or Direct Selection) switches to the Type tool with the caret where it was clicked; pressing Escape finishes editing and switches back to the selection tool used before double-clicking it (when in isolation mode, the first Escape exits text editing while preserving isolation mode, and a second Escape exits isolation).
|
||||
While a Type tool edits type, a click that lands on no type ends the editing without making new type, and that tool stays active; the next click makes new type (#2871, #2854).
|
||||
Inside a ligature (one glyph for several characters, such as fi), the Type tool's caret, clicks, arrow keys and selection stop between its characters, at the font's ligature carets ([OpenType GDEF ligature caret list](https://learn.microsoft.com/en-us/typography/opentype/spec/gdef)) or, when the font does not list one valid caret for each stop, at the glyph's width divided equally.
|
||||
Inside a ligature (one glyph for several characters, such as fi), the Type tool's caret, clicks, arrow keys and selection stop between its characters, at the font's ligature carets ([OpenType GDEF ligature caret list](https://learn.microsoft.com/en-us/typography/opentype/spec/gdef)) or, when the font does not list one valid caret for each stop, at the glyph's width divided equally. Contour-point carets use the font's unhinted outline at its default variation location; pixel-size device adjustments are not applied.
|
||||
Duplicate Artboards, the Artboard tool's Alt-drag and Paste give the new artboard an id that no other artboard has, also when an artboard's id is the largest possible value (#1718). Deleting the copy or the original then leaves the other's guides.
|
||||
- **Touch Type:** a click with the Touch Type tool (Shift+T) selects one character and shows a box around it. Dragging the box or its lower-left handle moves the character. The upper-left handle scales it vertically, the lower-right handle scales it horizontally, the upper-right handle scales it proportionally, and the upper-middle handle rotates it. Each drag is one undo step that sets the character's kerning, baseline shift, scales or rotation, and the type stays editable (`text.touchType`; behavior reference: [Touch Type tool](https://web.archive.org/web/20261007185617/https://helpx.adobe.com/illustrator/using/tool-techniques/touch-type-tool.html), [Add text with the Type, Area Type, and Touch Type tools](https://web.archive.org/web/20260926095922/https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/add-text-illustrator-text-tools.html)).
|
||||
Composition's leading-space reduction stays along the line when a character rotates. An opening bracket trimmed by Line-end Punctuation Half Width Mojikumi keeps that placement under Character Rotation; its turning point follows the composition adjustment, and removing the rotation keeps the trim. Scores and estimates are unchanged.
|
||||
|
||||
Reference in New Issue
Block a user