Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
38a0ce6
Add a C API
dfrg Sep 1, 2026
50ec843
Report shaping failures through the C API
dfrg Sep 1, 2026
00a6301
Abort on misuse, and add a HarfBuzz compatibility header
dfrg Sep 1, 2026
2854898
Read exhaustion from the buffer rather than the error
dfrg Sep 1, 2026
3d1ada1
Fix lifetime bugs around shaping, and prefer read-write locks
dfrg Sep 2, 2026
ffe40aa
Cut four per-call costs out of the shaping path
dfrg Sep 2, 2026
7012bfc
Let the plan cache grow, as HarfBuzz's does
dfrg Sep 2, 2026
beb5a59
Report only whether a shaper ran from hr_shape_full
dfrg Sep 2, 2026
da3a19f
[capi] Bound UTF-8 context decoding
behdad Sep 5, 2026
51b5888
[capi] Match shape-list signature with HarfBuzz
behdad Sep 5, 2026
f7dd893
[capi] Bulk append UTF-8 input
behdad Sep 5, 2026
1798da7
[capi] Preserve release LTO for C libraries
behdad Sep 5, 2026
857e70b
[capi] Cache the prepared shaper on fonts
behdad Sep 5, 2026
f3c25a4
[perf] Cache built-in font data on shapers
behdad Sep 5, 2026
fe4a86a
[capi] Defer built-in font table loading
behdad Sep 5, 2026
37a5809
[capi] Bound UTF-8 context decoding (#471)
behdad Sep 6, 2026
a1e46f5
Let a buffer say it has no script or language
dfrg Sep 8, 2026
c8eb3c2
Fail an empty shaper list rather than ignoring it
dfrg Sep 8, 2026
9bf14a6
Test that absent properties and empty shaper lists behave
dfrg Sep 8, 2026
52dc693
Run caller destructors on time, and outside the lock
dfrg Sep 8, 2026
05e0453
Append everything the glyphs came with
dfrg Sep 8, 2026
48f256c
Serialize whole items, and without a font
dfrg Sep 8, 2026
d68586b
Decode ill-formed text the way HarfBuzz decodes it
dfrg Sep 8, 2026
1e415f4
Compare shape plan coordinates the way the font reads them
dfrg Sep 8, 2026
08314b2
Read no callbacks as no callbacks, not as the built-in ones
dfrg Sep 8, 2026
5ba645c
Satisfy clippy on the new code
dfrg Sep 8, 2026
87c42c1
Say in the type that Buffer's script and language are optional
dfrg Sep 9, 2026
0407c8a
Shape with the font's variation rather than refusing
dfrg Sep 9, 2026
6147c69
Answer the glyph getters through the installed callbacks
dfrg Sep 9, 2026
ac10a34
Add hr_ot_font_set_funcs, the way back to the built-in callbacks
dfrg Sep 9, 2026
33f1bb1
Match HarfBuzz on serializing, adding codepoints, and guessing
dfrg Sep 10, 2026
e7063f3
Say what aborts, and test that it is what aborts
dfrg Sep 10, 2026
efc040e
Let C hold values Rust enumerations cannot
dfrg Sep 10, 2026
bdd1376
Ask the parent font, rather than copying what it had
dfrg Sep 10, 2026
1d779d5
Answer for buffers, faces and blobs the way HarfBuzz does
dfrg Sep 10, 2026
de830f4
Drop the scratch examples committed by mistake
dfrg Sep 10, 2026
9281fa7
Tell reading the tables apart from having nothing to say
dfrg Sep 10, 2026
4686696
Match HarfBuzz on tags, directions, sub-blobs and parent fonts
dfrg Sep 10, 2026
aed0ee7
Stop the compatibility header inventing HarfBuzz spellings
dfrg Sep 10, 2026
8cb0823
Answer for glyph metrics the way HarfBuzz answers
dfrg Sep 10, 2026
9fa3102
Answer for cmap and vertical metrics the way HarfBuzz answers
dfrg Sep 10, 2026
ff82026
Let a caller ask a font what it lets a caller answer for
dfrg Sep 10, 2026
1851a41
Name the crate harfrust_capi
dfrg Sep 10, 2026
cd44e63
Move the crate to harfrust_capi/ to match its name
dfrg Sep 10, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ This development version matches HarfBuzz [v14.3.1](https://github.com/harfbuzz/
already holds glyphs, a buffer with no direction, a font with nothing to
shape with, and a plan built for other properties. Running out of room is
reported through `Buffer::allocation_successful`, as before.
- Add the `harfrust_capi` crate, a C API mirroring the shaping half of
HarfBuzz's API with an `hr_` prefix in place of `hb_`.

## [0.13.3] - 2026-08-25

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["harfrust", "hr-shape", "fuzz"]
members = ["harfrust", "harfrust_capi", "hr-shape", "fuzz"]
resolver = "3"

[workspace.package]
Expand Down
66 changes: 52 additions & 14 deletions harfrust/src/hb/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,8 +774,10 @@ impl Buffer {

self.serial = 0;
self.scratch_flags = HB_BUFFER_SCRATCH_FLAG_DEFAULT;
self.cluster_level = HB_BUFFER_CLUSTER_LEVEL_DEFAULT;
self.not_found_variation_selector = None;
// How the buffer is configured -- its flags, cluster level, invisible
// glyph and variation-selector fallback -- outlives its contents, as
// it does in `hb_buffer_clear_contents`. `Buffer::reset` is what puts
// those back.
}

#[inline]
Expand Down Expand Up @@ -1839,6 +1841,19 @@ impl Buffer {
}
}

/// Returns the pre-context, in the reverse order
/// [`Buffer::set_pre_context_codepoints`] takes it.
#[inline]
pub fn pre_context_codepoints(&self) -> &[u32] {
&self.context[0][..self.context_len[0]]
}

/// Returns the post-context.
#[inline]
pub fn post_context_codepoints(&self) -> &[u32] {
&self.context[1][..self.context_len[1]]
}

pub(crate) fn next_syllable(&self, mut start: usize) -> usize {
if start >= self.len {
return start;
Expand Down Expand Up @@ -2016,28 +2031,31 @@ impl Buffer {
self.direction = direction;
}

/// Returns the buffer's ISO 15924 script.
/// Returns the buffer's ISO 15924 script, or `None` if it has none.
///
/// A buffer with no script is not one whose script is `Zzzz`: that is a
/// script a caller can ask for, meaning known to be unknown.
#[inline]
pub fn script(&self) -> Script {
self.script.unwrap_or(script::UNKNOWN)
pub fn script(&self) -> Option<Script> {
self.script
}

/// Sets the buffer's script from an ISO 15924 tag.
/// Sets the buffer's script from an ISO 15924 tag, or clears it.
#[inline]
pub fn set_script(&mut self, script: Script) {
self.script = Some(script);
pub fn set_script(&mut self, script: Option<Script>) {
self.script = script;
}

/// Returns the buffer's language.
/// Returns the buffer's language, or `None` if it has none.
#[inline]
pub fn language(&self) -> Option<Language> {
self.language.clone()
pub fn language(&self) -> Option<&Language> {
self.language.as_ref()
}

/// Sets the buffer's language.
/// Sets the buffer's language, or clears it.
#[inline]
pub fn set_language(&mut self, language: Language) {
self.language = Some(language);
pub fn set_language(&mut self, language: Option<Language>) {
self.language = language;
}

/// Returns the buffer's flags.
Expand Down Expand Up @@ -2704,6 +2722,26 @@ pub trait SerializerFont {
fn glyph_metrics(&self) -> GlyphMetrics<'_>;
}

/// Stands in for a font when a buffer is serialized without one, as
/// HarfBuzz's empty font does: glyphs serialize by number rather than by
/// name, and nothing has extents.
#[derive(Clone, Copy, Debug, Default)]
pub struct EmptySerializerFont;

impl SerializerFont for EmptySerializerFont {
fn coords(&self) -> &[F2Dot14] {
&[]
}

fn glyph_names(&self) -> GlyphNames<'_> {
GlyphNames::None
}

fn glyph_metrics(&self) -> GlyphMetrics<'_> {
GlyphMetrics::default()
}
}

impl SerializerFont for crate::Shaper<'_> {
fn coords(&self) -> &[F2Dot14] {
self.coords()
Expand Down
94 changes: 82 additions & 12 deletions harfrust/src/hb/face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,15 +245,20 @@ impl<'a> ShaperBuilder<'a> {
feature_variations,
);
let aat_tables = AatTables::new(&font, &self.data.aat_cache, &self.data.table_ranges);
let font = FontKind::FontRef(FontRefData {
let font_data = FontRefData {
font,
glyph_metrics,
charmap,
});
};
let glyph_metrics = Some(font_data.glyph_metrics.clone());
let charmap = Some(font_data.charmap.clone());
let font = FontKind::FontRef(font_data);
hb_font_t {
font,
units_per_em,
cmap_cache: &self.data.cmap_cache,
glyph_metrics,
charmap,
ot_tables,
aat_tables,
apply_trak: self.data.apply_trak,
Expand Down Expand Up @@ -341,7 +346,15 @@ impl<'a> ShapeOptions<'a> {
}

#[derive(Copy, Clone)]
pub(crate) struct Scale {
/// How font units become the units a caller asked for.
///
/// Shaping applies this to everything it reports, from
/// [`ShapeOptions::scale`]. A caller asking a font about one glyph rather
/// than about a run needs the same conversion, and needs it to be the same
/// one, so it is spelled once here -- down to the rounding, which follows
/// HarfBuzz's.
#[derive(Debug)]
pub struct Scale {
x_mult: i64,
y_mult: i64,
x_multf: f32,
Expand All @@ -362,7 +375,9 @@ impl Default for Scale {
// Various conversions between f32 and i32
#[allow(clippy::cast_precision_loss)]
impl Scale {
pub(crate) fn new(scale: Option<(i32, i32)>, upem: i32) -> Self {
/// The conversion from `upem` font units into `scale`, or the identity
/// when there is no scale to apply or the face has no units to convert.
pub fn new(scale: Option<(i32, i32)>, upem: i32) -> Self {
let (Some((x_scale, y_scale)), true) = (scale, upem != 0) else {
// When scale is not configured, or upem is zero, return results
// in font units.
Expand All @@ -378,13 +393,15 @@ impl Scale {
}
}

/// A horizontal distance in font units, in the units asked for.
#[inline(always)]
pub(crate) fn scale_x(&self, x: i32) -> i32 {
pub fn scale_x(&self, x: i32) -> i32 {
Self::scale_by_mult(x, self.x_mult)
}

/// A vertical distance in font units, in the units asked for.
#[inline(always)]
pub(crate) fn scale_y(&self, y: i32) -> i32 {
pub fn scale_y(&self, y: i32) -> i32 {
Self::scale_by_mult(y, self.y_mult)
}

Expand All @@ -404,7 +421,7 @@ impl Scale {
/// floor the origin corners and ceil the far corners before deriving the
/// final width/height.
/// hb_font_t::scale_glyph_extents: <https://github.com/harfbuzz/harfbuzz/blob/88adc6437ef561486a5adf1822410297ef4a852b/src/hb-font.hh#L201>'
pub(crate) fn scale_extents(&self, mut extents: GlyphExtents) -> GlyphExtents {
pub fn scale_extents(&self, mut extents: GlyphExtents) -> GlyphExtents {
let x1 = extents.x_bearing as f32 * self.x_multf;
let y1 = extents.y_bearing as f32 * self.y_multf;
let x2 = (i64::from(extents.x_bearing) + i64::from(extents.width)) as f32 * self.x_multf;
Expand Down Expand Up @@ -468,7 +485,7 @@ pub fn shape(
}
let mut buffer = buffer.0;
// As above, this signature cannot report a failure.
if let Err(err) = hb_font.shape_buffer(&mut buffer, options) {
if let Err(err) = hb_font.shape_buffer_inner(&mut buffer, options) {
panic!("{err}");
}
GlyphBuffer(buffer)
Expand Down Expand Up @@ -514,7 +531,7 @@ impl Buffer {
options = options.scale(Some((ppem * 65536.0) as i32));
}
}
hb_font.shape_buffer(self, options)
hb_font.shape_buffer_inner(self, options)
}
}

Expand Down Expand Up @@ -547,12 +564,39 @@ pub struct hb_font_t<'a> {
pub(crate) font: FontKind<'a>,
pub(crate) units_per_em: u16,
pub(crate) cmap_cache: &'a cmap_cache_t,
pub(crate) glyph_metrics: Option<GlyphMetrics<'a>>,
pub(crate) charmap: Option<Charmap<'a>>,
pub(crate) ot_tables: OtTables<'a>,
pub(crate) aat_tables: AatTables<'a>,
pub(crate) apply_trak: bool,
}

impl<'a> crate::Shaper<'a> {
/// Builds a shaper for the font instance, reusing the instance's cached
/// shaping data.
///
/// The shaper borrows the instance; callers that shape repeatedly can
/// build it once and reuse it across calls.
#[cfg(feature = "experimental_font_api")]
pub fn from_font_instance(font: &'a crate::font::FontInstance) -> Option<Self> {
Self::from_font(font)
}

/// Preloads the table views used by the built-in font functions.
///
/// This is an internal hook for bridges that cache a prepared shaper and
/// know that shaping will use the built-in functions.
#[doc(hidden)]
#[cfg(feature = "experimental_font_api")]
pub fn preload_builtin_font_data(&mut self) {
let FontKind::FontInstance(instance, metrics) = &self.font else {
return;
};
let tables = instance.tables();
self.glyph_metrics = Some(GlyphMetrics::from_tables(&tables, metrics));
self.charmap = Some(Charmap::from_tables(&tables));
}

pub(crate) fn from_font(font: &'a crate::font::FontInstance) -> Option<Self> {
let data = crate::font::_font_interop::_get_or_init_shaping_data(font, || {
Box::new(ShaperData::from_font(font))
Expand All @@ -578,6 +622,8 @@ impl<'a> crate::Shaper<'a> {
font: FontKind::FontInstance(font, metrics),
units_per_em: data.table_ranges.units_per_em,
cmap_cache: &data.cmap_cache,
glyph_metrics: None,
charmap: None,
ot_tables,
aat_tables,
apply_trak: data.apply_trak,
Expand Down Expand Up @@ -611,13 +657,32 @@ impl<'a> crate::Shaper<'a> {
let mut buffer = buffer.0;
// This signature cannot report a failure, and every way shaping can
// fail is a programming error, so panic.
if let Err(err) = self.shape_buffer(&mut buffer, options) {
if let Err(err) = self.shape_buffer_inner(&mut buffer, options) {
panic!("{err}");
}
GlyphBuffer(buffer)
}

pub(crate) fn shape_buffer(
/// Shapes a buffer in place using this prepared shaper.
///
/// On success the buffer holds [`BufferContentType::Glyphs`]. If a plan
/// is supplied through [`ShapeOptions::plan`] it must have been built for
/// this buffer's direction and script.
///
/// # Errors
///
/// Returns a [`ShapeError`] when the buffer has already been shaped, has
/// no direction and no plan, or does not match the supplied plan.
#[cfg(feature = "experimental_font_api")]
pub fn shape_buffer(
&self,
buffer: &mut Buffer,
options: ShapeOptions<'_>,
) -> Result<(), ShapeError> {
self.shape_buffer_inner(buffer, options)
}

fn shape_buffer_inner(
&self,
buffer: &mut Buffer,
options: ShapeOptions<'_>,
Expand Down Expand Up @@ -695,11 +760,16 @@ impl<'a> crate::Shaper<'a> {
Ok(())
}

pub(crate) fn glyph_names(&self) -> GlyphNames<'a> {
/// The names the face gives its glyphs, from `post` or from the CFF
/// charset, or nothing when it names none.
pub fn glyph_names(&self) -> GlyphNames<'a> {
GlyphNames::new(&self.font)
}

pub(crate) fn glyph_metrics(&self) -> GlyphMetrics<'a> {
if let Some(metrics) = &self.glyph_metrics {
return metrics.clone();
}
match &self.font {
FontKind::FontRef(data) => data.glyph_metrics.clone(),
FontKind::FontInstance(instance, metrics) => {
Expand Down
29 changes: 27 additions & 2 deletions harfrust/src/hb/font_funcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,19 @@ impl<'a> BuiltinFontFuncs<'a> {
}

fn charmap(&self) -> &Charmap<'a> {
if let Some(charmap) = &self.face.charmap {
return charmap;
}
self.charmap.get_or_init(|| match &self.face.font {
FontKind::FontRef(font) => font.charmap.clone(),
FontKind::FontInstance(instance, _) => Charmap::from_tables(&instance.tables()),
})
}

fn glyph_metrics(&self) -> &GlyphMetrics<'a> {
if let Some(metrics) = &self.face.glyph_metrics {
return metrics;
}
self.glyph_metrics.get_or_init(|| match &self.face.font {
FontKind::FontRef(font) => font.glyph_metrics.clone(),
FontKind::FontInstance(instance, metrics) => {
Expand All @@ -240,12 +246,19 @@ impl<'a> BuiltinFontFuncs<'a> {

/// Maps a Unicode scalar value to a nominal glyph.
pub fn nominal_glyph(&self, c: u32) -> Option<GlyphId> {
self.charmap().map(c)
// A cmap entry pointing at .notdef says the font has no glyph for
// the character, rather than that its glyph is .notdef. HarfBuzz
// reads it the same way, and the difference shows once a caller
// asks for a not-found glyph of its own.
self.charmap().map(c).filter(|glyph| glyph.to_u32() != 0)
}

/// Maps a Unicode scalar value and variation selector to a glyph.
pub fn variant_glyph(&self, c: u32, vs: u32) -> Option<GlyphId> {
self.charmap().map_variant(c, vs)
// As in `nominal_glyph`: .notdef is not a glyph the font has.
self.charmap()
.map_variant(c, vs)
.filter(|glyph| glyph.to_u32() != 0)
}

/// Returns the horizontal advance for a glyph.
Expand Down Expand Up @@ -393,6 +406,18 @@ pub trait FontFuncs {
}
}

impl<'a> hb_font_t<'a> {
/// The callbacks that read the font's own tables, which shaping falls
/// back on when nothing else answers.
///
/// These know about the legacy cmap subtables -- Macintosh Roman, and the
/// Windows symbol encoding's private-use pages -- so a caller looking a
/// glyph up outside shaping finds the same one shaping would.
pub fn builtin_font_funcs(&'a self) -> BuiltinFontFuncs<'a> {
BuiltinFontFuncs::new(self)
}
}

pub(crate) struct FontFuncsDispatch<'a, 'u> {
builtin: BuiltinFontFuncs<'a>,
scale: Scale,
Expand Down
Loading