collective

Browse / github.com/storytold/wordcraft / patch

deps: harfrust 0.15 (split from the cargo-dependencies group)

deps: harfrust 0.15 (split from the cargo-dependencies group) Splits the harfrust bump out of the red cargo-dependencies group PR (#4418), which fails to compile because harfrust 0.15 replaced `FontRef`, `ShaperData`, `ShaperInstance` and `UnicodeBuffer`. - `harfrust` 0.13 → 0.15 (`Cargo.toml`, `Cargo.lock`). - `FontFace` holds one `harfrust::Font` (variable-font named instances built with `instance_builder().variations(..)`) instead of `ShaperData` plus `ShaperInstance`; `FontFace::hb()` is gone (no other caller). - `shape_dir` shapes with `ShaperFont`, `Buffer` and `harfrust::shape`; a run that fails to shape falls through to the cmap fallback as befo

Status
open
Score
0.00
Adopters
0 other operators
Author
Harrison-Booth (imported from pull request #4582, open)
Base
4b8b06fe03f93c9fc81783ee3e5b01977be89efc
Size
+73 −72 in 5 files
Review
pending
Shared
2026-10-28
Id
76f5c802a6a3384f40a77896

How it went

No other operator has reported using it yet.

Diff

raw · Cargo.lock Cargo.toml crates/fonts/src/fontdb/face.rs crates/fonts/src/fontdb/names.rs crates/fonts/src/lib.rs

diff --git a/Cargo.lock b/Cargo.lock--- a/Cargo.lock+++ b/Cargo.lock@@ -1567,7 +1567,7 @@ dependencies = [  "ecolor",  "emath",  "epaint_default_fonts",- "font-types 0.12.6",+ "font-types 0.12.7",  "harfrust 0.12.0",  "log",  "nohash-hasher",@@ -1793,9 +1793,9 @@ dependencies = [  [[package]] name = "font-types"-version = "0.12.6"+version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274"+checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d" dependencies = [  "bytemuck", ]@@ -2170,15 +2170,15 @@ dependencies = [  [[package]] name = "harfrust"-version = "0.13.3"+version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "948d0741125ba89cd3e1c23e5642415b6ade7e1d29d67ba25fb925b533e989d6"+checksum = "865dcc4105d51c323d8fedbb2d75b5e9c8ecddf5613f74c82f851d0afbc97114" dependencies = [  "bitflags 2.13.2",  "bytemuck",- "once_cell",- "read-fonts 0.43.3",+ "read-fonts 0.46.0",  "smallvec",+ "yoke", ]  [[package]]@@ -4192,30 +4192,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [  "bytemuck",- "font-types 0.12.6",+ "font-types 0.12.7",  "once_cell", ]  [[package]] name = "read-fonts"-version = "0.43.3"+version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "005c8acf251756c478b0bf402885bfd88a1476020c4c7e6060edc1aa68da38ea"+checksum = "d5a0cf4bb6cbce4c29756270d189a5e400ac05e91192161d83a0bcb6e68492e8" dependencies = [  "bytemuck",- "font-types 0.12.6",+ "font-types 0.12.7",  "once_cell", ]  [[package]] name = "read-fonts"-version = "0.44.0"+version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index"-checksum = "d5a0cf4bb6cbce4c29756270d189a5e400ac05e91192161d83a0bcb6e68492e8"+checksum = "bdaa4fffe9ff3ca2c353fa9729792805b9b3e324695ff5a081f860612e81b330" dependencies = [  "bytemuck",- "font-types 0.12.6",+ "foldhash 0.2.0",+ "font-types 0.12.7",  "once_cell",+ "smallvec",+ "yoke", ]  [[package]]@@ -6788,7 +6791,7 @@ name = "wordcraft-fonts" version = "0.11.0" dependencies = [  "brotli-decompressor",- "harfrust 0.13.3",+ "harfrust 0.15.0",  "kurbo",  "log",  "skrifa 0.47.0",diff --git a/Cargo.toml b/Cargo.toml--- a/Cargo.toml+++ b/Cargo.toml@@ -48,7 +48,7 @@ regex-automata = { version = "0.4", default-features = false, features = ["std",  # text / rendering / formats skrifa = "0.47"-harfrust = "0.13"+harfrust = "0.15" # WOFF / WOFF2 web fonts added by the user (#582): pure Rust (MIT); its brotli and zlib decoders # (brotli-decompressor, flate2) are already in the tree. wuff = "0.2.9"diff --git a/crates/fonts/src/fontdb/face.rs b/crates/fonts/src/fontdb/face.rs--- a/crates/fonts/src/fontdb/face.rs+++ b/crates/fonts/src/fontdb/face.rs@@ -64,12 +64,12 @@ pub struct FontFace {     /// values).     pub cap_height: f64,     pub x_height: f64,-    pub shaper: harfrust::ShaperData,-    /// Variable fonts: the named instance's axis settings (user units; empty = default instance),-    /// the normalised location and the shaper's view of it.+    /// The shaper's view of the face, at the named instance's location for variable fonts.+    pub hb: harfrust::Font,+    /// Variable fonts: the named instance's axis settings (user units; empty = default instance)+    /// and the normalised location.     pub coords: Vec<([u8; 4], f32)>,     pub(super) location: Location,-    pub instance: Option<harfrust::ShaperInstance>,     /// Basic Multilingual Plane coverage bitset, built on first use.     pub(super) bmp: std::sync::OnceLock<Box<[u64]>>,     // Some legacy CJK fonts assemble their contours with TrueType instructions.@@ -185,9 +185,6 @@ impl FontFace {     pub fn skrifa(&self) -> Option<skrifa::FontRef<'_>> {         skrifa::FontRef::from_index(self.data(), self.index).ok()     }-    pub fn hb(&self) -> Option<harfrust::FontRef<'_>> {-        harfrust::FontRef::from_index(self.data(), self.index).ok()-    }     /// Where in the design space glyphs, metrics and outlines come from.     pub fn location(&self) -> LocationRef<'_> {         (&self.location).into()diff --git a/crates/fonts/src/fontdb/names.rs b/crates/fonts/src/fontdb/names.rs--- a/crates/fonts/src/fontdb/names.rs+++ b/crates/fonts/src/fontdb/names.rs@@ -352,10 +352,12 @@ pub(super) fn make_face(     let location = if coords.is_empty() { Location::default() } else { f.axes().location(settings.iter().copied()) };     let m = f.metrics(Size::unscaled(), &location);     let a = f.attributes();-    let hb = harfrust::FontRef::from_index(data, index).ok()?;-    let shaper = harfrust::ShaperData::new(&hb);-    let instance = (!coords.is_empty())-        .then(|| harfrust::ShaperInstance::from_variations(&hb, settings.iter().map(|(t, v)| harfrust::Variation { tag: *t, value: *v })));+    let source: harfrust::font::Source = match &bytes {+        FontBytes::Static(b) => (*b).into(),+        FontBytes::Owned(v) => (v.clone() as Arc<dyn AsRef<[u8]> + Send + Sync>).into(),+    };+    let hb = harfrust::Font::new(source, index)?;+    let hb = if coords.is_empty() { hb } else { hb.instance_builder().variations(coords.iter().map(|(t, v)| (harfrust::Tag::new(t), *v))).build() };     let axis = |tag: &[u8; 4]| coords.iter().find(|(t, _)| t == tag).map(|(_, v)| *v);     // Old releases of heavy faces (Arial Black in the core fonts) claim weight 400 where current     // ones say 900, which Word doesn't embolden: a family named for its weight is that heavy.@@ -379,10 +381,9 @@ pub(super) fn make_face(         descent: -(m.descent as f64),         cap_height: m.cap_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.72),         x_height: m.x_height.map(|v| v as f64).filter(|v| *v > 0.0).unwrap_or(m.ascent as f64 * 0.5),-        shaper,+        hb,         coords,         location,-        instance,         bytes,         index,         bmp: std::sync::OnceLock::new(),diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs--- a/crates/fonts/src/lib.rs+++ b/crates/fonts/src/lib.rs@@ -19,7 +19,7 @@ pub use fontdb::{     base_style, bundled, decode_font_file, is_font_file_name, name_language_matches, sniff, system_font_dirs, }; pub use harfrust::Feature;-use harfrust::{Direction, ShapeOptions, ShapePlan, ShapePlanKey, Shaper, Tag, UnicodeBuffer};+use harfrust::{Buffer, Direction, ShapeOptions, ShapePlan, ShapePlanKey, ShaperFont, Tag}; pub use kurbo::BezPath; pub use script::{fallback_rank, is_emoji, needs_layout_tables, script_fallbacks}; use skrifa::MetadataProvider;@@ -208,35 +208,35 @@ pub fn shape_run(face: &FontFace, text: &str, features: &[Feature], map: impl Fn  fn shape_dir(face: &FontFace, text: &str, features: &[Feature], map: &impl Fn(char) -> char, rtl: bool) -> Vec<ShapedGlyph> {     let mut out = Vec::with_capacity(text.len());-    let shaped = face.hb().map(|hb| {-        let shaper = face.shaper.shaper(&hb).instance(face.instance.as_ref()).build();-        // One script per shaping call: the shaper picks its script engine (Myanmar, Khmer,-        // Indic, USE reordering…) from the run's script, so "Hi မြန်မာ" in one face must not be-        // shaped as Latin. Runs come in text order; right-to-left callers regroup by cluster.-        for r in script::script_runs(text) {-            let Some(sub) = text.get(r.clone()) else { continue };-            let mut buf = UnicodeBuffer::new();-            for (i, c) in sub.char_indices() {-                buf.add(map(c), (r.start + i) as u32);-            }-            buf.guess_segment_properties();-            buf.set_direction(if rtl { Direction::RightToLeft } else { Direction::LeftToRight });-            let plan = shape_plan(face, &shaper, &buf, features);-            let gb = shaper.shape(buf, ShapeOptions::new().plan(plan.as_deref()).features(features));-            for (info, pos) in gb.glyph_infos().iter().zip(gb.glyph_positions()) {-                out.push(ShapedGlyph {-                    gid: info.glyph_id,-                    cluster: info.cluster as usize,-                    x_advance: pos.x_advance,-                    x_offset: pos.x_offset,-                    y_offset: pos.y_offset,-                });-            }+    let sf = ShaperFont::new(&face.hb);+    // One script per shaping call: the shaper picks its script engine (Myanmar, Khmer,+    // Indic, USE reordering…) from the run's script, so "Hi မြန်မာ" in one face must not be+    // shaped as Latin. Runs come in text order; right-to-left callers regroup by cluster.+    let mut shaped = false;+    for r in script::script_runs(text) {+        let Some(sub) = text.get(r.clone()) else { continue };+        let mut buf = Buffer::new();+        for (i, c) in sub.char_indices() {+            buf.push(map(c) as u32, (r.start + i) as u32);         }-    });-    if shaped.is_none()-        && let Some(f) = face.skrifa()-    {+        buf.guess_segment_properties();+        buf.set_direction(if rtl { Direction::RightToLeft } else { Direction::LeftToRight });+        let plan = shape_plan(face, &buf, features);+        if harfrust::shape(&sf, &mut buf, ShapeOptions::new().plan(plan.as_deref()).features(features)).is_err() {+            continue;+        }+        shaped = true;+        for (info, pos) in buf.glyph_infos().iter().zip(buf.glyph_positions()) {+            out.push(ShapedGlyph {+                gid: info.glyph_id,+                cluster: info.cluster as usize,+                x_advance: pos.x_advance,+                x_offset: pos.x_offset,+                y_offset: pos.y_offset,+            });+        }+    }+    if !shaped && let Some(f) = face.skrifa() {         let cmap = f.charmap();         let gm = f.glyph_metrics(Size::unscaled(), face.location());         for (i, c) in text.char_indices() {@@ -281,22 +281,22 @@ thread_local! { /// oldest dropped past [`MAX_SHAPE_PLANS`]). A kept plan is used only for the same face and when /// harfrust's own [`ShapePlanKey::matches`] accepts it (script, direction, language, the face's /// variation instance and the features). `None` leaves making it to the shaper.-fn shape_plan(face: &FontFace, shaper: &Shaper, buf: &UnicodeBuffer, features: &[Feature]) -> Option<std::rc::Rc<ShapePlan>> {+fn shape_plan(face: &FontFace, buf: &Buffer, features: &[Feature]) -> Option<std::rc::Rc<ShapePlan>> {     // The buffer's script is unset when it reads as unknown: the guess never sets that.-    let script = Some(buf.script()).filter(|s| *s != harfrust::script::UNKNOWN);+    let script = buf.script().filter(|s| *s != harfrust::Script::UNKNOWN);     let language = buf.language();     let direction = buf.direction();     if direction == Direction::Invalid {         return None;     }-    let key = ShapePlanKey::new(script, direction).language(language.as_ref()).instance(face.instance.as_ref()).features(features);+    let key = ShapePlanKey::new(&face.hb, script, direction).language(language).features(features);     let of = PlanFace::of(face);     SHAPE_PLANS.with(|cell| {         let mut plans = cell.try_borrow_mut().ok()?;         if let Some((_, plan)) = plans.iter().find(|(f, plan)| *f == of && key.matches(plan)) {             return Some(plan.clone());         }-        let plan = std::rc::Rc::new(ShapePlan::new(shaper, direction, script, language.as_ref(), features));+        let plan = std::rc::Rc::new(ShapePlan::new(&face.hb, direction, script, language, features));         if plans.len() >= MAX_SHAPE_PLANS {             plans.remove(0);         }@@ -361,14 +361,14 @@ mod tests {     #[test]     fn kept_shape_plans_shape_as_fresh_ones() {         let fresh = |face: &FontFace, text: &str, features: &[Feature], rtl: bool| -> Vec<(u32, u32, i32, i32, i32)> {-            let Some(hb) = face.hb() else { return Vec::new() };-            let shaper = face.shaper.shaper(&hb).instance(face.instance.as_ref()).build();-            let mut buf = UnicodeBuffer::new();+            let mut buf = Buffer::new();             buf.push_str(text);             buf.guess_segment_properties();             buf.set_direction(if rtl { Direction::RightToLeft } else { Direction::LeftToRight });-            let gb = shaper.shape(buf, ShapeOptions::new().features(features));-            gb.glyph_infos().iter().zip(gb.glyph_positions()).map(|(i, p)| (i.glyph_id, i.cluster, p.x_advance, p.x_offset, p.y_offset)).collect()+            if harfrust::shape(&ShaperFont::new(&face.hb), &mut buf, ShapeOptions::new().features(features)).is_err() {+                return Vec::new();+            }+            buf.glyph_infos().iter().zip(buf.glyph_positions()).map(|(i, p)| (i.glyph_id, i.cluster, p.x_advance, p.x_offset, p.y_offset)).collect()         };         let kept = |face: &FontFace, text: &str, features: &[Feature], rtl: bool| -> Vec<(u32, u32, i32, i32, i32)> {             shape_dir(face, text, features, &|c| c, rtl).iter().map(|g| (g.gid, g.cluster as u32, g.x_advance, g.x_offset, g.y_offset)).collect()@@ -394,14 +394,14 @@ mod tests {     fn kept_shape_plans_are_bounded() {         let face = FontDb::global().face(DEFAULT_FAMILY, "Regular");         let fresh = |features: &[Feature], rtl: bool| -> Vec<(u32, i32)> {-            let Some(hb) = face.hb() else { return Vec::new() };-            let shaper = face.shaper.shaper(&hb).instance(face.instance.as_ref()).build();-            let mut buf = UnicodeBuffer::new();+            let mut buf = Buffer::new();             buf.push_str("office");             buf.guess_segment_properties();             buf.set_direction(if rtl { Direction::RightToLeft } else { Direction::LeftToRight });-            let gb = shaper.shape(buf, ShapeOptions::new().features(features));-            gb.glyph_infos().iter().zip(gb.glyph_positions()).map(|(i, p)| (i.glyph_id, p.x_advance)).collect()+            if harfrust::shape(&ShaperFont::new(&face.hb), &mut buf, ShapeOptions::new().features(features)).is_err() {+                return Vec::new();+            }+            buf.glyph_infos().iter().zip(buf.glyph_positions()).map(|(i, p)| (i.glyph_id, p.x_advance)).collect()         };         for n in 0..3 * MAX_SHAPE_PLANS as u32 {             let features = [feature_value(b"kern", n % 2), feature_value(b"liga", n)];