Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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 }}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,29 @@ 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 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should set_message_function instead take an option?


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 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

HarfRust is not a full port of HarfBuzz. HarfBuzz (C++ edition) can roughly be split into 6 parts:
Expand Down
2 changes: 2 additions & 0 deletions harfrust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
23 changes: 19 additions & 4 deletions harfrust/src/aat/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ pub(crate) fn get_class<T: bytemuck::AnyBitPattern + FixedSize>(
///
/// See <https://github.com/harfbuzz/harfbuzz/blob/2c22a65f0cb99544c36580b9703a43b5dc97a9e1/src/hb-aat-layout-common.hh#L108>
#[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]>,
Expand All @@ -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,
Expand Down Expand Up @@ -744,8 +753,14 @@ mod tests {
buffer.clear_output();
buffer.next_glyph();

let mut context =
AatApplyContext::new(&plan, shaper_font.layout(), Scale::default(), &mut buffer);
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);
Expand Down
40 changes: 38 additions & 2 deletions harfrust/src/aat/kern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,20 @@ 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();

Expand All @@ -58,6 +70,8 @@ pub fn apply(
break;
};
subtable_idx += 1;
#[cfg(feature = "tracing")]
let message_index = subtable_idx - 1;

if subtable.is_variable() {
continue;
Expand All @@ -74,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;
Expand Down Expand Up @@ -120,25 +140,38 @@ pub fn apply(
}
_ => {}
}
message!(c, "end subtable {}", message_index);
}
if c.buffer_is_reversed {
c.reverse_buffer();
}
buffer_message!(c.buffer, font, "end table kern");
Some(())
}

fn machine_kern<F>(
layout: LayoutData<'_>,
scale: Scale,
buffer: &mut Buffer,
#[cfg(feature = "tracing")] font: &crate::ShaperFont<'_, '_>,
kern_mask: Mask,
cross_stream: bool,
get_kerning: F,
) 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, 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();
Expand Down Expand Up @@ -201,6 +234,7 @@ fn machine_kern<F>(

i = j;
}
buffer_message!(iter.buffer, font, "end kern");
}

fn apply_simple_kerning<T: SimpleKerning>(
Expand All @@ -215,6 +249,8 @@ fn apply_simple_kerning<T: SimpleKerning>(
c.layout,
c.scale,
c.buffer,
#[cfg(feature = "tracing")]
c.font,
c.plan.kern_mask,
is_cross_stream,
|left, right| {
Expand Down
37 changes: 27 additions & 10 deletions harfrust/src/aat/kerx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -59,11 +61,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,
"skipped subtable {} because no glyph matches",
message_index
);
continue;
}

let reverse = c.buffer.direction.is_backward();

message_continue!(c, "start subtable {}", message_index);

if !seen_cross_stream && subtable.is_cross_stream() {
seen_cross_stream = true;

Expand Down Expand Up @@ -91,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 {
Expand All @@ -110,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 {
Expand All @@ -130,13 +137,14 @@ 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 {}", message_index);
}

if c.buffer_is_reversed {
c.reverse_buffer();
}
Expand Down Expand Up @@ -213,8 +221,16 @@ impl SimpleKerning for Subtable6<'_> {
}

fn apply_simple_kerning<T: SimpleKerning>(c: &mut AatApplyContext, subtable: &Subtable, kind: &T) {
message_return!(c, "start kern");
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();
Expand Down Expand Up @@ -292,6 +308,7 @@ fn apply_simple_kerning<T: SimpleKerning>(c: &mut AatApplyContext, subtable: &Su

i = j;
}
message!(ctx, "end kern");
}

pub(crate) trait KerxStateEntryExt {
Expand Down
Loading
Loading