From 4df6c26dbf523fe5665c79b605e19bedb3fa0706 Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 15 Oct 2025 10:30:44 +0000 Subject: [PATCH 1/6] Add messaging machinery --- harfrust/src/buffer.rs | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/harfrust/src/buffer.rs b/harfrust/src/buffer.rs index 2e36ff15..ab211f4d 100644 --- a/harfrust/src/buffer.rs +++ b/harfrust/src/buffer.rs @@ -471,6 +471,8 @@ pub enum ContentType { /// This is the unified buffer type, matching HarfBuzz's `hb_buffer_t`. It /// carries a [`ContentType`] describing which of the two states it is /// currently in. +pub type MessageFunction = fn(buffer: &Buffer, font: &crate::ShaperFont<'_, '_>, message: &str) -> bool; + pub struct Buffer { // Information about how the text in the buffer should be treated. pub(crate) flags: BufferFlags, @@ -516,6 +518,8 @@ pub struct Buffer { pub(crate) max_len: usize, /// Maximum allowed operations. pub(crate) max_ops: i32, + pub(crate) message_depth: usize, + pub(crate) message_function: Option, } impl Buffer { @@ -548,6 +552,8 @@ impl Buffer { context_len: [0, 0], digest: SetDigest::new(), glyph_set: U32Set::default(), + message_depth: 0, + message_function: None, } } @@ -1275,6 +1281,24 @@ impl Buffer { true } + pub(crate) fn sync_so_far(&mut self) -> usize { + let had_output = self.have_output; + let out_i = self.out_len; + let i = self.idx; + let old_idx = self.idx; + if self.sync() { + self.idx = out_i; + } else { + self.idx = i; + } + if had_output { + self.have_output = true; + self.out_len = self.idx; + } + debug_assert!(self.idx <= self.len); + self.idx - old_idx + } + pub(crate) fn clear_output(&mut self) { self.have_output = true; self.have_positions = false; @@ -2120,6 +2144,31 @@ impl Buffer { lig_id } + #[inline] + pub fn set_message_function(&mut self, func: MessageFunction) { + #[cfg(feature = "std")] + { + self.message_function = Some(func); + } + } + + #[inline] + pub(crate) fn message(&mut self, font: &crate::ShaperFont<'_, '_>, msg: &str) -> bool { + if let Some(func) = self.message_function { + self.message_depth += 1; + let ret = func(self, font, msg); + self.message_depth -= 1; + ret + } else { + true + } + } + + #[inline] + pub(crate) fn messaging(&self) -> bool { + self.message_function.is_some() + } + pub(crate) fn serialize_impl( &self, font: Option<&crate::ShaperFont<'_, '_>>, @@ -2214,6 +2263,47 @@ impl core::fmt::Debug for Buffer { } } +macro_rules! message { + ($ctx:expr, $($arg:tt)*) => { + if $ctx.buffer.messaging() { + let msg = format!($($arg)*); + $ctx.buffer.message($ctx.font, &msg); + } + }; +} + +macro_rules! message_sync { + ($ctx:expr, $($arg:tt)*) => { + if $ctx.buffer.messaging() { + $ctx.buffer.sync_so_far(); + let msg = format!($($arg)*); + $ctx.buffer.message($ctx.font, &msg); + } + }; +} + +macro_rules! message_return { + ($ctx:expr, $($arg:tt)*) => { + if $ctx.buffer.messaging() { + let msg = format!($($arg)*); + if !$ctx.buffer.message($ctx.font, &msg) { + return; + } + } + }; +} + +macro_rules! message_continue { + ($ctx:expr, $($arg:tt)*) => { + if $ctx.buffer.messaging() { + let msg = format!($($arg)*); + if !$ctx.buffer.message($ctx.font, &msg) { + continue; + } + } + }; +} + pub(crate) fn _cluster_group_func(a: &GlyphInfo, b: &GlyphInfo) -> bool { a.cluster == b.cluster } From dfee656501d0627906840e12458cab9f5e93e46a Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 15 Oct 2025 14:09:40 +0000 Subject: [PATCH 2/6] Add messaging --- harfrust/Cargo.toml | 2 + harfrust/src/aat/common.rs | 15 +- harfrust/src/aat/kern.rs | 11 +- harfrust/src/aat/kerx.rs | 11 +- harfrust/src/aat/layout.rs | 11 +- harfrust/src/aat/morx.rs | 11 +- harfrust/src/buffer.rs | 158 ++++++++++++++-------- harfrust/src/fallback.rs | 18 ++- harfrust/src/normalize.rs | 3 +- harfrust/src/ot/apply.rs | 26 +++- harfrust/src/ot/gpos/cursive.rs | 4 + harfrust/src/ot/gpos/mark.rs | 2 + harfrust/src/ot/gpos/mod.rs | 4 + harfrust/src/ot/gpos/pair.rs | 8 +- harfrust/src/ot/gpos/single.rs | 4 + harfrust/src/ot/gsub/alternate.rs | 11 ++ harfrust/src/ot/gsub/ligature.rs | 32 +++++ harfrust/src/ot/gsub/mod.rs | 4 + harfrust/src/ot/gsub/multiple.rs | 46 ++++++- harfrust/src/ot/gsub/reverse_chain.rs | 11 +- harfrust/src/ot/gsub/single.rs | 20 +++ harfrust/src/ot/layout.rs | 21 ++- harfrust/src/ot/shaper/arabic_fallback.rs | 2 +- harfrust/src/ot/shaper/indic.rs | 11 +- harfrust/src/ot/shaper/khmer.rs | 5 + harfrust/src/ot/shaper/myanmar.rs | 2 + harfrust/src/ot/shaper/syllabic.rs | 10 ++ harfrust/src/ot/shaper/use_.rs | 2 + harfrust/src/shape.rs | 19 ++- 29 files changed, 381 insertions(+), 103 deletions(-) diff --git a/harfrust/Cargo.toml b/harfrust/Cargo.toml index 4b252bfd..3b26bfbc 100644 --- a/harfrust/Cargo.toml +++ b/harfrust/Cargo.toml @@ -26,6 +26,8 @@ default = ["std"] std = ["read-fonts/std"] libm = ["dep:core_maths", "dep:spin", "read-fonts/libm"] icu = ["dep:icu_properties", "dep:icu_normalizer"] +# Compile shaping messages and buffer callbacks; disabled by default. +tracing = [] [dev-dependencies] harfrust = { path = "../harfrust" } diff --git a/harfrust/src/aat/common.rs b/harfrust/src/aat/common.rs index 09f6328d..deec7669 100644 --- a/harfrust/src/aat/common.rs +++ b/harfrust/src/aat/common.rs @@ -37,10 +37,14 @@ pub(crate) fn get_class( /// /// See #[doc(alias = "hb_aat_apply_context_t")] -pub struct AatApplyContext<'a> { +pub struct AatApplyContext<'a, 'f, 'c> { pub plan: &'a ShapePlan, pub layout: LayoutData<'a>, pub scale: Scale, + #[cfg(feature = "tracing")] + pub font: &'a crate::ShaperFont<'f, 'c>, + #[cfg(not(feature = "tracing"))] + font_lifetimes: core::marker::PhantomData<(&'f (), &'c ())>, pub buffer: &'a mut Buffer, pub has_glyph_classes: bool, pub range_flags: Option<&'a [RangeFlags]>, @@ -55,18 +59,23 @@ pub struct AatApplyContext<'a> { pub(crate) safe_to_break: SafeToBreak<'a>, } -impl<'a> AatApplyContext<'a> { +impl<'a, 'f, 'c> AatApplyContext<'a, 'f, 'c> { pub fn new( plan: &'a ShapePlan, layout: LayoutData<'a>, scale: Scale, buffer: &'a mut Buffer, + #[cfg(feature = "tracing")] font: &'a crate::ShaperFont<'f, 'c>, ) -> Self { Self { plan, layout, scale, + #[cfg(not(feature = "tracing"))] + font_lifetimes: core::marker::PhantomData, buffer, + #[cfg(feature = "tracing")] + font, has_glyph_classes: layout.ot.has_glyph_classes(), range_flags: None, subtable_flags: 0, @@ -745,7 +754,7 @@ mod tests { buffer.next_glyph(); let mut context = - AatApplyContext::new(&plan, shaper_font.layout(), Scale::default(), &mut buffer); + AatApplyContext::new(&plan, shaper_font.layout(), Scale::default(), &mut buffer, #[cfg(feature = "tracing")] &shaper_font); context.output_glyph(DELETED_GLYPH); assert_eq!(context.buffer.out_len, 2); diff --git a/harfrust/src/aat/kern.rs b/harfrust/src/aat/kern.rs index 0d728a0a..1a96abb9 100644 --- a/harfrust/src/aat/kern.rs +++ b/harfrust/src/aat/kern.rs @@ -39,8 +39,11 @@ pub fn apply( layout: LayoutData<'_>, scale: Scale, buffer: &mut Buffer, + #[cfg(feature = "tracing")] font: &crate::ShaperFont<'_, '_>, ) -> Option<()> { - let mut c = AatApplyContext::new(plan, layout, scale, buffer); + let mut c = AatApplyContext::new(plan, layout, scale, buffer, #[cfg(feature = "tracing")] font); + + if !buffer_message!(c.buffer, font, "start table kern") { return None; } c.setup_buffer_glyph_set(); @@ -124,6 +127,7 @@ pub fn apply( if c.buffer_is_reversed { c.reverse_buffer(); } + buffer_message!(c.buffer, font, "end table kern"); Some(()) } @@ -131,6 +135,7 @@ fn machine_kern( layout: LayoutData<'_>, scale: Scale, buffer: &mut Buffer, + #[cfg(feature = "tracing")] font: &crate::ShaperFont<'_, '_>, kern_mask: Mask, cross_stream: bool, get_kerning: F, @@ -138,7 +143,7 @@ fn machine_kern( F: Fn(u32, u32) -> i32, { buffer.unsafe_to_concat(None, None); - let mut ctx = ApplyContext::new(LayoutTableKind::Gpos, layout, scale, buffer); + let mut ctx = ApplyContext::new(LayoutTableKind::Gpos, layout, scale, buffer, #[cfg(feature = "tracing")] font); ctx.set_lookup_mask(kern_mask); ctx.lookup_props = u32::from(lookup_flags::IGNORE_MARKS); ctx.update_matchers(); @@ -215,6 +220,8 @@ fn apply_simple_kerning( c.layout, c.scale, c.buffer, + #[cfg(feature = "tracing")] + c.font, c.plan.kern_mask, is_cross_stream, |left, right| { diff --git a/harfrust/src/aat/kerx.rs b/harfrust/src/aat/kerx.rs index 72af6209..0b6892b5 100644 --- a/harfrust/src/aat/kerx.rs +++ b/harfrust/src/aat/kerx.rs @@ -59,11 +59,18 @@ pub(crate) fn apply(c: &mut AatApplyContext) -> Option<()> { c.safe_to_break = safe_to_break.subtable(subtable_cache.safe_to_break)?; if !c.buffer_intersects_machine() { + message!( + c, + "skipping kerning subtable {} because no glyph matches", + subtable_idx + ); continue; } let reverse = c.buffer.direction.is_backward(); + message_continue!(c, "start subtable {}", subtable_idx); + if !seen_cross_stream && subtable.is_cross_stream() { seen_cross_stream = true; @@ -136,7 +143,9 @@ pub(crate) fn apply(c: &mut AatApplyContext) -> Option<()> { apply_simple_kerning(c, &subtable, format6); } } + message!(c, "end subtable {}", subtable_idx); } + if c.buffer_is_reversed { c.reverse_buffer(); } @@ -214,7 +223,7 @@ impl SimpleKerning for Subtable6<'_> { fn apply_simple_kerning(c: &mut AatApplyContext, subtable: &Subtable, kind: &T) { let scale = c.scale; - let mut ctx = ApplyContext::new(LayoutTableKind::Gpos, c.layout, c.scale, c.buffer); + let mut ctx = ApplyContext::new(LayoutTableKind::Gpos, c.layout, c.scale, c.buffer, #[cfg(feature = "tracing")] c.font); ctx.set_lookup_mask(c.plan.kern_mask); ctx.lookup_props = u32::from(lookup_flags::IGNORE_MARKS); ctx.update_matchers(); diff --git a/harfrust/src/aat/layout.rs b/harfrust/src/aat/layout.rs index f8259590..37bb0dcb 100644 --- a/harfrust/src/aat/layout.rs +++ b/harfrust/src/aat/layout.rs @@ -503,6 +503,7 @@ pub fn substitute( layout: LayoutData<'_>, buffer: &mut Buffer, features: &[Feature], + #[cfg(feature = "tracing")] font: &crate::ShaperFont<'_, '_>, ) { let mut aat_map = map::AatMap::default(); if !features.is_empty() { @@ -513,7 +514,8 @@ pub fn substitute( builder.compile(layout.aat, &mut aat_map); } - let mut c = AatApplyContext::new(plan, layout, Scale::default(), buffer); + let mut c = AatApplyContext::new(plan, layout, Scale::default(), buffer, #[cfg(feature = "tracing")] font); + message_return!(c, "start table morx"); morx::apply( &mut c, if features.is_empty() { @@ -522,6 +524,7 @@ pub fn substitute( &aat_map }, ); + message!(c, "end table morx"); } fn is_deleted_glyph(info: &GlyphInfo) -> bool { @@ -542,9 +545,11 @@ pub fn remove_deleted_glyphs(buffer: &mut Buffer) { /// /// See #[doc(alias = "hb_aat_layout_position")] -pub fn position(plan: &ShapePlan, layout: LayoutData<'_>, scale: Scale, buffer: &mut Buffer) { - let mut c = AatApplyContext::new(plan, layout, scale, buffer); +pub fn position(plan: &ShapePlan, layout: LayoutData<'_>, scale: Scale, buffer: &mut Buffer, #[cfg(feature = "tracing")] font: &crate::ShaperFont<'_, '_>) { + let mut c = AatApplyContext::new(plan, layout, scale, buffer, #[cfg(feature = "tracing")] font); + message_return!(c, "start table kerx"); kerx::apply(&mut c); + message!(c, "end table kerx"); } /// HB: hb_aat_layout_track diff --git a/harfrust/src/aat/morx.rs b/harfrust/src/aat/morx.rs index 52dab29f..f97f2afa 100644 --- a/harfrust/src/aat/morx.rs +++ b/harfrust/src/aat/morx.rs @@ -167,7 +167,7 @@ pub fn compile_flags(aat: &AatData, builder: &AatMapBuilder, map: &mut AatMap) - } // Chain::apply in harfbuzz -pub fn apply<'a>(c: &mut AatApplyContext<'a>, map: &'a AatMap) -> Option<()> { +pub fn apply<'a>(c: &mut AatApplyContext<'a, '_, '_>, map: &'a AatMap) -> Option<()> { c.buffer.unsafe_to_concat(None, None); c.setup_buffer_glyph_set(); @@ -198,7 +198,7 @@ pub fn apply<'a>(c: &mut AatApplyContext<'a>, map: &'a AatMap) -> Option<()> { } fn apply_table<'a>( - c: &mut AatApplyContext<'a>, + c: &mut AatApplyContext<'a, '_, '_>, map: &'a AatMap, safe_to_break: &'a SafeToBreakAccel, table_bytes: &'a [u8], @@ -241,8 +241,10 @@ fn apply_table<'a>( c.safe_to_break = safe_to_break.subtable(subtable_cache.safe_to_break)?; if !c.buffer_intersects_machine() { + message!(c, "skipped chainsubtable {} because no glyph matches", subtable_idx); continue; } + message_continue!(c, "start chainsubtable {}", subtable_idx); // Buffer contents is always in logical direction. Determine if // we need to reverse before applying this subtable. We reverse @@ -277,6 +279,7 @@ fn apply_table<'a>( } } } + message!(c, "end chainsubtable {}", subtable_idx); } if c.buffer_is_reversed { c.reverse_buffer(); @@ -649,7 +652,7 @@ where } } -fn apply_morx_subtable<'a>(kind: morx::SubtableKind<'a>, ac: &mut AatApplyContext<'a>) { +fn apply_morx_subtable<'a>(kind: morx::SubtableKind<'a>, ac: &mut AatApplyContext<'a, '_, '_>) { match kind { morx::SubtableKind::Rearrangement(table) => { let mut c = RearrangementCtx { start: 0, end: 0 }; @@ -682,7 +685,7 @@ fn apply_morx_subtable<'a>(kind: morx::SubtableKind<'a>, ac: &mut AatApplyContex } } -fn apply_mort_subtable<'a>(kind: mort::SubtableKind<'a>, ac: &mut AatApplyContext<'a>) { +fn apply_mort_subtable<'a>(kind: mort::SubtableKind<'a>, ac: &mut AatApplyContext<'a, '_, '_>) { match kind { mort::SubtableKind::Rearrangement(table) => { let mut c = RearrangementCtx { start: 0, end: 0 }; diff --git a/harfrust/src/buffer.rs b/harfrust/src/buffer.rs index ab211f4d..64fbcbe8 100644 --- a/harfrust/src/buffer.rs +++ b/harfrust/src/buffer.rs @@ -5,6 +5,8 @@ use crate::unicode::{CharExt, Codepoint}; use crate::U32Set; use crate::{BufferFlags, ClusterLevel, Direction, Language, Script, SerializeFlags}; use alloc::{string::String, vec::Vec}; +#[cfg(feature = "tracing")] +use alloc::boxed::Box; use core::cmp::min; use read_fonts::types::{GlyphId, GlyphId16}; @@ -466,13 +468,14 @@ pub enum ContentType { Glyphs, } +#[cfg(feature = "tracing")] +type MessageFunction = dyn FnMut(&Buffer, &crate::ShaperFont<'_, '_>, &str) -> bool + Send + Sync; + /// A buffer of text to be shaped, and of the glyphs that shaping produces. /// /// This is the unified buffer type, matching HarfBuzz's `hb_buffer_t`. It /// carries a [`ContentType`] describing which of the two states it is /// currently in. -pub type MessageFunction = fn(buffer: &Buffer, font: &crate::ShaperFont<'_, '_>, message: &str) -> bool; - pub struct Buffer { // Information about how the text in the buffer should be treated. pub(crate) flags: BufferFlags, @@ -518,8 +521,8 @@ pub struct Buffer { pub(crate) max_len: usize, /// Maximum allowed operations. pub(crate) max_ops: i32, - pub(crate) message_depth: usize, - pub(crate) message_function: Option, + #[cfg(feature = "tracing")] + message_function: Option>, } impl Buffer { @@ -552,7 +555,7 @@ impl Buffer { context_len: [0, 0], digest: SetDigest::new(), glyph_set: U32Set::default(), - message_depth: 0, + #[cfg(feature = "tracing")] message_function: None, } } @@ -1281,22 +1284,23 @@ impl Buffer { true } - pub(crate) fn sync_so_far(&mut self) -> usize { - let had_output = self.have_output; - let out_i = self.out_len; - let i = self.idx; - let old_idx = self.idx; - if self.sync() { - self.idx = out_i; - } else { - self.idx = i; + // Make the output produced so far visible to callbacks without ending the + // current substitution pass. The input cursor can move in either direction. + #[cfg(feature = "tracing")] + pub(crate) fn sync_so_far(&mut self) -> isize { + if !self.have_output || !self.successful { + return 0; } - if had_output { - self.have_output = true; - self.out_len = self.idx; + let old_idx = self.idx; + let out_idx = self.out_len; + if !self.sync() { + return 0; } + self.idx = out_idx; + self.have_output = true; + self.out_len = out_idx; debug_assert!(self.idx <= self.len); - self.idx - old_idx + self.idx as isize - old_idx as isize } pub(crate) fn clear_output(&mut self) { @@ -2144,26 +2148,46 @@ impl Buffer { lig_id } - #[inline] - pub fn set_message_function(&mut self, func: MessageFunction) { - #[cfg(feature = "std")] - { - self.message_function = Some(func); - } + /// Sets the callback used to trace shaping operations. + /// + /// The callback receives the current buffer, shaping font, and a HarfBuzz + /// tracing message. Return `false` from a `start` message to skip that + /// operation; return values from other messages are ignored. Message text + /// may change as shaping algorithms evolve. + /// + /// The callback can inspect [`Self::content_type`], [`Self::glyph_infos`], + /// [`Self::glyph_positions`], or [`Self::serialize`]. Positions are empty + /// until positioning begins. The callback is retained by [`Self::clear`] + /// and [`Self::reset`]. To collect results, capture owned shared state such + /// as an `Arc>>`. + /// + /// Requires the `tracing` feature, which also works without `std`. + #[cfg(feature = "tracing")] + pub fn set_message_function(&mut self, function: F) + where + F: FnMut(&Self, &crate::ShaperFont<'_, '_>, &str) -> bool + Send + Sync + 'static, + { + self.message_function = Some(Box::new(function)); } - #[inline] + /// Removes the shaping message callback. + #[cfg(feature = "tracing")] + pub fn clear_message_function(&mut self) { + self.message_function = None; + } + + #[cfg(feature = "tracing")] pub(crate) fn message(&mut self, font: &crate::ShaperFont<'_, '_>, msg: &str) -> bool { - if let Some(func) = self.message_function { - self.message_depth += 1; - let ret = func(self, font, msg); - self.message_depth -= 1; + if let Some(mut function) = self.message_function.take() { + let ret = function(self, font, msg); + self.message_function = Some(function); ret } else { true } } + #[cfg(feature = "tracing")] #[inline] pub(crate) fn messaging(&self) -> bool { self.message_function.is_some() @@ -2179,10 +2203,23 @@ impl Buffer { let mut s = String::with_capacity(64); let info = self.glyph_infos(); + if self.content_type == Some(ContentType::Unicode) { + for info in info { + s.push(if s.is_empty() { '[' } else { '|' }); + write!(&mut s, "U+{:04X}", info.glyph_id)?; + if !flags.contains(SerializeFlags::NO_CLUSTERS) { + write!(&mut s, "={}", info.cluster)?; + } + } + if !s.is_empty() { s.push(']'); } + return Ok(s); + } let pos = self.glyph_positions(); + let default_pos = GlyphPosition::default(); let mut x: i32 = 0; let mut y: i32 = 0; - for (info, pos) in info.iter().zip(pos) { + for (index, info) in info.iter().enumerate() { + let position = pos.get(index).unwrap_or(&default_pos); s.push(if s.is_empty() { '[' } else { '|' }); if !flags.contains(SerializeFlags::NO_GLYPH_NAMES) { @@ -2198,17 +2235,17 @@ impl Buffer { write!(&mut s, "={}", info.cluster)?; } - if !flags.contains(SerializeFlags::NO_POSITIONS) { - let dx = x.saturating_add(pos.x_offset); - let dy = y.saturating_add(pos.y_offset); + if !pos.is_empty() && !flags.contains(SerializeFlags::NO_POSITIONS) { + let dx = x.saturating_add(position.x_offset); + let dy = y.saturating_add(position.y_offset); if dx != 0 || dy != 0 { write!(&mut s, "@{dx},{dy}")?; } if !flags.contains(SerializeFlags::NO_ADVANCES) { - write!(&mut s, "+{}", pos.x_advance)?; - if pos.y_advance != 0 { - write!(&mut s, ",{}", pos.y_advance)?; + write!(&mut s, "+{}", position.x_advance)?; + if position.y_advance != 0 { + write!(&mut s, ",{}", position.y_advance)?; } } } @@ -2231,8 +2268,8 @@ impl Buffer { } if flags.contains(SerializeFlags::NO_ADVANCES) { - x = x.saturating_add(pos.x_advance); - y = y.saturating_add(pos.y_advance); + x = x.saturating_add(position.x_advance); + y = y.saturating_add(position.y_advance); } } @@ -2263,44 +2300,49 @@ impl core::fmt::Debug for Buffer { } } +// All arguments (including formatting and font references) disappear when +// tracing is disabled. Allocate message text only when a callback is installed. +macro_rules! buffer_message { + ($buffer:expr, $font:expr, $($arg:tt)*) => {{ + #[cfg(feature = "tracing")] + { + if $buffer.messaging() { + let msg = alloc::format!($($arg)*); + $buffer.message($font, &msg) + } else { + true + } + } + #[cfg(not(feature = "tracing"))] + { true } + }}; +} + macro_rules! message { ($ctx:expr, $($arg:tt)*) => { - if $ctx.buffer.messaging() { - let msg = format!($($arg)*); - $ctx.buffer.message($ctx.font, &msg); - } + buffer_message!($ctx.buffer, $ctx.font, $($arg)*) }; } macro_rules! message_sync { - ($ctx:expr, $($arg:tt)*) => { + ($ctx:expr, $($arg:tt)*) => {{ + #[cfg(feature = "tracing")] if $ctx.buffer.messaging() { $ctx.buffer.sync_so_far(); - let msg = format!($($arg)*); - $ctx.buffer.message($ctx.font, &msg); + message!($ctx, $($arg)*); } - }; + }}; } macro_rules! message_return { ($ctx:expr, $($arg:tt)*) => { - if $ctx.buffer.messaging() { - let msg = format!($($arg)*); - if !$ctx.buffer.message($ctx.font, &msg) { - return; - } - } + if !message!($ctx, $($arg)*) { return; } }; } macro_rules! message_continue { ($ctx:expr, $($arg:tt)*) => { - if $ctx.buffer.messaging() { - let msg = format!($($arg)*); - if !$ctx.buffer.message($ctx.font, &msg) { - continue; - } - } + if !message!($ctx, $($arg)*) { continue; } }; } diff --git a/harfrust/src/fallback.rs b/harfrust/src/fallback.rs index ef7193e0..4f27a77b 100644 --- a/harfrust/src/fallback.rs +++ b/harfrust/src/fallback.rs @@ -95,14 +95,18 @@ fn recategorize_combining_class(u: u32, mut class: u8) -> u8 { } } -pub fn recategorize_marks(buffer: &mut Buffer) { - let len = buffer.len; - for info in &mut buffer.info[..len] { - if info.general_category() == GeneralCategory::NON_SPACING_MARK { - let mut class = info.modified_combining_class(); - class = recategorize_combining_class(info.glyph_id, class); - info.set_modified_combining_class(class); +pub fn recategorize_marks(buffer: &mut Buffer, #[cfg(feature = "tracing")] font: &ShaperFont<'_, '_>) { + if buffer_message!(buffer, font, "start fallback mark") { + let len = buffer.len; + for info in &mut buffer.info[..len] { + if info.general_category() == GeneralCategory::NON_SPACING_MARK { + let mut class = info.modified_combining_class(); + class = recategorize_combining_class(info.glyph_id, class); + info.set_modified_combining_class(class); + } } + + buffer_message!(buffer, font, "end fallback mark"); } } diff --git a/harfrust/src/normalize.rs b/harfrust/src/normalize.rs index 48367df7..2a2bd8fd 100644 --- a/harfrust/src/normalize.rs +++ b/harfrust/src/normalize.rs @@ -376,7 +376,7 @@ pub fn normalize<'x>(plan: &ShapePlan, buffer: &'x mut Buffer, font: &'x ShaperF } // Second round, reorder (inplace) - if !all_simple { + if !all_simple && buffer_message!(ctx.buffer, ctx.font, "start reorder") { let count = ctx.buffer.len; let mut i = 0; while i < count { @@ -401,6 +401,7 @@ pub fn normalize<'x>(plan: &ShapePlan, buffer: &'x mut Buffer, font: &'x ShaperF i = end + 1; } + message!(ctx, "end reorder"); } if ctx.buffer.scratch_flags & HB_BUFFER_SCRATCH_FLAG_HAS_CGJ != 0 { // For all CGJ, check if it prevented any reordering at all. diff --git a/harfrust/src/ot/apply.rs b/harfrust/src/ot/apply.rs index 3bfa0fe1..33a0aef1 100644 --- a/harfrust/src/ot/apply.rs +++ b/harfrust/src/ot/apply.rs @@ -358,7 +358,7 @@ pub struct SkippingIterator<'f, 'c, F> { } impl<'f, 'c> SkippingIterator<'f, 'c, fn(&mut GlyphInfo, u32) -> bool> { - pub fn new(ctx: &'c mut ApplyContext<'f>, context_match: bool) -> Self { + pub fn new(ctx: &'c mut ApplyContext<'f, '_, '_>, context_match: bool) -> Self { Self::with_match_fn(ctx, context_match, None) } } @@ -368,7 +368,7 @@ where F: Fn(&mut GlyphInfo, u32) -> bool, { pub fn with_match_fn( - ctx: &'c mut ApplyContext<'f>, + ctx: &'c mut ApplyContext<'f, '_, '_>, context_match: bool, match_fn: Option, ) -> Self { @@ -575,10 +575,19 @@ pub(crate) fn apply_lookup( break; } + message_sync!( + ctx, + "recursing to lookup {} at {}", + record.lookup_list_index.get(), + ctx.buffer.idx + ); + if ctx.recurse(record.lookup_list_index.get()).is_none() { continue; } + message_sync!(ctx, "recursed to lookup {}", record.lookup_list_index.get(),); + let new_len = ctx.buffer.backtrack_len() + ctx.buffer.lookahead_len(); let mut delta = new_len as isize - orig_len as isize; if delta == 0 { @@ -895,10 +904,14 @@ pub fn check_glyph_property(ot: &OtData, info: &GlyphInfo, match_props: u32) -> true } -pub struct ApplyContext<'a> { +pub struct ApplyContext<'a, 'f, 'c> { pub table_index: LayoutTableKind, pub layout: crate::LayoutData<'a>, pub scale: Scale, + #[cfg(feature = "tracing")] + pub font: &'a crate::ShaperFont<'f, 'c>, + #[cfg(not(feature = "tracing"))] + font_lifetimes: core::marker::PhantomData<(&'f (), &'c ())>, pub buffer: &'a mut Buffer, lookup_mask: Mask, pub per_syllable: bool, @@ -918,18 +931,23 @@ pub struct ApplyContext<'a> { pub(crate) match_positions: MatchPositions, } -impl<'a> ApplyContext<'a> { +impl<'a, 'f, 'c> ApplyContext<'a, 'f, 'c> { pub fn new( table_index: LayoutTableKind, layout: crate::LayoutData<'a>, scale: Scale, buffer: &'a mut Buffer, + #[cfg(feature = "tracing")] font: &'a crate::ShaperFont<'f, 'c>, ) -> Self { Self { table_index, layout, scale, + #[cfg(not(feature = "tracing"))] + font_lifetimes: core::marker::PhantomData, buffer, + #[cfg(feature = "tracing")] + font, lookup_mask: 1, per_syllable: false, lookup_index: u16::MAX, diff --git a/harfrust/src/ot/gpos/cursive.rs b/harfrust/src/ot/gpos/cursive.rs index ab2fd47f..71d40423 100644 --- a/harfrust/src/ot/gpos/cursive.rs +++ b/harfrust/src/ot/gpos/cursive.rs @@ -49,6 +49,8 @@ impl Apply for CursivePosFormat1<'_> { let j = ctx.buffer.idx; ctx.buffer.unsafe_to_break(Some(i), Some(j + 1)); + message!(ctx, "cursive attaching glyph at {} to glyph at {}", i, j); + let pos = &mut ctx.buffer.pos; match direction { Direction::LeftToRight => { @@ -133,6 +135,8 @@ impl Apply for CursivePosFormat1<'_> { } } + message!(ctx, "cursive attached glyph at {} to glyph at {}", i, j); + ctx.buffer.idx += 1; Some(()) } diff --git a/harfrust/src/ot/gpos/mark.rs b/harfrust/src/ot/gpos/mark.rs index 1ee4d93b..c3ce71d8 100644 --- a/harfrust/src/ot/gpos/mark.rs +++ b/harfrust/src/ot/gpos/mark.rs @@ -80,6 +80,7 @@ impl MarkArrayExt for MarkArray<'_> { ctx.buffer.idx += 1; return Some(()); }; + message!(ctx, "attaching mark glyph at {} to glyph at {}", idx, glyph_pos); let pos = ctx.buffer.cur_pos_mut(); pos.x_offset = x_offset; pos.y_offset = y_offset; @@ -90,6 +91,7 @@ impl MarkArrayExt for MarkArray<'_> { } pos.set_attach_type(attach_type::MARK); pos.set_attach_chain(chain); + message!(ctx, "attached mark glyph at {} to glyph at {}", idx, glyph_pos); ctx.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT; ctx.buffer.idx += 1; diff --git a/harfrust/src/ot/gpos/mod.rs b/harfrust/src/ot/gpos/mod.rs index f1e63fda..66cc9e38 100644 --- a/harfrust/src/ot/gpos/mod.rs +++ b/harfrust/src/ot/gpos/mod.rs @@ -13,8 +13,12 @@ use crate::Direction; use crate::ShaperFont; pub fn position(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) { + #[cfg(feature = "tracing")] + let tag = plan.ot_map.chosen_script(LayoutTableKind::Gpos).unwrap_or(crate::Tag::new(b"DFLT")); + if !buffer_message!(buffer, font, "start table GPOS script tag '{tag}'") { return; } let table = font.layout().ot.gpos.clone(); apply_layout_table(plan, font, buffer, table.as_ref()); + buffer_message!(buffer, font, "end table GPOS script tag '{tag}'"); } pub mod attach_type { diff --git a/harfrust/src/ot/gpos/pair.rs b/harfrust/src/ot/gpos/pair.rs index 283bc886..e1522fa9 100644 --- a/harfrust/src/ot/gpos/pair.rs +++ b/harfrust/src/ot/gpos/pair.rs @@ -94,6 +94,7 @@ impl Apply for PairPosFormat1<'_> { let success = |ctx: &mut ApplyContext, iter_index: &mut usize, flag1, flag2, has_record2| { if flag1 || flag2 { + message!(ctx, "kerned glyphs at {} and {second_glyph_index}", ctx.buffer.idx); ctx.buffer .unsafe_to_break(Some(ctx.buffer.idx), Some(second_glyph_index + 1)); } else { @@ -102,6 +103,7 @@ impl Apply for PairPosFormat1<'_> { ctx.buffer .unsafe_to_concat(Some(ctx.buffer.idx), Some(second_glyph_index + 1)); } + message!(ctx, "tried kerning glyphs at {} and {second_glyph_index}", ctx.buffer.idx); finish(ctx, iter_index, has_record2) }; @@ -135,8 +137,10 @@ impl Apply for PairPosFormat1<'_> { } else if glyph_id > second_glyph { hi = mid; } else { + message!(ctx, "try kerning glyphs at {} and {second_glyph_index}", ctx.buffer.idx); let has_record2 = !format2.is_empty(); - let worked1 = !format1.is_empty() + message!(ctx, "try kerning glyphs at {} and {second_glyph_index}", ctx.buffer.idx); + let worked1 = !format1.is_empty() && super::value::apply( ctx, ctx.buffer.idx, @@ -237,6 +241,7 @@ impl Apply for PairPosFormat2<'_> { let success = |ctx: &mut ApplyContext, iter_index: &mut usize, flag1, flag2, has_record2| { if flag1 || flag2 { + message!(ctx, "kerned glyphs at {} and {second_glyph_index}", ctx.buffer.idx); ctx.buffer .unsafe_to_break(Some(ctx.buffer.idx), Some(second_glyph_index + 1)); finish(ctx, iter_index, has_record2) @@ -277,6 +282,7 @@ impl Apply for PairPosFormat2<'_> { + (class2 as usize * record_size) + self.class1_records_byte_range().start; let has_record2 = !format2.is_empty(); + message!(ctx, "try kerning glyphs at {} and {second_glyph_index}", ctx.buffer.idx); let worked1 = !format1.is_empty() && super::value::apply(ctx, ctx.buffer.idx, &data, record_offset, format1) == Some(true); diff --git a/harfrust/src/ot/gpos/single.rs b/harfrust/src/ot/gpos/single.rs index 97bbc423..5bb1b072 100644 --- a/harfrust/src/ot/gpos/single.rs +++ b/harfrust/src/ot/gpos/single.rs @@ -8,7 +8,9 @@ impl Apply for SinglePosFormat1<'_> { self.coverage().ok()?.get(glyph)?; let format = self.value_format(); let offset = self.value_record_byte_range().start; + message!(ctx, "positioning glyph at {}", ctx.buffer.idx); super::value::apply(ctx, ctx.buffer.idx, &self.offset_data(), offset, format); + message!(ctx, "positioned glyph at {}", ctx.buffer.idx); ctx.buffer.idx += 1; Some(()) } @@ -20,7 +22,9 @@ impl Apply for SinglePosFormat2<'_> { let index = self.coverage().ok()?.get(glyph)? as usize; let format = self.value_format(); let offset = self.value_records_byte_range().start + (format.record_byte_len() * index); + message!(ctx, "positioning glyph at {}", ctx.buffer.idx); super::value::apply(ctx, ctx.buffer.idx, &self.offset_data(), offset, format); + message!(ctx, "positioned glyph at {}", ctx.buffer.idx); ctx.buffer.idx += 1; Some(()) } diff --git a/harfrust/src/ot/gsub/alternate.rs b/harfrust/src/ot/gsub/alternate.rs index 7e660e6c..ab84822c 100644 --- a/harfrust/src/ot/gsub/alternate.rs +++ b/harfrust/src/ot/gsub/alternate.rs @@ -25,7 +25,18 @@ impl Apply for AlternateSet<'_> { } let idx = u16::try_from(alt_index).ok()?.checked_sub(1)?; + + message_sync!( + ctx, + "replacing glyph at {} (alternate substitution)", + ctx.buffer.idx, + ); ctx.replace_glyph(alternates.get(idx as usize)?.get().into()); + message_sync!( + ctx, + "replaced glyph at {} (alternate substitution)", + ctx.buffer.idx - 1, + ); Some(()) } diff --git a/harfrust/src/ot/gsub/ligature.rs b/harfrust/src/ot/gsub/ligature.rs index 992eced4..a415079b 100644 --- a/harfrust/src/ot/gsub/ligature.rs +++ b/harfrust/src/ot/gsub/ligature.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "tracing")] +use alloc::string::String; use crate::buffer::GlyphInfo; use crate::ot::apply::ApplyContext; use crate::ot::apply::{ @@ -34,7 +36,17 @@ impl Apply for Ligature<'_> { // as a "ligated" substitution. let components = self.component_glyph_ids(); if components.is_empty() { + message_sync!( + ctx, + "replacing glyph at {} (ligature substitution)", + ctx.buffer.idx + ); ctx.replace_glyph(self.ligature_glyph().into()); + message_sync!( + ctx, + "replaced glyph at {} (ligature substitution)", + ctx.buffer.idx - 1, + ); Some(()) } else { let f = |info: &mut GlyphInfo, index| { @@ -57,6 +69,25 @@ impl Apply for Ligature<'_> { return None; } let count = components.len() + 1; + #[cfg(feature = "tracing")] + let mut pos = 0; + #[cfg(feature = "tracing")] + if ctx.buffer.messaging() { + let delta = ctx.buffer.sync_so_far(); + pos = ctx.buffer.idx; + let count = components.len(); + match_end = match_end.checked_add_signed(delta)?; + let mut msg = String::from("ligating glyphs at "); + for i in 0..=count { + ctx.match_positions[i] = (ctx.match_positions[i] as usize).checked_add_signed(delta)? as u32; + if i > 0 { + msg.push(','); + } + use core::fmt::Write; + let _ = write!(msg, "{}", ctx.match_positions[i]); + } + ctx.buffer.message(ctx.font, &msg); + } ligate_input( ctx, count, @@ -64,6 +95,7 @@ impl Apply for Ligature<'_> { total_component_count, self.ligature_glyph().into(), ); + message_sync!(ctx, "ligated glyph at {}", pos); Some(()) } } diff --git a/harfrust/src/ot/gsub/mod.rs b/harfrust/src/ot/gsub/mod.rs index 597d693e..c1147648 100644 --- a/harfrust/src/ot/gsub/mod.rs +++ b/harfrust/src/ot/gsub/mod.rs @@ -13,6 +13,10 @@ use crate::plan::ShapePlan; use crate::ShaperFont; pub fn substitute(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) { + #[cfg(feature = "tracing")] + let tag = plan.ot_map.chosen_script(LayoutTableKind::Gsub).unwrap_or(crate::Tag::new(b"DFLT")); + if !buffer_message!(buffer, font, "start table GSUB script tag '{tag}'") { return; } let table = font.layout().ot.gsub.clone(); apply_layout_table(plan, font, buffer, table.as_ref()); + buffer_message!(buffer, font, "end table GSUB script tag '{tag}'"); } diff --git a/harfrust/src/ot/gsub/multiple.rs b/harfrust/src/ot/gsub/multiple.rs index 0608218b..982186e7 100644 --- a/harfrust/src/ot/gsub/multiple.rs +++ b/harfrust/src/ot/gsub/multiple.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "tracing")] +use alloc::string::String; use crate::buffer::GlyphPropsFlags; use crate::ot::apply::ApplyContext; use crate::ot::apply::{Apply, WouldApply, WouldApplyContext}; @@ -20,13 +22,38 @@ impl Apply for MultipleSubstFormat1<'_> { match substs.len() { // Spec disallows this, but Uniscribe allows it. // https://github.com/harfbuzz/harfbuzz/issues/253 - 0 => ctx.buffer.delete_glyph(), + 0 => { + message_sync!( + ctx, + "deleting glyph at {} (multiple substitution)", + ctx.buffer.idx + ); + ctx.buffer.delete_glyph(); + message_sync!( + ctx, + "deleted glyph at {} (multiple substitution)", + ctx.buffer.idx, + ); + } // Special-case to make it in-place and not consider this // as a "multiplied" substitution. - 1 => ctx.replace_glyph(substs.first()?.get().into()), + 1 => { + message_sync!( + ctx, + "replacing glyph at {} (multiple substitution)", + ctx.buffer.idx + ); + ctx.replace_glyph(substs.first()?.get().into()); + message_sync!( + ctx, + "replaced glyph at {} (multiple substitution)", + ctx.buffer.idx - 1, + ); + } _ => { + message_sync!(ctx, "multiplying glyph at {}", ctx.buffer.idx); let class = if ctx.buffer.cur(0).is_ligature() { GlyphPropsFlags::BASE_GLYPH } else { @@ -46,6 +73,21 @@ impl Apply for MultipleSubstFormat1<'_> { } ctx.buffer.skip_glyph(); + + #[cfg(feature = "tracing")] + if ctx.buffer.messaging() { + ctx.buffer.sync_so_far(); + let count = substs.len(); + let mut msg = String::from("multiplied glyphs at "); + for i in (ctx.buffer.idx - count)..ctx.buffer.idx { + if i > (ctx.buffer.idx - count) { + msg.push(','); + } + use core::fmt::Write; + let _ = write!(msg, "{i}"); + } + ctx.buffer.message(ctx.font, &msg); + } } } Some(()) diff --git a/harfrust/src/ot/gsub/reverse_chain.rs b/harfrust/src/ot/gsub/reverse_chain.rs index d5879aaa..14dee077 100644 --- a/harfrust/src/ot/gsub/reverse_chain.rs +++ b/harfrust/src/ot/gsub/reverse_chain.rs @@ -62,10 +62,19 @@ impl Apply for ReverseChainSingleSubstFormat1<'_> { ctx.buffer.idx + 1, &mut end_index, ) { + message_sync!( + ctx, + "replacing glyph at {} (reverse chaining substitution)", + ctx.buffer.idx + ); ctx.buffer .unsafe_to_break_from_outbuffer(Some(start_index), Some(end_index)); ctx.replace_glyph_inplace(subst.into()); - + message_sync!( + ctx, + "replaced glyph at {} (reverse chaining substitution)", + ctx.buffer.idx + ); // Note: We DON'T decrease buffer.idx. The main loop does it // for us. This is useful for preventing surprises if someone // calls us through a Context lookup. diff --git a/harfrust/src/ot/gsub/single.rs b/harfrust/src/ot/gsub/single.rs index 0b4ec3a0..4e4ec65e 100644 --- a/harfrust/src/ot/gsub/single.rs +++ b/harfrust/src/ot/gsub/single.rs @@ -14,7 +14,17 @@ impl Apply for SingleSubstFormat1<'_> { let glyph = ctx.buffer.cur(0).as_glyph(); self.coverage().ok()?.get(glyph)?; let subst = (glyph.to_u32() as i32 + self.delta_glyph_id() as i32) as u16; + message_sync!( + ctx, + "replacing glyph at {} (single substitution)", + ctx.buffer.idx + ); ctx.replace_glyph(subst.into()); + message_sync!( + ctx, + "replaced glyph at {} (single substitution)", + ctx.buffer.idx - 1 + ); Some(()) } } @@ -33,7 +43,17 @@ impl Apply for SingleSubstFormat2<'_> { let glyph = ctx.buffer.cur(0).as_glyph(); let index = self.coverage().ok()?.get(glyph)? as usize; let subst = self.substitute_glyph_ids().get(index)?.get().to_u16(); + message_sync!( + ctx, + "replacing glyph at {} (single substitution)", + ctx.buffer.idx + ); ctx.replace_glyph(subst.into()); + message_sync!( + ctx, + "replaced glyph at {} (single substitution)", + ctx.buffer.idx - 1 + ); Some(()) } } diff --git a/harfrust/src/ot/layout.rs b/harfrust/src/ot/layout.rs index 6ef0ead1..8235b274 100644 --- a/harfrust/src/ot/layout.rs +++ b/harfrust/src/ot/layout.rs @@ -161,7 +161,7 @@ pub fn apply_layout_table( buffer: &mut Buffer, table: Option<&T>, ) { - let mut ctx = ApplyContext::new(T::KIND, font.layout(), font.scale, buffer); + let mut ctx = ApplyContext::new(T::KIND, font.layout(), font.scale, buffer, #[cfg(feature = "tracing")] font); for (stage_index, stage) in plan.ot_map.stages(T::KIND).iter().enumerate() { if let Some(table) = table { @@ -170,6 +170,8 @@ pub fn apply_layout_table( continue; }; + message_continue!(ctx, "start lookup {}", lookup_map.index); + if lookup.digest().may_intersect(&ctx.buffer.digest) && (ctx .buffer @@ -185,8 +187,13 @@ pub fn apply_layout_table( ctx.random = lookup_map.random; ctx.per_syllable = lookup_map.per_syllable; - apply_string::(&mut ctx, lookup); + if !apply_string::(&mut ctx, lookup) { + message!(ctx, "skipped lookup {} because no glyph matches", lookup_map.index); + } + } else { + message!(ctx, "skipped lookup {} because no glyph matches", lookup_map.index); } + message!(ctx, "end lookup {}", lookup_map.index); } } @@ -198,11 +205,12 @@ pub fn apply_layout_table( } } -fn apply_string(ctx: &mut ApplyContext, lookup: &LookupInfo) { +fn apply_string(ctx: &mut ApplyContext, lookup: &LookupInfo) -> bool { if ctx.buffer.is_empty() || ctx.lookup_mask() == 0 { - return; + return false; } + let ret; ctx.lookup_props = lookup.props(); ctx.update_matchers(); @@ -212,7 +220,7 @@ fn apply_string(ctx: &mut ApplyContext, lookup: &LookupInfo) { ctx.buffer.clear_output(); } ctx.buffer.idx = 0; - apply_forward(ctx, lookup); + ret = apply_forward(ctx, lookup); if !T::IN_PLACE { ctx.buffer.sync(); @@ -222,8 +230,9 @@ fn apply_string(ctx: &mut ApplyContext, lookup: &LookupInfo) { debug_assert!(!ctx.buffer.have_output); ctx.buffer.idx = ctx.buffer.len - 1; - apply_backward(ctx, lookup); + ret = apply_backward(ctx, lookup); } + ret } fn apply_forward(ctx: &mut ApplyContext, lookup: &LookupInfo) -> bool { diff --git a/harfrust/src/ot/shaper/arabic_fallback.rs b/harfrust/src/ot/shaper/arabic_fallback.rs index d29d0a1d..1be0f37a 100644 --- a/harfrust/src/ot/shaper/arabic_fallback.rs +++ b/harfrust/src/ot/shaper/arabic_fallback.rs @@ -211,7 +211,7 @@ impl FallbackPlan { } pub(crate) fn apply(&self, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) { - let mut ctx = ApplyContext::new(LayoutTableKind::Gsub, font.layout(), font.scale, buffer); + let mut ctx = ApplyContext::new(LayoutTableKind::Gsub, font.layout(), font.scale, buffer, #[cfg(feature = "tracing")] font); for lookup in &self.lookups { ctx.set_lookup_mask(lookup.mask); apply_synthesized_subst_lookup(&mut ctx, &lookup.info, &lookup.data); diff --git a/harfrust/src/ot/shaper/indic.rs b/harfrust/src/ot/shaper/indic.rs index bc987892..57ebb892 100644 --- a/harfrust/src/ot/shaper/indic.rs +++ b/harfrust/src/ot/shaper/indic.rs @@ -580,6 +580,7 @@ fn initial_reordering(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut use super::indic_machine::SyllableType; let mut ret = false; + if !buffer_message!(buffer, font, "start reordering indic initial") { return ret; } let indic_plan = plan.data::(); @@ -603,6 +604,7 @@ fn initial_reordering(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut end = buffer.next_syllable(start); } + buffer_message!(buffer, font, "end reordering indic initial"); ret } @@ -1265,9 +1267,12 @@ fn final_reordering(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Bu return false; } - foreach_syllable!(buffer, start, end, { - final_reordering_impl(plan, font, start, end, buffer); - }); + if buffer_message!(buffer, font, "start reordering indic final") { + foreach_syllable!(buffer, start, end, { + final_reordering_impl(plan, font, start, end, buffer); + }); + buffer_message!(buffer, font, "end reordering indic final"); + } buffer.deallocate_var(GlyphInfo::INDIC_CATEGORY_VAR); buffer.deallocate_var(GlyphInfo::INDIC_POSITION_VAR); diff --git a/harfrust/src/ot/shaper/khmer.rs b/harfrust/src/ot/shaper/khmer.rs index f27252a9..a5a92493 100644 --- a/harfrust/src/ot/shaper/khmer.rs +++ b/harfrust/src/ot/shaper/khmer.rs @@ -141,6 +141,10 @@ fn reorder_khmer(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffe use super::khmer_machine::SyllableType; let mut ret = false; + if !buffer_message!(buffer, font, "start reordering khmer") { + buffer.deallocate_var(GlyphInfo::KHMER_CATEGORY_VAR); + return ret; + } if insert_dotted_circles( font, @@ -165,6 +169,7 @@ fn reorder_khmer(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffe buffer.deallocate_var(GlyphInfo::KHMER_CATEGORY_VAR); + buffer_message!(buffer, font, "end reordering khmer"); ret } diff --git a/harfrust/src/ot/shaper/myanmar.rs b/harfrust/src/ot/shaper/myanmar.rs index 9a8b07a9..f10bffc8 100644 --- a/harfrust/src/ot/shaper/myanmar.rs +++ b/harfrust/src/ot/shaper/myanmar.rs @@ -133,6 +133,7 @@ fn reorder_myanmar(_: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer use super::myanmar_machine::SyllableType; let mut ret = false; + if !buffer_message!(buffer, font, "start reordering myanmar") { return ret; } if insert_dotted_circles( font, @@ -156,6 +157,7 @@ fn reorder_myanmar(_: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer buffer.deallocate_var(GlyphInfo::MYANMAR_CATEGORY_VAR); buffer.deallocate_var(GlyphInfo::MYANMAR_POSITION_VAR); + buffer_message!(buffer, font, "end reordering myanmar"); ret } diff --git a/harfrust/src/ot/shaper/syllabic.rs b/harfrust/src/ot/shaper/syllabic.rs index 9de3f04c..6444505c 100644 --- a/harfrust/src/ot/shaper/syllabic.rs +++ b/harfrust/src/ot/shaper/syllabic.rs @@ -20,6 +20,14 @@ pub fn insert_dotted_circles( } if (buffer.scratch_flags & HB_BUFFER_SCRATCH_FLAG_HAS_BROKEN_SYLLABLE) == 0 { + buffer_message!(buffer, + font, + "skipped inserting dotted-circles because there is no broken syllables", + ); + return false; + } + + if !buffer_message!(buffer, font, "start inserting dotted-circles") { return false; } @@ -70,6 +78,8 @@ pub fn insert_dotted_circles( buffer.sync(); + buffer_message!(buffer, font, "end inserting dotted-circles"); + true } diff --git a/harfrust/src/ot/shaper/use_.rs b/harfrust/src/ot/shaper/use_.rs index 4aa00e75..6ada5553 100644 --- a/harfrust/src/ot/shaper/use_.rs +++ b/harfrust/src/ot/shaper/use_.rs @@ -379,6 +379,7 @@ fn reorder_use(_: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) -> use super::use_machine::SyllableType; let mut ret = false; + if !buffer_message!(buffer, font, "start reordering USE") { return ret; } if insert_dotted_circles( font, @@ -401,6 +402,7 @@ fn reorder_use(_: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) -> buffer.deallocate_var(GlyphInfo::USE_CATEGORY_VAR); + buffer_message!(buffer, font, "end reordering USE"); ret } diff --git a/harfrust/src/shape.rs b/harfrust/src/shape.rs index 4c576bec..b8b7b173 100644 --- a/harfrust/src/shape.rs +++ b/harfrust/src/shape.rs @@ -121,7 +121,10 @@ impl OtShapeContext<'_, '_, '_> { ensure_native_direction(self.buffer); if let Some(func) = self.plan.shaper.preprocess_text { - func(self.plan, self.font, self.buffer); + if buffer_message!(self.buffer, self.font, "start preprocess text") { + func(self.plan, self.font, self.buffer); + message!(self, "end preprocess text"); + } } self.substitute_pre(); @@ -158,7 +161,10 @@ impl OtShapeContext<'_, '_, '_> { hide_default_ignorables(self.buffer, self.font); if let Some(func) = self.plan.shaper.postprocess_glyphs { - func(self.plan, self.font, self.buffer); + if buffer_message!(self.buffer, self.font, "start postprocess glyphs") { + func(self.plan, self.font, self.buffer); + message!(self, "end postprocess glyphs"); + } } } @@ -175,10 +181,11 @@ impl OtShapeContext<'_, '_, '_> { // This is unfortunate to go here, but necessary... if self.plan.fallback_mark_positioning { - fallback::recategorize_marks(self.buffer); + fallback::recategorize_marks(self.buffer, #[cfg(feature = "tracing")] self.font); } map_glyphs_fast(self.buffer); + self.buffer.content_type = Some(ContentType::Glyphs); self.buffer .deallocate_var(GlyphInfo::NORMALIZER_GLYPH_INDEX_VAR); @@ -193,7 +200,7 @@ impl OtShapeContext<'_, '_, '_> { synthesize_glyph_classes(self.buffer); } - aat::layout::substitute(self.plan, self.font.layout(), self.buffer, self.features); + aat::layout::substitute(self.plan, self.font.layout(), self.buffer, self.features, #[cfg(feature = "tracing")] self.font); // The digest is only read by the OT lookup-apply loop; without // GPOS ahead, nothing consumes it. if self.plan.apply_gpos { @@ -299,10 +306,10 @@ impl OtShapeContext<'_, '_, '_> { if plan.apply_gpos { gpos::position(plan, self.font, buffer); } else if plan.apply_kerx { - aat::layout::position(plan, layout, self.font.scale, buffer); + aat::layout::position(plan, layout, self.font.scale, buffer, #[cfg(feature = "tracing")] self.font); } if plan.apply_kern { - aat::kern::apply(plan, layout, self.font.scale, buffer); + aat::kern::apply(plan, layout, self.font.scale, buffer, #[cfg(feature = "tracing")] self.font); } else if plan.apply_fallback_kern { fallback::fallback_kern(plan, buffer); } From c29a6e9c5db8dba6cbe15bd31037c83065fa35bb Mon Sep 17 00:00:00 2001 From: Simon Cozens Date: Wed, 15 Oct 2025 14:09:47 +0000 Subject: [PATCH 3/6] Example of tracing --- hr-shape/Cargo.toml | 2 +- hr-shape/src/lib.rs | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/hr-shape/Cargo.toml b/hr-shape/Cargo.toml index 0b7bfc3d..b5c21a5e 100644 --- a/hr-shape/Cargo.toml +++ b/hr-shape/Cargo.toml @@ -11,7 +11,7 @@ keywords.workspace = true categories.workspace = true [dependencies] -harfrust = { workspace = true, features = ["std"] } +harfrust = { workspace = true, features = ["std", "tracing"] } clap = { version = "4", features = ["derive"] } [[bin]] diff --git a/hr-shape/src/lib.rs b/hr-shape/src/lib.rs index f127f42c..87389ea5 100644 --- a/hr-shape/src/lib.rs +++ b/hr-shape/src/lib.rs @@ -112,6 +112,10 @@ pub struct Args { #[arg(long)] single_par: bool, + /// Output interim shaping results + #[arg(short = 'V', long)] + trace: bool, + /// Set text direction (ltr/rtl/ttb/btt) #[arg(long)] direction: Option, @@ -405,6 +409,21 @@ pub fn render(mut args: Args) -> Result { let features = &args.features; let mut shape_plan_cache = ShapePlanCache::default(); let mut reusable_buffer = Buffer::new(); + let trace_output = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + if args.trace { + let trace_output = trace_output.clone(); + reusable_buffer.set_message_function(move |buffer, font, message| { + let snapshot = buffer.serialize( + Some(font), + SerializeFlags::from_bits_truncate(format_flags), + ); + trace_output + .lock() + .unwrap() + .push(format!("trace: {message}\t{snapshot}\n")); + true + }); + } let text = if let Some(ref path) = args.text_file { if path == &PathBuf::from("-") { @@ -503,6 +522,11 @@ pub fn render(mut args: Args) -> Result { .features(features), ) .map_err(|e| format!("Error: {e}"))?; + if args.trace { + for trace in trace_output.lock().unwrap().drain(..) { + output.extend_from_slice(trace.as_bytes()); + } + } } &reusable_buffer }; From f8a6087adb9eb9527e7e2ab5f27d43df477097ed Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 9 Oct 2026 22:49:14 -0600 Subject: [PATCH 4/6] Finish opt-in tracing on the public buffer API Expose FnMut callbacks using Buffer and ShaperFont, and present substitution snapshots without changing live shaping state. Complete the tracing feature, CLI output, regression coverage, documentation, and no_std CI builds. --- .github/workflows/main.yml | 10 +- CHANGELOG.md | 6 + README.md | 20 ++ harfrust/src/aat/common.rs | 10 +- harfrust/src/aat/kern.rs | 22 ++- harfrust/src/aat/kerx.rs | 9 +- harfrust/src/aat/layout.rs | 26 ++- harfrust/src/aat/morx.rs | 6 +- harfrust/src/buffer.rs | 74 ++++---- harfrust/src/fallback.rs | 22 +-- harfrust/src/ot/apply.rs | 6 +- harfrust/src/ot/gpos/mark.rs | 14 +- harfrust/src/ot/gpos/mod.rs | 9 +- harfrust/src/ot/gpos/pair.rs | 49 +++-- harfrust/src/ot/gsub/alternate.rs | 8 +- harfrust/src/ot/gsub/ligature.rs | 25 ++- harfrust/src/ot/gsub/mod.rs | 9 +- harfrust/src/ot/gsub/multiple.rs | 30 +-- harfrust/src/ot/gsub/reverse_chain.rs | 8 +- harfrust/src/ot/gsub/single.rs | 16 +- harfrust/src/ot/layout.rs | 37 +++- harfrust/src/ot/map.rs | 8 + harfrust/src/ot/shaper/arabic_fallback.rs | 9 +- harfrust/src/ot/shaper/indic.rs | 4 +- harfrust/src/ot/shaper/myanmar.rs | 6 +- harfrust/src/ot/shaper/syllabic.rs | 3 +- harfrust/src/ot/shaper/use_.rs | 5 +- harfrust/src/shape.rs | 29 ++- harfrust/tests/shaping/main.rs | 23 ++- harfrust/tests/tracing.rs | 216 ++++++++++++++++++++++ hr-shape/README.md | 3 + hr-shape/src/lib.rs | 16 +- 32 files changed, 590 insertions(+), 148 deletions(-) create mode 100644 harfrust/tests/tracing.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cae01e05..533eeba2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,6 +26,9 @@ jobs: - name: Build with std run: cargo build + - name: Build library without tracing + run: cargo build -p harfrust + - name: MSRV Build (Rust 1.85) run: cargo +1.85 build @@ -54,8 +57,11 @@ jobs: run: cargo clippy --all-features --all-targets -- -D warnings check-no-std: - name: cargo check no std + name: cargo check no std (${{ matrix.features }}) runs-on: ubuntu-latest + strategy: + matrix: + features: [libm, "libm,tracing"] steps: - uses: actions/checkout@v6 @@ -67,4 +73,4 @@ jobs: target: thumbv7em-none-eabihf - name: cargo build harfrust - run: cargo build -p harfrust --target thumbv7em-none-eabihf --no-default-features --features=libm + run: cargo build -p harfrust --target thumbv7em-none-eabihf --no-default-features --features=${{ matrix.features }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c52a6c33..cff288d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added + +- Add opt-in `tracing` support with `Buffer::set_message_function` callbacks + and `hr-shape --trace` output, including intermediate Unicode and glyph + contents. Tracing also works without `std` and compiles out when disabled. + ### Fixed - Reject GSUB/GPOS lookups with an out-of-range `lookupType` instead of diff --git a/README.md b/README.md index 41a36744..e3207849 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,26 @@ HarfRust is less than 25% slower than HarfBuzz on most common fonts. For a compa You can run `cargo bench` to see the performance of HarfRust on your machine. +## Tracing shaping + +Enable the opt-in `tracing` Cargo feature to receive HarfBuzz shaping messages: + +```toml +harfrust = { version = "0.14", features = ["tracing"] } +``` + +Install a callback before shaping with `buffer.set_message_function`. It receives +the public `Buffer`, `ShaperFont`, and message text, and can inspect or serialize +intermediate Unicode and glyph contents. Return `true` to continue shaping, or +`false` from a `start` message to skip that operation. Mutable closures are +supported; callbacks own their captures and must be `Send + Sync + 'static`. +Use `buffer.clear_message_function()` to remove a callback. + +Tracing also works with `no_std` and `libm`. Without the `tracing` feature, the +callback storage, message formatting, and intermediate snapshots compile out. +The `hr-shape` tool enables tracing and exposes `--trace` to print messages +alongside intermediate buffer contents. + ## Notes about the port HarfRust is not a full port of HarfBuzz. HarfBuzz (C++ edition) can roughly be split into 6 parts: diff --git a/harfrust/src/aat/common.rs b/harfrust/src/aat/common.rs index deec7669..3d49a690 100644 --- a/harfrust/src/aat/common.rs +++ b/harfrust/src/aat/common.rs @@ -753,8 +753,14 @@ mod tests { buffer.clear_output(); buffer.next_glyph(); - let mut context = - AatApplyContext::new(&plan, shaper_font.layout(), Scale::default(), &mut buffer, #[cfg(feature = "tracing")] &shaper_font); + let mut context = AatApplyContext::new( + &plan, + shaper_font.layout(), + Scale::default(), + &mut buffer, + #[cfg(feature = "tracing")] + &shaper_font, + ); context.output_glyph(DELETED_GLYPH); assert_eq!(context.buffer.out_len, 2); diff --git a/harfrust/src/aat/kern.rs b/harfrust/src/aat/kern.rs index 1a96abb9..cc71cf6a 100644 --- a/harfrust/src/aat/kern.rs +++ b/harfrust/src/aat/kern.rs @@ -41,9 +41,18 @@ pub fn apply( buffer: &mut Buffer, #[cfg(feature = "tracing")] font: &crate::ShaperFont<'_, '_>, ) -> Option<()> { - let mut c = AatApplyContext::new(plan, layout, scale, buffer, #[cfg(feature = "tracing")] font); + let mut c = AatApplyContext::new( + plan, + layout, + scale, + buffer, + #[cfg(feature = "tracing")] + font, + ); - if !buffer_message!(c.buffer, font, "start table kern") { return None; } + if !buffer_message!(c.buffer, font, "start table kern") { + return None; + } c.setup_buffer_glyph_set(); @@ -143,7 +152,14 @@ fn machine_kern( F: Fn(u32, u32) -> i32, { buffer.unsafe_to_concat(None, None); - let mut ctx = ApplyContext::new(LayoutTableKind::Gpos, layout, scale, buffer, #[cfg(feature = "tracing")] font); + let mut ctx = ApplyContext::new( + LayoutTableKind::Gpos, + layout, + scale, + buffer, + #[cfg(feature = "tracing")] + font, + ); ctx.set_lookup_mask(kern_mask); ctx.lookup_props = u32::from(lookup_flags::IGNORE_MARKS); ctx.update_matchers(); diff --git a/harfrust/src/aat/kerx.rs b/harfrust/src/aat/kerx.rs index 0b6892b5..23a4d2b5 100644 --- a/harfrust/src/aat/kerx.rs +++ b/harfrust/src/aat/kerx.rs @@ -223,7 +223,14 @@ impl SimpleKerning for Subtable6<'_> { fn apply_simple_kerning(c: &mut AatApplyContext, subtable: &Subtable, kind: &T) { let scale = c.scale; - let mut ctx = ApplyContext::new(LayoutTableKind::Gpos, c.layout, c.scale, c.buffer, #[cfg(feature = "tracing")] c.font); + let mut ctx = ApplyContext::new( + LayoutTableKind::Gpos, + c.layout, + c.scale, + c.buffer, + #[cfg(feature = "tracing")] + c.font, + ); ctx.set_lookup_mask(c.plan.kern_mask); ctx.lookup_props = u32::from(lookup_flags::IGNORE_MARKS); ctx.update_matchers(); diff --git a/harfrust/src/aat/layout.rs b/harfrust/src/aat/layout.rs index 37bb0dcb..7ddfb4fb 100644 --- a/harfrust/src/aat/layout.rs +++ b/harfrust/src/aat/layout.rs @@ -514,7 +514,14 @@ pub fn substitute( builder.compile(layout.aat, &mut aat_map); } - let mut c = AatApplyContext::new(plan, layout, Scale::default(), buffer, #[cfg(feature = "tracing")] font); + let mut c = AatApplyContext::new( + plan, + layout, + Scale::default(), + buffer, + #[cfg(feature = "tracing")] + font, + ); message_return!(c, "start table morx"); morx::apply( &mut c, @@ -545,8 +552,21 @@ pub fn remove_deleted_glyphs(buffer: &mut Buffer) { /// /// See #[doc(alias = "hb_aat_layout_position")] -pub fn position(plan: &ShapePlan, layout: LayoutData<'_>, scale: Scale, buffer: &mut Buffer, #[cfg(feature = "tracing")] font: &crate::ShaperFont<'_, '_>) { - let mut c = AatApplyContext::new(plan, layout, scale, buffer, #[cfg(feature = "tracing")] font); +pub fn position( + plan: &ShapePlan, + layout: LayoutData<'_>, + scale: Scale, + buffer: &mut Buffer, + #[cfg(feature = "tracing")] font: &crate::ShaperFont<'_, '_>, +) { + let mut c = AatApplyContext::new( + plan, + layout, + scale, + buffer, + #[cfg(feature = "tracing")] + font, + ); message_return!(c, "start table kerx"); kerx::apply(&mut c); message!(c, "end table kerx"); diff --git a/harfrust/src/aat/morx.rs b/harfrust/src/aat/morx.rs index f97f2afa..22fc3b1d 100644 --- a/harfrust/src/aat/morx.rs +++ b/harfrust/src/aat/morx.rs @@ -241,7 +241,11 @@ fn apply_table<'a>( c.safe_to_break = safe_to_break.subtable(subtable_cache.safe_to_break)?; if !c.buffer_intersects_machine() { - message!(c, "skipped chainsubtable {} because no glyph matches", subtable_idx); + message!( + c, + "skipped chainsubtable {} because no glyph matches", + subtable_idx + ); continue; } message_continue!(c, "start chainsubtable {}", subtable_idx); diff --git a/harfrust/src/buffer.rs b/harfrust/src/buffer.rs index 64fbcbe8..4fe2548c 100644 --- a/harfrust/src/buffer.rs +++ b/harfrust/src/buffer.rs @@ -4,9 +4,9 @@ use crate::set_digest::SetDigest; use crate::unicode::{CharExt, Codepoint}; use crate::U32Set; use crate::{BufferFlags, ClusterLevel, Direction, Language, Script, SerializeFlags}; -use alloc::{string::String, vec::Vec}; #[cfg(feature = "tracing")] use alloc::boxed::Box; +use alloc::{string::String, vec::Vec}; use core::cmp::min; use read_fonts::types::{GlyphId, GlyphId16}; @@ -1284,25 +1284,6 @@ impl Buffer { true } - // Make the output produced so far visible to callbacks without ending the - // current substitution pass. The input cursor can move in either direction. - #[cfg(feature = "tracing")] - pub(crate) fn sync_so_far(&mut self) -> isize { - if !self.have_output || !self.successful { - return 0; - } - let old_idx = self.idx; - let out_idx = self.out_len; - if !self.sync() { - return 0; - } - self.idx = out_idx; - self.have_output = true; - self.out_len = out_idx; - debug_assert!(self.idx <= self.len); - self.idx as isize - old_idx as isize - } - pub(crate) fn clear_output(&mut self) { self.have_output = true; self.have_positions = false; @@ -2179,7 +2160,34 @@ impl Buffer { #[cfg(feature = "tracing")] pub(crate) fn message(&mut self, font: &crate::ShaperFont<'_, '_>, msg: &str) -> bool { if let Some(mut function) = self.message_function.take() { - let ret = function(self, font, msg); + // Substitution keeps separate input/output cursors. Present the + // emitted prefix followed by the remaining input without syncing + // the live buffer, which could change its allocation-limit behavior. + let ret = if self.have_output && (self.have_separate_output || self.out_len != self.idx) + { + let mut info = Vec::with_capacity(self.out_len + self.len - self.idx); + info.extend_from_slice(&self.out_info()[..self.out_len]); + info.extend_from_slice(&self.info[self.idx..self.len]); + let snapshot = Buffer { + flags: self.flags, + cluster_level: self.cluster_level, + invisible: self.invisible, + not_found_variation_selector: self.not_found_variation_selector, + content_type: self.content_type, + direction: self.direction, + script: self.script, + language: self.language.clone(), + successful: self.successful, + len: info.len(), + info, + context: self.context, + context_len: self.context_len, + ..Buffer::new() + }; + function(&snapshot, font, msg) + } else { + function(self, font, msg) + }; self.message_function = Some(function); ret } else { @@ -2187,6 +2195,16 @@ impl Buffer { } } + // The cursor position in the logical buffer shown to message callbacks. + #[cfg(feature = "tracing")] + pub(crate) fn message_idx(&self) -> usize { + if self.have_output { + self.out_len + } else { + self.idx + } + } + #[cfg(feature = "tracing")] #[inline] pub(crate) fn messaging(&self) -> bool { @@ -2211,7 +2229,9 @@ impl Buffer { write!(&mut s, "={}", info.cluster)?; } } - if !s.is_empty() { s.push(']'); } + if !s.is_empty() { + s.push(']'); + } return Ok(s); } let pos = self.glyph_positions(); @@ -2324,16 +2344,6 @@ macro_rules! message { }; } -macro_rules! message_sync { - ($ctx:expr, $($arg:tt)*) => {{ - #[cfg(feature = "tracing")] - if $ctx.buffer.messaging() { - $ctx.buffer.sync_so_far(); - message!($ctx, $($arg)*); - } - }}; -} - macro_rules! message_return { ($ctx:expr, $($arg:tt)*) => { if !message!($ctx, $($arg)*) { return; } diff --git a/harfrust/src/fallback.rs b/harfrust/src/fallback.rs index 4f27a77b..750993b0 100644 --- a/harfrust/src/fallback.rs +++ b/harfrust/src/fallback.rs @@ -95,18 +95,14 @@ fn recategorize_combining_class(u: u32, mut class: u8) -> u8 { } } -pub fn recategorize_marks(buffer: &mut Buffer, #[cfg(feature = "tracing")] font: &ShaperFont<'_, '_>) { - if buffer_message!(buffer, font, "start fallback mark") { - let len = buffer.len; - for info in &mut buffer.info[..len] { - if info.general_category() == GeneralCategory::NON_SPACING_MARK { - let mut class = info.modified_combining_class(); - class = recategorize_combining_class(info.glyph_id, class); - info.set_modified_combining_class(class); - } +pub fn recategorize_marks(buffer: &mut Buffer) { + let len = buffer.len; + for info in &mut buffer.info[..len] { + if info.general_category() == GeneralCategory::NON_SPACING_MARK { + let mut class = info.modified_combining_class(); + class = recategorize_combining_class(info.glyph_id, class); + info.set_modified_combining_class(class); } - - buffer_message!(buffer, font, "end fallback mark"); } } @@ -426,6 +422,9 @@ pub fn position_marks<'x>( buffer: &'x mut Buffer, adjust_offsets_when_zeroing: bool, ) { + if !buffer_message!(buffer, font, "start fallback mark") { + return; + } let mut ctx = FallbackShapeContext { plan, units_per_em: font.layout().units_per_em, @@ -450,6 +449,7 @@ pub fn position_marks<'x>( } position_cluster(&mut ctx, start, len); + message!(ctx, "end fallback mark"); } pub fn fallback_kern(_: &ShapePlan, _: &mut Buffer) { diff --git a/harfrust/src/ot/apply.rs b/harfrust/src/ot/apply.rs index 33a0aef1..de692189 100644 --- a/harfrust/src/ot/apply.rs +++ b/harfrust/src/ot/apply.rs @@ -575,18 +575,18 @@ pub(crate) fn apply_lookup( break; } - message_sync!( + message!( ctx, "recursing to lookup {} at {}", record.lookup_list_index.get(), - ctx.buffer.idx + ctx.buffer.message_idx() ); if ctx.recurse(record.lookup_list_index.get()).is_none() { continue; } - message_sync!(ctx, "recursed to lookup {}", record.lookup_list_index.get(),); + message!(ctx, "recursed to lookup {}", record.lookup_list_index.get(),); let new_len = ctx.buffer.backtrack_len() + ctx.buffer.lookahead_len(); let mut delta = new_len as isize - orig_len as isize; diff --git a/harfrust/src/ot/gpos/mark.rs b/harfrust/src/ot/gpos/mark.rs index c3ce71d8..6c5fd9d3 100644 --- a/harfrust/src/ot/gpos/mark.rs +++ b/harfrust/src/ot/gpos/mark.rs @@ -80,7 +80,12 @@ impl MarkArrayExt for MarkArray<'_> { ctx.buffer.idx += 1; return Some(()); }; - message!(ctx, "attaching mark glyph at {} to glyph at {}", idx, glyph_pos); + message!( + ctx, + "attaching mark glyph at {} to glyph at {}", + idx, + glyph_pos + ); let pos = ctx.buffer.cur_pos_mut(); pos.x_offset = x_offset; pos.y_offset = y_offset; @@ -91,7 +96,12 @@ impl MarkArrayExt for MarkArray<'_> { } pos.set_attach_type(attach_type::MARK); pos.set_attach_chain(chain); - message!(ctx, "attached mark glyph at {} to glyph at {}", idx, glyph_pos); + message!( + ctx, + "attached mark glyph at {} to glyph at {}", + idx, + glyph_pos + ); ctx.buffer.scratch_flags |= HB_BUFFER_SCRATCH_FLAG_HAS_GPOS_ATTACHMENT; ctx.buffer.idx += 1; diff --git a/harfrust/src/ot/gpos/mod.rs b/harfrust/src/ot/gpos/mod.rs index 66cc9e38..f5638c2c 100644 --- a/harfrust/src/ot/gpos/mod.rs +++ b/harfrust/src/ot/gpos/mod.rs @@ -14,8 +14,13 @@ use crate::ShaperFont; pub fn position(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) { #[cfg(feature = "tracing")] - let tag = plan.ot_map.chosen_script(LayoutTableKind::Gpos).unwrap_or(crate::Tag::new(b"DFLT")); - if !buffer_message!(buffer, font, "start table GPOS script tag '{tag}'") { return; } + let tag = plan + .ot_map + .chosen_script(LayoutTableKind::Gpos) + .unwrap_or(crate::Tag::new(b"DFLT")); + if !buffer_message!(buffer, font, "start table GPOS script tag '{tag}'") { + return; + } let table = font.layout().ot.gpos.clone(); apply_layout_table(plan, font, buffer, table.as_ref()); buffer_message!(buffer, font, "end table GPOS script tag '{tag}'"); diff --git a/harfrust/src/ot/gpos/pair.rs b/harfrust/src/ot/gpos/pair.rs index e1522fa9..87cd35d7 100644 --- a/harfrust/src/ot/gpos/pair.rs +++ b/harfrust/src/ot/gpos/pair.rs @@ -94,7 +94,11 @@ impl Apply for PairPosFormat1<'_> { let success = |ctx: &mut ApplyContext, iter_index: &mut usize, flag1, flag2, has_record2| { if flag1 || flag2 { - message!(ctx, "kerned glyphs at {} and {second_glyph_index}", ctx.buffer.idx); + message!( + ctx, + "kerned glyphs at {} and {second_glyph_index}", + ctx.buffer.idx + ); ctx.buffer .unsafe_to_break(Some(ctx.buffer.idx), Some(second_glyph_index + 1)); } else { @@ -103,7 +107,11 @@ impl Apply for PairPosFormat1<'_> { ctx.buffer .unsafe_to_concat(Some(ctx.buffer.idx), Some(second_glyph_index + 1)); } - message!(ctx, "tried kerning glyphs at {} and {second_glyph_index}", ctx.buffer.idx); + message!( + ctx, + "tried kerning glyphs at {} and {second_glyph_index}", + ctx.buffer.idx + ); finish(ctx, iter_index, has_record2) }; @@ -137,10 +145,13 @@ impl Apply for PairPosFormat1<'_> { } else if glyph_id > second_glyph { hi = mid; } else { - message!(ctx, "try kerning glyphs at {} and {second_glyph_index}", ctx.buffer.idx); + message!( + ctx, + "try kerning glyphs at {} and {second_glyph_index}", + ctx.buffer.idx + ); let has_record2 = !format2.is_empty(); - message!(ctx, "try kerning glyphs at {} and {second_glyph_index}", ctx.buffer.idx); - let worked1 = !format1.is_empty() + let worked1 = !format1.is_empty() && super::value::apply( ctx, ctx.buffer.idx, @@ -232,22 +243,26 @@ impl Apply for PairPosFormat2<'_> { Some(()) }; - let boring = |ctx: &mut ApplyContext, iter_index: &mut usize, has_record2| { - ctx.buffer - .unsafe_to_concat(Some(ctx.buffer.idx), Some(second_glyph_index + 1)); - finish(ctx, iter_index, has_record2) - }; - let success = |ctx: &mut ApplyContext, iter_index: &mut usize, flag1, flag2, has_record2| { if flag1 || flag2 { - message!(ctx, "kerned glyphs at {} and {second_glyph_index}", ctx.buffer.idx); + message!( + ctx, + "kerned glyphs at {} and {second_glyph_index}", + ctx.buffer.idx + ); ctx.buffer .unsafe_to_break(Some(ctx.buffer.idx), Some(second_glyph_index + 1)); - finish(ctx, iter_index, has_record2) } else { - boring(ctx, iter_index, has_record2) + ctx.buffer + .unsafe_to_concat(Some(ctx.buffer.idx), Some(second_glyph_index + 1)); } + message!( + ctx, + "tried kerning glyphs at {} and {second_glyph_index}", + ctx.buffer.idx + ); + finish(ctx, iter_index, has_record2) }; let data = self.offset_data(); let (class1, class2) = match external_cache { @@ -282,7 +297,11 @@ impl Apply for PairPosFormat2<'_> { + (class2 as usize * record_size) + self.class1_records_byte_range().start; let has_record2 = !format2.is_empty(); - message!(ctx, "try kerning glyphs at {} and {second_glyph_index}", ctx.buffer.idx); + message!( + ctx, + "try kerning glyphs at {} and {second_glyph_index}", + ctx.buffer.idx + ); let worked1 = !format1.is_empty() && super::value::apply(ctx, ctx.buffer.idx, &data, record_offset, format1) == Some(true); diff --git a/harfrust/src/ot/gsub/alternate.rs b/harfrust/src/ot/gsub/alternate.rs index ab84822c..cce6fdc3 100644 --- a/harfrust/src/ot/gsub/alternate.rs +++ b/harfrust/src/ot/gsub/alternate.rs @@ -26,16 +26,16 @@ impl Apply for AlternateSet<'_> { let idx = u16::try_from(alt_index).ok()?.checked_sub(1)?; - message_sync!( + message!( ctx, "replacing glyph at {} (alternate substitution)", - ctx.buffer.idx, + ctx.buffer.message_idx(), ); ctx.replace_glyph(alternates.get(idx as usize)?.get().into()); - message_sync!( + message!( ctx, "replaced glyph at {} (alternate substitution)", - ctx.buffer.idx - 1, + ctx.buffer.message_idx() - 1, ); Some(()) diff --git a/harfrust/src/ot/gsub/ligature.rs b/harfrust/src/ot/gsub/ligature.rs index a415079b..4bd520bd 100644 --- a/harfrust/src/ot/gsub/ligature.rs +++ b/harfrust/src/ot/gsub/ligature.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "tracing")] -use alloc::string::String; use crate::buffer::GlyphInfo; use crate::ot::apply::ApplyContext; use crate::ot::apply::{ @@ -10,6 +8,8 @@ use crate::ot::apply::{ use crate::ot::{coverage_index, coverage_index_cached, CoverageInfo}; use crate::set_digest::SetDigest; use alloc::boxed::Box; +#[cfg(feature = "tracing")] +use alloc::string::String; use read_fonts::tables::gsub::{Ligature, LigatureSet, LigatureSubstFormat1}; use read_fonts::types::GlyphId; @@ -36,16 +36,16 @@ impl Apply for Ligature<'_> { // as a "ligated" substitution. let components = self.component_glyph_ids(); if components.is_empty() { - message_sync!( + message!( ctx, "replacing glyph at {} (ligature substitution)", - ctx.buffer.idx + ctx.buffer.message_idx() ); ctx.replace_glyph(self.ligature_glyph().into()); - message_sync!( + message!( ctx, "replaced glyph at {} (ligature substitution)", - ctx.buffer.idx - 1, + ctx.buffer.message_idx() - 1, ); Some(()) } else { @@ -73,18 +73,15 @@ impl Apply for Ligature<'_> { let mut pos = 0; #[cfg(feature = "tracing")] if ctx.buffer.messaging() { - let delta = ctx.buffer.sync_so_far(); - pos = ctx.buffer.idx; - let count = components.len(); - match_end = match_end.checked_add_signed(delta)?; + pos = ctx.buffer.message_idx(); let mut msg = String::from("ligating glyphs at "); - for i in 0..=count { - ctx.match_positions[i] = (ctx.match_positions[i] as usize).checked_add_signed(delta)? as u32; + for i in 0..count { + let index = ctx.match_positions[i] as usize - ctx.buffer.idx + pos; if i > 0 { msg.push(','); } use core::fmt::Write; - let _ = write!(msg, "{}", ctx.match_positions[i]); + let _ = write!(msg, "{index}"); } ctx.buffer.message(ctx.font, &msg); } @@ -95,7 +92,7 @@ impl Apply for Ligature<'_> { total_component_count, self.ligature_glyph().into(), ); - message_sync!(ctx, "ligated glyph at {}", pos); + message!(ctx, "ligated glyph at {}", pos); Some(()) } } diff --git a/harfrust/src/ot/gsub/mod.rs b/harfrust/src/ot/gsub/mod.rs index c1147648..bf516254 100644 --- a/harfrust/src/ot/gsub/mod.rs +++ b/harfrust/src/ot/gsub/mod.rs @@ -14,8 +14,13 @@ use crate::ShaperFont; pub fn substitute(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) { #[cfg(feature = "tracing")] - let tag = plan.ot_map.chosen_script(LayoutTableKind::Gsub).unwrap_or(crate::Tag::new(b"DFLT")); - if !buffer_message!(buffer, font, "start table GSUB script tag '{tag}'") { return; } + let tag = plan + .ot_map + .chosen_script(LayoutTableKind::Gsub) + .unwrap_or(crate::Tag::new(b"DFLT")); + if !buffer_message!(buffer, font, "start table GSUB script tag '{tag}'") { + return; + } let table = font.layout().ot.gsub.clone(); apply_layout_table(plan, font, buffer, table.as_ref()); buffer_message!(buffer, font, "end table GSUB script tag '{tag}'"); diff --git a/harfrust/src/ot/gsub/multiple.rs b/harfrust/src/ot/gsub/multiple.rs index 982186e7..67f057cb 100644 --- a/harfrust/src/ot/gsub/multiple.rs +++ b/harfrust/src/ot/gsub/multiple.rs @@ -1,8 +1,8 @@ -#[cfg(feature = "tracing")] -use alloc::string::String; use crate::buffer::GlyphPropsFlags; use crate::ot::apply::ApplyContext; use crate::ot::apply::{Apply, WouldApply, WouldApplyContext}; +#[cfg(feature = "tracing")] +use alloc::string::String; use read_fonts::tables::gsub::MultipleSubstFormat1; impl WouldApply for MultipleSubstFormat1<'_> { @@ -23,37 +23,37 @@ impl Apply for MultipleSubstFormat1<'_> { // Spec disallows this, but Uniscribe allows it. // https://github.com/harfbuzz/harfbuzz/issues/253 0 => { - message_sync!( + message!( ctx, "deleting glyph at {} (multiple substitution)", - ctx.buffer.idx + ctx.buffer.message_idx() ); ctx.buffer.delete_glyph(); - message_sync!( + message!( ctx, "deleted glyph at {} (multiple substitution)", - ctx.buffer.idx, + ctx.buffer.message_idx(), ); } // Special-case to make it in-place and not consider this // as a "multiplied" substitution. 1 => { - message_sync!( + message!( ctx, "replacing glyph at {} (multiple substitution)", - ctx.buffer.idx + ctx.buffer.message_idx() ); ctx.replace_glyph(substs.first()?.get().into()); - message_sync!( + message!( ctx, "replaced glyph at {} (multiple substitution)", - ctx.buffer.idx - 1, + ctx.buffer.message_idx() - 1, ); } _ => { - message_sync!(ctx, "multiplying glyph at {}", ctx.buffer.idx); + message!(ctx, "multiplying glyph at {}", ctx.buffer.message_idx()); let class = if ctx.buffer.cur(0).is_ligature() { GlyphPropsFlags::BASE_GLYPH } else { @@ -75,12 +75,12 @@ impl Apply for MultipleSubstFormat1<'_> { ctx.buffer.skip_glyph(); #[cfg(feature = "tracing")] - if ctx.buffer.messaging() { - ctx.buffer.sync_so_far(); + if ctx.buffer.messaging() && ctx.buffer.successful { let count = substs.len(); + let end = ctx.buffer.message_idx(); let mut msg = String::from("multiplied glyphs at "); - for i in (ctx.buffer.idx - count)..ctx.buffer.idx { - if i > (ctx.buffer.idx - count) { + for i in (end - count)..end { + if i > (end - count) { msg.push(','); } use core::fmt::Write; diff --git a/harfrust/src/ot/gsub/reverse_chain.rs b/harfrust/src/ot/gsub/reverse_chain.rs index 14dee077..65eea8b9 100644 --- a/harfrust/src/ot/gsub/reverse_chain.rs +++ b/harfrust/src/ot/gsub/reverse_chain.rs @@ -62,18 +62,18 @@ impl Apply for ReverseChainSingleSubstFormat1<'_> { ctx.buffer.idx + 1, &mut end_index, ) { - message_sync!( + message!( ctx, "replacing glyph at {} (reverse chaining substitution)", - ctx.buffer.idx + ctx.buffer.message_idx() ); ctx.buffer .unsafe_to_break_from_outbuffer(Some(start_index), Some(end_index)); ctx.replace_glyph_inplace(subst.into()); - message_sync!( + message!( ctx, "replaced glyph at {} (reverse chaining substitution)", - ctx.buffer.idx + ctx.buffer.message_idx() ); // Note: We DON'T decrease buffer.idx. The main loop does it // for us. This is useful for preventing surprises if someone diff --git a/harfrust/src/ot/gsub/single.rs b/harfrust/src/ot/gsub/single.rs index 4e4ec65e..28a484fc 100644 --- a/harfrust/src/ot/gsub/single.rs +++ b/harfrust/src/ot/gsub/single.rs @@ -14,16 +14,16 @@ impl Apply for SingleSubstFormat1<'_> { let glyph = ctx.buffer.cur(0).as_glyph(); self.coverage().ok()?.get(glyph)?; let subst = (glyph.to_u32() as i32 + self.delta_glyph_id() as i32) as u16; - message_sync!( + message!( ctx, "replacing glyph at {} (single substitution)", - ctx.buffer.idx + ctx.buffer.message_idx() ); ctx.replace_glyph(subst.into()); - message_sync!( + message!( ctx, "replaced glyph at {} (single substitution)", - ctx.buffer.idx - 1 + ctx.buffer.message_idx() - 1 ); Some(()) } @@ -43,16 +43,16 @@ impl Apply for SingleSubstFormat2<'_> { let glyph = ctx.buffer.cur(0).as_glyph(); let index = self.coverage().ok()?.get(glyph)? as usize; let subst = self.substitute_glyph_ids().get(index)?.get().to_u16(); - message_sync!( + message!( ctx, "replacing glyph at {} (single substitution)", - ctx.buffer.idx + ctx.buffer.message_idx() ); ctx.replace_glyph(subst.into()); - message_sync!( + message!( ctx, "replaced glyph at {} (single substitution)", - ctx.buffer.idx - 1 + ctx.buffer.message_idx() - 1 ); Some(()) } diff --git a/harfrust/src/ot/layout.rs b/harfrust/src/ot/layout.rs index 8235b274..1114c9a4 100644 --- a/harfrust/src/ot/layout.rs +++ b/harfrust/src/ot/layout.rs @@ -161,7 +161,14 @@ pub fn apply_layout_table( buffer: &mut Buffer, table: Option<&T>, ) { - let mut ctx = ApplyContext::new(T::KIND, font.layout(), font.scale, buffer, #[cfg(feature = "tracing")] font); + let mut ctx = ApplyContext::new( + T::KIND, + font.layout(), + font.scale, + buffer, + #[cfg(feature = "tracing")] + font, + ); for (stage_index, stage) in plan.ot_map.stages(T::KIND).iter().enumerate() { if let Some(table) = table { @@ -170,7 +177,12 @@ pub fn apply_layout_table( continue; }; - message_continue!(ctx, "start lookup {}", lookup_map.index); + message_continue!( + ctx, + "start lookup {} feature '{}'", + lookup_map.index, + lookup_map.feature_tag + ); if lookup.digest().may_intersect(&ctx.buffer.digest) && (ctx @@ -188,12 +200,27 @@ pub fn apply_layout_table( ctx.per_syllable = lookup_map.per_syllable; if !apply_string::(&mut ctx, lookup) { - message!(ctx, "skipped lookup {} because no glyph matches", lookup_map.index); + message!( + ctx, + "skipped lookup {} feature '{}' because no glyph matches", + lookup_map.index, + lookup_map.feature_tag + ); } } else { - message!(ctx, "skipped lookup {} because no glyph matches", lookup_map.index); + message!( + ctx, + "skipped lookup {} feature '{}' because no glyph matches", + lookup_map.index, + lookup_map.feature_tag + ); } - message!(ctx, "end lookup {}", lookup_map.index); + message!( + ctx, + "end lookup {} feature '{}'", + lookup_map.index, + lookup_map.feature_tag + ); } } diff --git a/harfrust/src/ot/map.rs b/harfrust/src/ot/map.rs index 08b332a5..c41ad036 100644 --- a/harfrust/src/ot/map.rs +++ b/harfrust/src/ot/map.rs @@ -60,6 +60,8 @@ pub struct LookupMap { pub random: bool, pub mask: Mask, pub per_syllable: bool, + #[cfg(feature = "tracing")] + pub feature_tag: Tag, } #[derive(Clone, Copy)] @@ -593,6 +595,10 @@ impl<'a> OtMapBuilder<'a> { ) -> Option<()> { let table = self.layout.ot.layout_table(table_index)?; + #[cfg(feature = "tracing")] + let feature_tag = table + .feature_tag(feature_index) + .unwrap_or(Tag::new(b"DFLT")); let lookup_count = table.lookup_count(); let feature = match variation_index { Some(idx) => table @@ -611,6 +617,8 @@ impl<'a> OtMapBuilder<'a> { random, mask, per_syllable, + #[cfg(feature = "tracing")] + feature_tag, }); } } diff --git a/harfrust/src/ot/shaper/arabic_fallback.rs b/harfrust/src/ot/shaper/arabic_fallback.rs index 1be0f37a..21b25077 100644 --- a/harfrust/src/ot/shaper/arabic_fallback.rs +++ b/harfrust/src/ot/shaper/arabic_fallback.rs @@ -211,7 +211,14 @@ impl FallbackPlan { } pub(crate) fn apply(&self, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) { - let mut ctx = ApplyContext::new(LayoutTableKind::Gsub, font.layout(), font.scale, buffer, #[cfg(feature = "tracing")] font); + let mut ctx = ApplyContext::new( + LayoutTableKind::Gsub, + font.layout(), + font.scale, + buffer, + #[cfg(feature = "tracing")] + font, + ); for lookup in &self.lookups { ctx.set_lookup_mask(lookup.mask); apply_synthesized_subst_lookup(&mut ctx, &lookup.info, &lookup.data); diff --git a/harfrust/src/ot/shaper/indic.rs b/harfrust/src/ot/shaper/indic.rs index 57ebb892..fd1fd516 100644 --- a/harfrust/src/ot/shaper/indic.rs +++ b/harfrust/src/ot/shaper/indic.rs @@ -580,7 +580,9 @@ fn initial_reordering(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut use super::indic_machine::SyllableType; let mut ret = false; - if !buffer_message!(buffer, font, "start reordering indic initial") { return ret; } + if !buffer_message!(buffer, font, "start reordering indic initial") { + return ret; + } let indic_plan = plan.data::(); diff --git a/harfrust/src/ot/shaper/myanmar.rs b/harfrust/src/ot/shaper/myanmar.rs index f10bffc8..e96ddaa2 100644 --- a/harfrust/src/ot/shaper/myanmar.rs +++ b/harfrust/src/ot/shaper/myanmar.rs @@ -133,7 +133,11 @@ fn reorder_myanmar(_: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer use super::myanmar_machine::SyllableType; let mut ret = false; - if !buffer_message!(buffer, font, "start reordering myanmar") { return ret; } + if !buffer_message!(buffer, font, "start reordering myanmar") { + buffer.deallocate_var(GlyphInfo::MYANMAR_CATEGORY_VAR); + buffer.deallocate_var(GlyphInfo::MYANMAR_POSITION_VAR); + return ret; + } if insert_dotted_circles( font, diff --git a/harfrust/src/ot/shaper/syllabic.rs b/harfrust/src/ot/shaper/syllabic.rs index 6444505c..7c863521 100644 --- a/harfrust/src/ot/shaper/syllabic.rs +++ b/harfrust/src/ot/shaper/syllabic.rs @@ -20,7 +20,8 @@ pub fn insert_dotted_circles( } if (buffer.scratch_flags & HB_BUFFER_SCRATCH_FLAG_HAS_BROKEN_SYLLABLE) == 0 { - buffer_message!(buffer, + buffer_message!( + buffer, font, "skipped inserting dotted-circles because there is no broken syllables", ); diff --git a/harfrust/src/ot/shaper/use_.rs b/harfrust/src/ot/shaper/use_.rs index 6ada5553..de6c693f 100644 --- a/harfrust/src/ot/shaper/use_.rs +++ b/harfrust/src/ot/shaper/use_.rs @@ -379,7 +379,10 @@ fn reorder_use(_: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) -> use super::use_machine::SyllableType; let mut ret = false; - if !buffer_message!(buffer, font, "start reordering USE") { return ret; } + if !buffer_message!(buffer, font, "start reordering USE") { + buffer.deallocate_var(GlyphInfo::USE_CATEGORY_VAR); + return ret; + } if insert_dotted_circles( font, diff --git a/harfrust/src/shape.rs b/harfrust/src/shape.rs index b8b7b173..78ade880 100644 --- a/harfrust/src/shape.rs +++ b/harfrust/src/shape.rs @@ -181,7 +181,7 @@ impl OtShapeContext<'_, '_, '_> { // This is unfortunate to go here, but necessary... if self.plan.fallback_mark_positioning { - fallback::recategorize_marks(self.buffer, #[cfg(feature = "tracing")] self.font); + fallback::recategorize_marks(self.buffer); } map_glyphs_fast(self.buffer); @@ -200,7 +200,14 @@ impl OtShapeContext<'_, '_, '_> { synthesize_glyph_classes(self.buffer); } - aat::layout::substitute(self.plan, self.font.layout(), self.buffer, self.features, #[cfg(feature = "tracing")] self.font); + aat::layout::substitute( + self.plan, + self.font.layout(), + self.buffer, + self.features, + #[cfg(feature = "tracing")] + self.font, + ); // The digest is only read by the OT lookup-apply loop; without // GPOS ahead, nothing consumes it. if self.plan.apply_gpos { @@ -306,10 +313,24 @@ impl OtShapeContext<'_, '_, '_> { if plan.apply_gpos { gpos::position(plan, self.font, buffer); } else if plan.apply_kerx { - aat::layout::position(plan, layout, self.font.scale, buffer, #[cfg(feature = "tracing")] self.font); + aat::layout::position( + plan, + layout, + self.font.scale, + buffer, + #[cfg(feature = "tracing")] + self.font, + ); } if plan.apply_kern { - aat::kern::apply(plan, layout, self.font.scale, buffer, #[cfg(feature = "tracing")] self.font); + aat::kern::apply( + plan, + layout, + self.font.scale, + buffer, + #[cfg(feature = "tracing")] + self.font, + ); } else if plan.apply_fallback_kern { fallback::fallback_kern(plan, buffer); } diff --git a/harfrust/tests/shaping/main.rs b/harfrust/tests/shaping/main.rs index f5e473d7..5b3e7dd0 100644 --- a/harfrust/tests/shaping/main.rs +++ b/harfrust/tests/shaping/main.rs @@ -8,8 +8,27 @@ mod regressions; mod text_rendering_tests; pub fn shape(font_path: &str, text: &str, options: &str) -> String { - hr_shape::shape(font_path, text, options) + let output = hr_shape::shape(font_path, text, options) .unwrap_or_else(|err| panic!("hr-shape failed: {err}")) .trim_end() - .to_string() + .to_string(); + // Exercise tracing on the whole shaping corpus, including contextual GSUB, + // length-changing substitutions, script reordering, and AAT state machines. + #[cfg(feature = "tracing")] + if text.len() <= 1024 && !font_path.ends_with("TestGSUBThree.ttf") { + // Full snapshots at every operation are quadratic for the long-run + // regression and the malformed font that expands to the shaping limit. + let traced = hr_shape::shape(font_path, text, &format!("{options} --trace")) + .unwrap_or_else(|err| panic!("hr-shape tracing failed: {err}")); + let result = traced + .lines() + .filter(|line| !line.starts_with("trace: ")) + .collect::>() + .join("\n"); + assert_eq!( + result, output, + "tracing changed shaping: {font_path} {text:?} {options}" + ); + } + output } diff --git a/harfrust/tests/tracing.rs b/harfrust/tests/tracing.rs new file mode 100644 index 00000000..eba788b2 --- /dev/null +++ b/harfrust/tests/tracing.rs @@ -0,0 +1,216 @@ +#![cfg(feature = "tracing")] + +use harfrust::{ + font::Font, shape, Buffer, ContentType, Feature, SerializeFlags, ShapeOptions, ShaperFont, +}; +use std::sync::{Arc, Mutex}; + +fn instance() -> Font { + Font::new( + include_bytes!("../benches/fonts/Roboto-Regular.ttf").to_vec(), + 0, + ) + .unwrap() + .instance_builder() + .build() +} + +fn buffer(text: &str) -> Buffer { + let mut buffer = Buffer::new(); + buffer.push_str(text); + buffer.guess_segment_properties(); + buffer +} + +#[test] +fn callback_observes_unicode_glyphs_and_positions() { + let instance = instance(); + let font = ShaperFont::new(&instance); + let mut buffer = buffer("o\u{0301}ffice AV"); + let messages = Arc::new(Mutex::new(Vec::new())); + let captured = messages.clone(); + let mut calls = 0; + buffer.set_message_function(move |buffer, font, message| { + calls += 1; + assert_eq!(buffer.glyph_infos().len(), buffer.len()); + assert!( + buffer.glyph_positions().is_empty() || buffer.glyph_positions().len() == buffer.len() + ); + let snapshot = buffer.serialize(Some(font), SerializeFlags::default()); + assert!(!snapshot.is_empty()); + captured.lock().unwrap().push(( + calls, + message.to_string(), + buffer.content_type(), + !buffer.glyph_positions().is_empty(), + snapshot, + )); + true + }); + shape(&font, &mut buffer, ShapeOptions::new()).unwrap(); + let messages = messages.lock().unwrap(); + assert!(messages + .iter() + .any(|(_, message, content, positioned, snapshot)| { + message == "start reorder" + && *content == Some(ContentType::Unicode) + && !positioned + && snapshot.contains("U+") + })); + assert!(messages.iter().any(|(_, message, content, positioned, _)| { + message.starts_with("start table GSUB") + && *content == Some(ContentType::Glyphs) + && !positioned + })); + assert!(messages.iter().any(|(_, message, _, positioned, _)| { + message.starts_with("end table GPOS") && *positioned + })); + assert_eq!(messages.last().unwrap().0, messages.len()); +} + +#[test] +fn rejecting_gsub_matches_disabling_ligatures() { + let instance = instance(); + let font = ShaperFont::new(&instance); + let mut skipped = buffer("ffi"); + skipped.set_message_function(|_, _, message| !message.starts_with("start table GSUB")); + shape(&font, &mut skipped, ShapeOptions::new()).unwrap(); + + let mut disabled = buffer("ffi"); + let features = ["liga=0".parse::().unwrap()]; + shape( + &font, + &mut disabled, + ShapeOptions::new().features(&features), + ) + .unwrap(); + assert_eq!( + skipped.serialize(Some(&font), SerializeFlags::default()), + disabled.serialize(Some(&font), SerializeFlags::default()) + ); + assert_eq!(skipped.len(), 3); + + let mut normal = buffer("ffi"); + shape(&font, &mut normal, ShapeOptions::new()).unwrap(); + assert!(normal.len() < skipped.len()); +} + +#[test] +fn rejecting_lookup_skips_its_operations_and_end_message() { + let instance = instance(); + let font = ShaperFont::new(&instance); + let mut buffer = buffer("ffi"); + let messages = Arc::new(Mutex::new(Vec::new())); + let captured = messages.clone(); + buffer.set_message_function(move |_, _, message| { + captured.lock().unwrap().push(message.to_string()); + !message.starts_with("start lookup") + }); + shape(&font, &mut buffer, ShapeOptions::new()).unwrap(); + assert_eq!(buffer.len(), 3); + let messages = messages.lock().unwrap(); + assert!(messages + .iter() + .any(|message| message.starts_with("start lookup") && message.contains("feature 'liga'"))); + assert!(!messages + .iter() + .any(|message| message.starts_with("end lookup") || message.starts_with("ligating"))); + assert!(messages + .iter() + .any(|message| message.starts_with("end table GSUB"))); +} + +#[test] +fn return_value_from_end_messages_does_not_skip_shaping() { + let instance = instance(); + let font = ShaperFont::new(&instance); + let mut normal = buffer("ffi AV"); + shape(&font, &mut normal, ShapeOptions::new()).unwrap(); + let mut traced = buffer("ffi AV"); + traced.set_message_function(|_, _, message| !message.starts_with("end")); + shape(&font, &mut traced, ShapeOptions::new()).unwrap(); + assert_eq!( + traced.serialize(Some(&font), SerializeFlags::default()), + normal.serialize(Some(&font), SerializeFlags::default()) + ); +} + +#[test] +fn callback_survives_reuse_and_can_be_replaced_or_removed() { + let instance = instance(); + let font = ShaperFont::new(&instance); + let mut buffer = buffer("ffi"); + let calls = Arc::new(Mutex::new(0)); + let captured = calls.clone(); + buffer.set_message_function(move |_, _, _| { + *captured.lock().unwrap() += 1; + true + }); + shape(&font, &mut buffer, ShapeOptions::new()).unwrap(); + let first = *calls.lock().unwrap(); + assert!(first > 0); + for reset in [false, true] { + if reset { + buffer.reset(); + } else { + buffer.clear(); + } + buffer.push_str("ffi"); + buffer.guess_segment_properties(); + shape(&font, &mut buffer, ShapeOptions::new()).unwrap(); + } + assert_eq!(*calls.lock().unwrap(), first * 3); + buffer.set_message_function(|_, _, _| true); + buffer.clear(); + buffer.push_str("ffi"); + buffer.guess_segment_properties(); + shape(&font, &mut buffer, ShapeOptions::new()).unwrap(); + buffer.clear_message_function(); + buffer.clear(); + buffer.push_str("ffi"); + buffer.guess_segment_properties(); + shape(&font, &mut buffer, ShapeOptions::new()).unwrap(); + assert_eq!(*calls.lock().unwrap(), first * 3); +} + +#[test] +fn tracing_preserves_buffer_send_and_sync() { + fn send_sync() {} + send_sync::(); +} + +#[test] +fn tracing_preserves_results_at_the_shaping_limit() { + let instance = Font::new( + include_bytes!("fonts/text-rendering-tests/TestGSUBThree.ttf").to_vec(), + 0, + ) + .unwrap() + .instance_builder() + .build(); + let font = ShaperFont::new(&instance); + let mut normal = buffer("lol"); + shape(&font, &mut normal, ShapeOptions::new()).unwrap(); + let mut traced = buffer("lol"); + traced.set_message_function(|buffer, _, _| { + assert_eq!(buffer.glyph_infos().len(), buffer.len()); + true + }); + shape(&font, &mut traced, ShapeOptions::new()).unwrap(); + assert_eq!( + traced.allocation_successful(), + normal.allocation_successful() + ); + assert_eq!(traced.len(), normal.len()); + let observed = traced.serialize(Some(&font), SerializeFlags::default()); + let expected = normal.serialize(Some(&font), SerializeFlags::default()); + assert_eq!(observed.len(), expected.len()); + // Report only the first differing byte for these very large buffers. + assert_eq!( + observed + .bytes() + .zip(expected.bytes()) + .position(|(a, b)| a != b), + None + ); +} diff --git a/hr-shape/README.md b/hr-shape/README.md index 1d6b34f4..a2c13cf0 100644 --- a/hr-shape/README.md +++ b/hr-shape/README.md @@ -16,4 +16,7 @@ cargo install hr-shape hr-shape [OPTIONS] ``` +Pass `--trace` to print HarfBuzz shaping messages and intermediate buffer +contents before the final glyph result. + For more information, see the main [HarfRust repository](https://github.com/harfbuzz/harfrust). diff --git a/hr-shape/src/lib.rs b/hr-shape/src/lib.rs index 87389ea5..7326f0a6 100644 --- a/hr-shape/src/lib.rs +++ b/hr-shape/src/lib.rs @@ -113,7 +113,7 @@ pub struct Args { single_par: bool, /// Output interim shaping results - #[arg(short = 'V', long)] + #[arg(long)] trace: bool, /// Set text direction (ltr/rtl/ttb/btt) @@ -409,14 +409,14 @@ pub fn render(mut args: Args) -> Result { let features = &args.features; let mut shape_plan_cache = ShapePlanCache::default(); let mut reusable_buffer = Buffer::new(); - let trace_output = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - if args.trace { + let trace_output = args + .trace + .then(|| std::sync::Arc::new(std::sync::Mutex::new(Vec::new()))); + if let Some(trace_output) = &trace_output { let trace_output = trace_output.clone(); reusable_buffer.set_message_function(move |buffer, font, message| { - let snapshot = buffer.serialize( - Some(font), - SerializeFlags::from_bits_truncate(format_flags), - ); + let snapshot = + buffer.serialize(Some(font), SerializeFlags::from_bits_truncate(format_flags)); trace_output .lock() .unwrap() @@ -522,7 +522,7 @@ pub fn render(mut args: Args) -> Result { .features(features), ) .map_err(|e| format!("Error: {e}"))?; - if args.trace { + if let Some(trace_output) = &trace_output { for trace in trace_output.lock().unwrap().drain(..) { output.extend_from_slice(trace.as_bytes()); } From 85a747d3d9685e2d8481343fad4df2c6173c53fe Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 9 Oct 2026 22:54:18 -0600 Subject: [PATCH 5/6] Use diagnostic assertions for trace snapshots --- harfrust/tests/tracing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harfrust/tests/tracing.rs b/harfrust/tests/tracing.rs index eba788b2..9ffd14c3 100644 --- a/harfrust/tests/tracing.rs +++ b/harfrust/tests/tracing.rs @@ -37,7 +37,7 @@ fn callback_observes_unicode_glyphs_and_positions() { buffer.glyph_positions().is_empty() || buffer.glyph_positions().len() == buffer.len() ); let snapshot = buffer.serialize(Some(font), SerializeFlags::default()); - assert!(!snapshot.is_empty()); + assert_ne!(snapshot, ""); captured.lock().unwrap().push(( calls, message.to_string(), From 05e96ae80bf6135981c8e23ebf82a771977df49d Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Fri, 9 Oct 2026 23:20:27 -0600 Subject: [PATCH 6/6] Align tracing messages with HarfBuzz --- README.md | 11 ++- harfrust/src/aat/kern.rs | 13 +++ harfrust/src/aat/kerx.rs | 27 +++--- harfrust/src/aat/layout.rs | 10 +- harfrust/src/buffer.rs | 32 ++++--- harfrust/src/normalize.rs | 5 +- harfrust/src/ot/apply.rs | 12 ++- harfrust/src/ot/gpos/mark.rs | 12 +-- harfrust/src/ot/gpos/mod.rs | 9 +- harfrust/src/ot/gpos/pair.rs | 12 +-- harfrust/src/ot/gsub/mod.rs | 9 +- harfrust/src/ot/layout.rs | 15 +-- harfrust/src/ot/map.rs | 30 +++++- harfrust/src/shape.rs | 8 +- harfrust/tests/shaping/main.rs | 6 +- harfrust/tests/tracing.rs | 169 +++++++++++++++++++++++++++++++++ hr-shape/README.md | 3 +- hr-shape/src/lib.rs | 76 +++++++++++++-- 18 files changed, 368 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index e3207849..a99f0f9c 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,17 @@ harfrust = { version = "0.14", features = ["tracing"] } Install a callback before shaping with `buffer.set_message_function`. It receives the public `Buffer`, `ShaperFont`, and message text, and can inspect or serialize intermediate Unicode and glyph contents. Return `true` to continue shaping, or -`false` from a `start` message to skip that operation. Mutable closures are -supported; callbacks own their captures and must be `Send + Sync + 'static`. +`false` from a skippable `start` message to skip that operation. Per-glyph and +recursion messages are informational and ignore the return value. Mutable closures +are supported; callbacks own their captures and must be `Send + Sync + 'static`. Use `buffer.clear_message_function()` to remove a callback. Tracing also works with `no_std` and `libm`. Without the `tracing` feature, the callback storage, message formatting, and intermediate snapshots compile out. -The `hr-shape` tool enables tracing and exposes `--trace` to print messages -alongside intermediate buffer contents. +The `hr-shape` tool enables tracing and exposes `--trace` to print messages and +intermediate buffer contents to stderr. Its library rendering API includes traces +in the returned string. Per-glyph messages follow HarfBuzz's detailed trace mode +(`HB_BUFFER_MESSAGE_MORE=1`), which release builds of HarfBuzz normally disable. ## Notes about the port diff --git a/harfrust/src/aat/kern.rs b/harfrust/src/aat/kern.rs index cc71cf6a..30510730 100644 --- a/harfrust/src/aat/kern.rs +++ b/harfrust/src/aat/kern.rs @@ -70,6 +70,8 @@ pub fn apply( break; }; subtable_idx += 1; + #[cfg(feature = "tracing")] + let message_index = subtable_idx - 1; if subtable.is_variable() { continue; @@ -86,11 +88,17 @@ pub fn apply( c.safe_to_break = safe_to_break.subtable(subtable_cache.safe_to_break)?; if !c.buffer_intersects_machine() { + message!( + c, + "skipped subtable {} because no glyph matches", + message_index + ); continue; } let reverse = c.buffer.direction.is_backward(); let is_cross_stream = subtable.is_cross_stream(); + message_continue!(c, "start subtable {}", message_index); if !seen_cross_stream && is_cross_stream { seen_cross_stream = true; @@ -132,6 +140,7 @@ pub fn apply( } _ => {} } + message!(c, "end subtable {}", message_index); } if c.buffer_is_reversed { c.reverse_buffer(); @@ -151,6 +160,9 @@ fn machine_kern( ) where F: Fn(u32, u32) -> i32, { + if !buffer_message!(buffer, font, "start kern") { + return; + } buffer.unsafe_to_concat(None, None); let mut ctx = ApplyContext::new( LayoutTableKind::Gpos, @@ -222,6 +234,7 @@ fn machine_kern( i = j; } + buffer_message!(iter.buffer, font, "end kern"); } fn apply_simple_kerning( diff --git a/harfrust/src/aat/kerx.rs b/harfrust/src/aat/kerx.rs index 23a4d2b5..10fdfed2 100644 --- a/harfrust/src/aat/kerx.rs +++ b/harfrust/src/aat/kerx.rs @@ -47,6 +47,8 @@ pub(crate) fn apply(c: &mut AatApplyContext) -> Option<()> { break; }; subtable_idx += 1; + #[cfg(feature = "tracing")] + let message_index = subtable_idx - 1; if c.buffer.direction.is_horizontal() != subtable.is_horizontal() { continue; @@ -61,15 +63,15 @@ pub(crate) fn apply(c: &mut AatApplyContext) -> Option<()> { if !c.buffer_intersects_machine() { message!( c, - "skipping kerning subtable {} because no glyph matches", - subtable_idx + "skipped subtable {} because no glyph matches", + message_index ); continue; } let reverse = c.buffer.direction.is_backward(); - message_continue!(c, "start subtable {}", subtable_idx); + message_continue!(c, "start subtable {}", message_index); if !seen_cross_stream && subtable.is_cross_stream() { seen_cross_stream = true; @@ -98,10 +100,9 @@ pub(crate) fn apply(c: &mut AatApplyContext) -> Option<()> { match &kind { SubtableKind::Format0(format0) => { - if !c.plan.requested_kerning { - continue; + if c.plan.requested_kerning { + apply_simple_kerning(c, &subtable, format0); } - apply_simple_kerning(c, &subtable, format0); } SubtableKind::Format1(format1) => { let mut driver = Driver1 { @@ -117,10 +118,9 @@ pub(crate) fn apply(c: &mut AatApplyContext) -> Option<()> { ); } SubtableKind::Format2(format2) => { - if !c.plan.requested_kerning { - continue; + if c.plan.requested_kerning { + apply_simple_kerning(c, &subtable, format2); } - apply_simple_kerning(c, &subtable, format2); } SubtableKind::Format4(format4) => { let mut driver = Driver4 { @@ -137,13 +137,12 @@ pub(crate) fn apply(c: &mut AatApplyContext) -> Option<()> { ); } SubtableKind::Format6(format6) => { - if !c.plan.requested_kerning { - continue; + if c.plan.requested_kerning { + apply_simple_kerning(c, &subtable, format6); } - apply_simple_kerning(c, &subtable, format6); } } - message!(c, "end subtable {}", subtable_idx); + message!(c, "end subtable {}", message_index); } if c.buffer_is_reversed { @@ -222,6 +221,7 @@ impl SimpleKerning for Subtable6<'_> { } fn apply_simple_kerning(c: &mut AatApplyContext, subtable: &Subtable, kind: &T) { + message_return!(c, "start kern"); let scale = c.scale; let mut ctx = ApplyContext::new( LayoutTableKind::Gpos, @@ -308,6 +308,7 @@ fn apply_simple_kerning(c: &mut AatApplyContext, subtable: &Su i = j; } + message!(ctx, "end kern"); } pub(crate) trait KerxStateEntryExt { diff --git a/harfrust/src/aat/layout.rs b/harfrust/src/aat/layout.rs index 7ddfb4fb..7e5163de 100644 --- a/harfrust/src/aat/layout.rs +++ b/harfrust/src/aat/layout.rs @@ -522,7 +522,13 @@ pub fn substitute( #[cfg(feature = "tracing")] font, ); - message_return!(c, "start table morx"); + #[cfg(feature = "tracing")] + let table = if layout.aat.morx.is_some() { + "morx" + } else { + "mort" + }; + message_return!(c, "start table {table}"); morx::apply( &mut c, if features.is_empty() { @@ -531,7 +537,7 @@ pub fn substitute( &aat_map }, ); - message!(c, "end table morx"); + message!(c, "end table {table}"); } fn is_deleted_glyph(info: &GlyphInfo) -> bool { diff --git a/harfrust/src/buffer.rs b/harfrust/src/buffer.rs index 4fe2548c..41ebfb50 100644 --- a/harfrust/src/buffer.rs +++ b/harfrust/src/buffer.rs @@ -2132,9 +2132,10 @@ impl Buffer { /// Sets the callback used to trace shaping operations. /// /// The callback receives the current buffer, shaping font, and a HarfBuzz - /// tracing message. Return `false` from a `start` message to skip that - /// operation; return values from other messages are ignored. Message text - /// may change as shaping algorithms evolve. + /// tracing message. Return `false` from a skippable `start` message to skip + /// that operation. Per-glyph and recursion messages are informational and + /// ignore the return value. Message text may change as shaping algorithms + /// evolve. /// /// The callback can inspect [`Self::content_type`], [`Self::glyph_infos`], /// [`Self::glyph_positions`], or [`Self::serialize`]. Positions are empty @@ -2159,6 +2160,12 @@ impl Buffer { #[cfg(feature = "tracing")] pub(crate) fn message(&mut self, font: &crate::ShaperFont<'_, '_>, msg: &str) -> bool { + // HarfBuzz formats into a 100-byte C string, including its terminator. + let mut end = msg.find('\0').unwrap_or(msg.len()).min(99); + while !msg.is_char_boundary(end) { + end -= 1; + } + let msg = &msg[..end]; if let Some(mut function) = self.message_function.take() { // Substitution keeps separate input/output cursors. Present the // emitted prefix followed by the remaining input without syncing @@ -2223,14 +2230,14 @@ impl Buffer { let info = self.glyph_infos(); if self.content_type == Some(ContentType::Unicode) { for info in info { - s.push(if s.is_empty() { '[' } else { '|' }); + s.push(if s.is_empty() { '<' } else { '|' }); write!(&mut s, "U+{:04X}", info.glyph_id)?; if !flags.contains(SerializeFlags::NO_CLUSTERS) { write!(&mut s, "={}", info.cluster)?; } } if !s.is_empty() { - s.push(']'); + s.push('>'); } return Ok(s); } @@ -2277,14 +2284,13 @@ impl Buffer { } if flags.contains(SerializeFlags::GLYPH_EXTENTS) { - let extents = font - .and_then(|font| font.glyph_extents(info.as_glyph())) - .unwrap_or_default(); - write!( - &mut s, - "<{},{},{},{}>", - extents.x_bearing, extents.y_bearing, extents.width, extents.height - )?; + if let Some(extents) = font.and_then(|font| font.glyph_extents(info.as_glyph())) { + write!( + &mut s, + "<{},{},{},{}>", + extents.x_bearing, extents.y_bearing, extents.width, extents.height + )?; + } } if flags.contains(SerializeFlags::NO_ADVANCES) { diff --git a/harfrust/src/normalize.rs b/harfrust/src/normalize.rs index 2a2bd8fd..02445f66 100644 --- a/harfrust/src/normalize.rs +++ b/harfrust/src/normalize.rs @@ -324,7 +324,7 @@ pub fn normalize<'x>(plan: &ShapePlan, buffer: &'x mut Buffer, font: &'x ShaperF // First round, decompose let mut all_simple = true; - { + if message!(ctx, "start decompose") { ctx.buffer.clear_output(); let count = ctx.buffer.len; ctx.buffer.idx = 0; @@ -373,6 +373,7 @@ pub fn normalize<'x>(plan: &ShapePlan, buffer: &'x mut Buffer, font: &'x ShaperF } ctx.buffer.sync(); + message!(ctx, "end decompose"); } // Second round, reorder (inplace) @@ -425,6 +426,7 @@ pub fn normalize<'x>(plan: &ShapePlan, buffer: &'x mut Buffer, font: &'x ShaperF && ctx.buffer.successful && (mode == NormalizationMode::ComposedDiacritics || mode == NormalizationMode::ComposedDiacriticsNoShortCircuit) + && message!(ctx, "start compose") { // As noted in the comment earlier, we don't try to combine // ccc=0 chars with their previous Starter. @@ -481,5 +483,6 @@ pub fn normalize<'x>(plan: &ShapePlan, buffer: &'x mut Buffer, font: &'x ShaperF } ctx.buffer.sync(); + message!(ctx, "end compose"); } } diff --git a/harfrust/src/ot/apply.rs b/harfrust/src/ot/apply.rs index de692189..5762efbe 100644 --- a/harfrust/src/ot/apply.rs +++ b/harfrust/src/ot/apply.rs @@ -577,17 +577,21 @@ pub(crate) fn apply_lookup( message!( ctx, - "recursing to lookup {} at {}", + "start recursing to lookup {} at {}", record.lookup_list_index.get(), ctx.buffer.message_idx() ); - if ctx.recurse(record.lookup_list_index.get()).is_none() { + let applied = ctx.recurse(record.lookup_list_index.get()).is_some(); + message!( + ctx, + "end recursing to lookup {}", + record.lookup_list_index.get(), + ); + if !applied { continue; } - message!(ctx, "recursed to lookup {}", record.lookup_list_index.get(),); - let new_len = ctx.buffer.backtrack_len() + ctx.buffer.lookahead_len(); let mut delta = new_len as isize - orig_len as isize; if delta == 0 { diff --git a/harfrust/src/ot/gpos/mark.rs b/harfrust/src/ot/gpos/mark.rs index 6c5fd9d3..e0ca063e 100644 --- a/harfrust/src/ot/gpos/mark.rs +++ b/harfrust/src/ot/gpos/mark.rs @@ -73,6 +73,12 @@ impl MarkArrayExt for MarkArray<'_> { let base_offset = resolve_cross_offset(&ctx.buffer.pos, glyph_pos, ctx.buffer.direction); let horizontal = ctx.buffer.direction.is_horizontal(); let idx = ctx.buffer.idx; + message!( + ctx, + "attaching mark glyph at {} to glyph at {}", + idx, + glyph_pos + ); // If the distance to the base does not fit in the i16 chain field, // leave the mark unattached. Matches HarfBuzz. let Ok(chain) = i16::try_from(glyph_pos as isize - idx as isize) else { @@ -80,12 +86,6 @@ impl MarkArrayExt for MarkArray<'_> { ctx.buffer.idx += 1; return Some(()); }; - message!( - ctx, - "attaching mark glyph at {} to glyph at {}", - idx, - glyph_pos - ); let pos = ctx.buffer.cur_pos_mut(); pos.x_offset = x_offset; pos.y_offset = y_offset; diff --git a/harfrust/src/ot/gpos/mod.rs b/harfrust/src/ot/gpos/mod.rs index f5638c2c..cb0d3858 100644 --- a/harfrust/src/ot/gpos/mod.rs +++ b/harfrust/src/ot/gpos/mod.rs @@ -14,16 +14,13 @@ use crate::ShaperFont; pub fn position(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) { #[cfg(feature = "tracing")] - let tag = plan - .ot_map - .chosen_script(LayoutTableKind::Gpos) - .unwrap_or(crate::Tag::new(b"DFLT")); - if !buffer_message!(buffer, font, "start table GPOS script tag '{tag}'") { + let tag = crate::ot::map::MessageTag(plan.ot_map.chosen_script(LayoutTableKind::Gpos)); + if !buffer_message!(buffer, font, "start table GPOS script tag '{tag:#}'") { return; } let table = font.layout().ot.gpos.clone(); apply_layout_table(plan, font, buffer, table.as_ref()); - buffer_message!(buffer, font, "end table GPOS script tag '{tag}'"); + buffer_message!(buffer, font, "end table GPOS script tag '{tag:#}'"); } pub mod attach_type { diff --git a/harfrust/src/ot/gpos/pair.rs b/harfrust/src/ot/gpos/pair.rs index 87cd35d7..c9886e28 100644 --- a/harfrust/src/ot/gpos/pair.rs +++ b/harfrust/src/ot/gpos/pair.rs @@ -96,7 +96,7 @@ impl Apply for PairPosFormat1<'_> { if flag1 || flag2 { message!( ctx, - "kerned glyphs at {} and {second_glyph_index}", + "kerned glyphs at {},{second_glyph_index}", ctx.buffer.idx ); ctx.buffer @@ -109,7 +109,7 @@ impl Apply for PairPosFormat1<'_> { } message!( ctx, - "tried kerning glyphs at {} and {second_glyph_index}", + "tried kerning glyphs at {},{second_glyph_index}", ctx.buffer.idx ); finish(ctx, iter_index, has_record2) @@ -147,7 +147,7 @@ impl Apply for PairPosFormat1<'_> { } else { message!( ctx, - "try kerning glyphs at {} and {second_glyph_index}", + "try kerning glyphs at {},{second_glyph_index}", ctx.buffer.idx ); let has_record2 = !format2.is_empty(); @@ -248,7 +248,7 @@ impl Apply for PairPosFormat2<'_> { if flag1 || flag2 { message!( ctx, - "kerned glyphs at {} and {second_glyph_index}", + "kerned glyphs at {},{second_glyph_index}", ctx.buffer.idx ); ctx.buffer @@ -259,7 +259,7 @@ impl Apply for PairPosFormat2<'_> { } message!( ctx, - "tried kerning glyphs at {} and {second_glyph_index}", + "tried kerning glyphs at {},{second_glyph_index}", ctx.buffer.idx ); finish(ctx, iter_index, has_record2) @@ -299,7 +299,7 @@ impl Apply for PairPosFormat2<'_> { let has_record2 = !format2.is_empty(); message!( ctx, - "try kerning glyphs at {} and {second_glyph_index}", + "try kerning glyphs at {},{second_glyph_index}", ctx.buffer.idx ); let worked1 = !format1.is_empty() diff --git a/harfrust/src/ot/gsub/mod.rs b/harfrust/src/ot/gsub/mod.rs index bf516254..2bab65fe 100644 --- a/harfrust/src/ot/gsub/mod.rs +++ b/harfrust/src/ot/gsub/mod.rs @@ -14,14 +14,11 @@ use crate::ShaperFont; pub fn substitute(plan: &ShapePlan, font: &ShaperFont<'_, '_>, buffer: &mut Buffer) { #[cfg(feature = "tracing")] - let tag = plan - .ot_map - .chosen_script(LayoutTableKind::Gsub) - .unwrap_or(crate::Tag::new(b"DFLT")); - if !buffer_message!(buffer, font, "start table GSUB script tag '{tag}'") { + let tag = crate::ot::map::MessageTag(plan.ot_map.chosen_script(LayoutTableKind::Gsub)); + if !buffer_message!(buffer, font, "start table GSUB script tag '{tag:#}'") { return; } let table = font.layout().ot.gsub.clone(); apply_layout_table(plan, font, buffer, table.as_ref()); - buffer_message!(buffer, font, "end table GSUB script tag '{tag}'"); + buffer_message!(buffer, font, "end table GSUB script tag '{tag:#}'"); } diff --git a/harfrust/src/ot/layout.rs b/harfrust/src/ot/layout.rs index 1114c9a4..3e8bf8b8 100644 --- a/harfrust/src/ot/layout.rs +++ b/harfrust/src/ot/layout.rs @@ -181,7 +181,7 @@ pub fn apply_layout_table( ctx, "start lookup {} feature '{}'", lookup_map.index, - lookup_map.feature_tag + crate::ot::map::MessageTag(Some(lookup_map.feature_tag)) ); if lookup.digest().may_intersect(&ctx.buffer.digest) @@ -199,27 +199,20 @@ pub fn apply_layout_table( ctx.random = lookup_map.random; ctx.per_syllable = lookup_map.per_syllable; - if !apply_string::(&mut ctx, lookup) { - message!( - ctx, - "skipped lookup {} feature '{}' because no glyph matches", - lookup_map.index, - lookup_map.feature_tag - ); - } + apply_string::(&mut ctx, lookup); } else { message!( ctx, "skipped lookup {} feature '{}' because no glyph matches", lookup_map.index, - lookup_map.feature_tag + crate::ot::map::MessageTag(Some(lookup_map.feature_tag)) ); } message!( ctx, "end lookup {} feature '{}'", lookup_map.index, - lookup_map.feature_tag + crate::ot::map::MessageTag(Some(lookup_map.feature_tag)) ); } } diff --git a/harfrust/src/ot/map.rs b/harfrust/src/ot/map.rs index c41ad036..ad6582eb 100644 --- a/harfrust/src/ot/map.rs +++ b/harfrust/src/ot/map.rs @@ -64,6 +64,27 @@ pub struct LookupMap { pub feature_tag: Tag, } +// HarfBuzz prints tag bytes directly, whereas Tag's Display escapes them. +#[cfg(feature = "tracing")] +pub(crate) struct MessageTag(pub Option); + +#[cfg(feature = "tracing")] +impl core::fmt::Display for MessageTag { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + use core::fmt::Write; + if let Some(tag) = self.0 { + for &byte in tag.as_ref() { + // Script tags use a C string; feature tags use four %c fields. + if f.alternate() && byte == 0 { + break; + } + f.write_char(char::from(byte))?; + } + } + Ok(()) + } +} + #[derive(Clone, Copy)] pub struct StageMap { // Cumulative @@ -519,6 +540,8 @@ impl<'a> OtMapBuilder<'a> { true, false, false, + #[cfg(feature = "tracing")] + Tag::new(b" "), ); } } @@ -536,6 +559,8 @@ impl<'a> OtMapBuilder<'a> { feature.auto_zwj, feature.random, feature.per_syllable, + #[cfg(feature = "tracing")] + feature.tag, ); } } @@ -592,13 +617,10 @@ impl<'a> OtMapBuilder<'a> { auto_zwj: bool, random: bool, per_syllable: bool, + #[cfg(feature = "tracing")] feature_tag: Tag, ) -> Option<()> { let table = self.layout.ot.layout_table(table_index)?; - #[cfg(feature = "tracing")] - let feature_tag = table - .feature_tag(feature_index) - .unwrap_or(Tag::new(b"DFLT")); let lookup_count = table.lookup_count(); let feature = match variation_index { Some(idx) => table diff --git a/harfrust/src/shape.rs b/harfrust/src/shape.rs index 78ade880..2710afc2 100644 --- a/harfrust/src/shape.rs +++ b/harfrust/src/shape.rs @@ -121,9 +121,9 @@ impl OtShapeContext<'_, '_, '_> { ensure_native_direction(self.buffer); if let Some(func) = self.plan.shaper.preprocess_text { - if buffer_message!(self.buffer, self.font, "start preprocess text") { + if buffer_message!(self.buffer, self.font, "start preprocess-text") { func(self.plan, self.font, self.buffer); - message!(self, "end preprocess text"); + message!(self, "end preprocess-text"); } } @@ -161,9 +161,9 @@ impl OtShapeContext<'_, '_, '_> { hide_default_ignorables(self.buffer, self.font); if let Some(func) = self.plan.shaper.postprocess_glyphs { - if buffer_message!(self.buffer, self.font, "start postprocess glyphs") { + if buffer_message!(self.buffer, self.font, "start postprocess-glyphs") { func(self.plan, self.font, self.buffer); - message!(self, "end postprocess glyphs"); + message!(self, "end postprocess-glyphs"); } } } diff --git a/harfrust/tests/shaping/main.rs b/harfrust/tests/shaping/main.rs index 5b3e7dd0..6f6da4dc 100644 --- a/harfrust/tests/shaping/main.rs +++ b/harfrust/tests/shaping/main.rs @@ -22,7 +22,11 @@ pub fn shape(font_path: &str, text: &str, options: &str) -> String { .unwrap_or_else(|err| panic!("hr-shape tracing failed: {err}")); let result = traced .lines() - .filter(|line| !line.starts_with("trace: ")) + .filter(|line| { + !line + .trim_start_matches(|c: char| c.is_ascii_digit() || c == ':' || c == ' ') + .starts_with("trace: ") + }) .collect::>() .join("\n"); assert_eq!( diff --git a/harfrust/tests/tracing.rs b/harfrust/tests/tracing.rs index 9ffd14c3..ee9c1db1 100644 --- a/harfrust/tests/tracing.rs +++ b/harfrust/tests/tracing.rs @@ -49,6 +49,28 @@ fn callback_observes_unicode_glyphs_and_positions() { }); shape(&font, &mut buffer, ShapeOptions::new()).unwrap(); let messages = messages.lock().unwrap(); + assert_eq!(messages[0].1, "start decompose"); + assert_eq!(messages[0].4, ""); + let normalization: Vec<_> = messages + .iter() + .filter(|(_, message, ..)| { + message.ends_with("decompose") + || message.ends_with("reorder") + || message.ends_with("compose") + }) + .map(|(_, message, ..)| message.as_str()) + .collect(); + assert_eq!( + normalization, + [ + "start decompose", + "end decompose", + "start reorder", + "end reorder", + "start compose", + "end compose" + ] + ); assert!(messages .iter() .any(|(_, message, content, positioned, snapshot)| { @@ -214,3 +236,150 @@ fn tracing_preserves_results_at_the_shaping_limit() { None ); } + +#[test] +fn rejecting_composition_keeps_decomposed_glyphs() { + let instance = instance(); + let font = ShaperFont::new(&instance); + let mut normal = buffer("o\u{0301}"); + // GSUB can compose the pair through ccmp independently of normalization. + normal.set_message_function(|_, _, message| !message.starts_with("start table GSUB")); + shape(&font, &mut normal, ShapeOptions::new()).unwrap(); + let mut skipped = buffer("o\u{0301}"); + let messages = Arc::new(Mutex::new(Vec::new())); + let captured = messages.clone(); + skipped.set_message_function(move |_, _, message| { + captured.lock().unwrap().push(message.to_string()); + message != "start compose" && !message.starts_with("start table GSUB") + }); + shape(&font, &mut skipped, ShapeOptions::new()).unwrap(); + assert_eq!(normal.len(), 1); + assert_eq!(skipped.len(), 2); + let messages = messages.lock().unwrap(); + assert!(messages.iter().any(|m| m == "start compose")); + assert!(!messages.iter().any(|m| m == "end compose")); +} + +#[test] +fn recursion_notifications_are_balanced_and_informational() { + let instance = Font::new( + include_bytes!("fonts/aots/gsub_context1_simple_f1.otf").to_vec(), + 0, + ) + .unwrap() + .instance_builder() + .build(); + let font = ShaperFont::new(&instance); + let features = ["test".parse::().unwrap()]; + let mut buffer = buffer("\0\u{0014}\u{0015}\u{0016}\0"); + let messages = Arc::new(Mutex::new(Vec::new())); + let captured = messages.clone(); + buffer.set_message_function(move |_, _, message| { + if message.contains("recursing") { + captured.lock().unwrap().push(message.to_string()); + } + !message.starts_with("start recursing") + }); + shape(&font, &mut buffer, ShapeOptions::new().features(&features)).unwrap(); + let ids: Vec<_> = buffer + .glyph_infos() + .iter() + .map(|info| info.glyph_id) + .collect(); + assert_eq!(ids, [0, 60, 61, 62, 0]); + assert_eq!( + *messages.lock().unwrap(), + [ + "start recursing to lookup 0 at 1", + "end recursing to lookup 0", + "start recursing to lookup 0 at 2", + "end recursing to lookup 0", + "start recursing to lookup 0 at 3", + "end recursing to lookup 0", + ] + ); +} + +fn cli_trace(font: &str, text: &str) -> String { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fonts") + .join(font); + hr_shape::shape(path.to_str().unwrap(), text, "--trace").unwrap() +} + +#[test] +fn kern_subtable_and_machine_messages_nest_and_can_skip() { + let output = cli_trace("text-rendering-tests/TestKERNOne.otf", "uT"); + let messages: Vec<_> = output + .lines() + .filter_map(|line| line.strip_prefix("trace: ")?.split_once("\tbuffer: ")) + .map(|(message, _)| message) + .filter(|message| message.contains("kern") || message.contains("subtable")) + .collect(); + assert_eq!( + messages, + [ + "start table kern", + "start subtable 0", + "start kern", + "end kern", + "end subtable 0", + "end table kern" + ] + ); + // Missing layout tables are reported with an empty chosen-script tag. + assert!(output.contains("start table GSUB script tag ''\tbuffer: ")); + let instance = Font::new( + include_bytes!("fonts/text-rendering-tests/TestKERNOne.otf").to_vec(), + 0, + ) + .unwrap() + .instance_builder() + .build(); + let font = ShaperFont::new(&instance); + let mut skipped = buffer("uT"); + skipped.set_message_function(|_, _, message| message != "start kern"); + shape(&font, &mut skipped, ShapeOptions::new()).unwrap(); + let mut disabled = buffer("uT"); + let features = ["kern=0".parse::().unwrap()]; + shape( + &font, + &mut disabled, + ShapeOptions::new().features(&features), + ) + .unwrap(); + assert_eq!( + skipped.serialize(Some(&font), SerializeFlags::default()), + disabled.serialize(Some(&font), SerializeFlags::default()) + ); +} + +#[test] +fn mort_trace_uses_the_mort_table_name() { + let output = cli_trace("text-rendering-tests/TestAATMort.ttf", "ABCEFGX"); + assert!(output.contains("trace: start table mort\tbuffer: ")); + assert!(output.contains("trace: end table mort\tbuffer: ")); + assert!(!output.contains("table morx")); +} + +#[test] +fn required_features_use_blank_trace_tags() { + let output = cli_trace( + "in-house/a59fd13f1525a91cbe529c882e93d9d1fbb80463.ttf", + "AB", + ); + assert!(output.contains("trace: start lookup 0 feature ' '\tbuffer: ")); + assert!(output.contains("trace: end lookup 0 feature ' '\tbuffer: ")); +} + +#[test] +fn unavailable_glyph_extents_are_omitted() { + let instance = instance(); + let font = ShaperFont::new(&instance); + let mut buffer = buffer("ffi"); + shape(&font, &mut buffer, ShapeOptions::new()).unwrap(); + assert_eq!( + buffer.serialize(None, SerializeFlags::GLYPH_EXTENTS), + buffer.serialize(None, SerializeFlags::default()) + ); +} diff --git a/hr-shape/README.md b/hr-shape/README.md index a2c13cf0..bc056df4 100644 --- a/hr-shape/README.md +++ b/hr-shape/README.md @@ -17,6 +17,7 @@ hr-shape [OPTIONS] ``` Pass `--trace` to print HarfBuzz shaping messages and intermediate buffer -contents before the final glyph result. +contents to stderr. Glyph results go to stdout or the requested output file. +The library's `render` API includes traces in its returned string. For more information, see the main [HarfRust repository](https://github.com/harfbuzz/harfrust). diff --git a/hr-shape/src/lib.rs b/hr-shape/src/lib.rs index 7326f0a6..64ac3545 100644 --- a/hr-shape/src/lib.rs +++ b/hr-shape/src/lib.rs @@ -254,9 +254,13 @@ pub fn try_main() -> Result<(), String> { /// Returns an error string if shaping or output writing fails. pub fn run_and_write(args: Args) -> Result<(), String> { let output_file = args.output_file.clone(); - let output = render(args)?; - write_output(&output, output_file.as_ref())?; - Ok(()) + let mut trace = Vec::new(); + let output = render_with_trace(args, Some(&mut trace))?; + io::stderr() + .lock() + .write_all(&trace) + .map_err(|err| format!("Error: cannot write trace output: {err}"))?; + write_output(&output, output_file.as_ref()) } /// Parses `hr-shape` arguments from an iterator and returns the rendered output. @@ -309,12 +313,22 @@ pub fn shape(font_path: &str, text: &str, options: &str) -> Result Result { +pub fn render(args: Args) -> Result { + render_with_trace(args, None) +} + +fn render_with_trace( + mut args: Args, + mut trace_sink: Option<&mut Vec>, +) -> Result { normalize_args(&mut args); let mut font_set_as_free_arg = false; @@ -414,13 +428,17 @@ pub fn render(mut args: Args) -> Result { .then(|| std::sync::Arc::new(std::sync::Mutex::new(Vec::new()))); if let Some(trace_output) = &trace_output { let trace_output = trace_output.clone(); + let serialize_trace = !args.output_format.is_empty(); reusable_buffer.set_message_function(move |buffer, font, message| { - let snapshot = - buffer.serialize(Some(font), SerializeFlags::from_bits_truncate(format_flags)); + let snapshot = if serialize_trace { + buffer.serialize(Some(font), SerializeFlags::from_bits_truncate(format_flags)) + } else { + String::new() + }; trace_output .lock() .unwrap() - .push(format!("trace: {message}\t{snapshot}\n")); + .push(format!("trace: {message}\tbuffer: {snapshot}\n")); true }); } @@ -524,7 +542,11 @@ pub fn render(mut args: Args) -> Result { .map_err(|e| format!("Error: {e}"))?; if let Some(trace_output) = &trace_output { for trace in trace_output.lock().unwrap().drain(..) { - output.extend_from_slice(trace.as_bytes()); + let sink = trace_sink.as_deref_mut().unwrap_or(&mut output); + if args.show_line_num { + write!(sink, "{line_no}: ").unwrap(); + } + sink.extend_from_slice(trace.as_bytes()); } } } @@ -649,6 +671,42 @@ fn serialize_unicode(text: &str, utf8_clusters: bool) -> String { mod tests { use super::*; + #[test] + fn trace_stream_has_harfbuzz_prefix_and_line_numbers() { + let font = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../harfrust/benches/fonts/Roboto-Regular.ttf" + ); + let args = Args::try_parse_from([ + "hr-shape", + font, + "ffi", + "--trace", + "--show-line-num", + "--no-glyph-names", + ]) + .unwrap(); + let mut trace = Vec::new(); + let output = render_with_trace(args.clone(), Some(&mut trace)).unwrap(); + let trace = String::from_utf8(trace).unwrap(); + assert!(!output.contains("trace: ")); + assert!(output.starts_with("1: [")); + assert!( + trace.starts_with("1: trace: start decompose\tbuffer: \n") + ); + assert!(trace.lines().all(|line| line.starts_with("1: trace: "))); + let combined = render(args.clone()).unwrap(); + assert!(combined.contains(&trace)); + assert!(combined.ends_with(&output)); + let mut args = args; + args.output_format.clear(); + let mut trace = Vec::new(); + let output = render_with_trace(args, Some(&mut trace)).unwrap(); + assert_eq!(output, "1: \n"); + let trace = String::from_utf8(trace).unwrap(); + assert!(trace.lines().all(|line| line.ends_with("\tbuffer: "))); + } + #[test] fn empty_output_format_is_accepted() { let args = Args::try_parse_from(["hr-shape", "font.ttf", "text", "-O", ""]).unwrap();