Python bindings for HarfRust, a pure-Rust port of the HarfBuzz text shaping engine.
Text shaping is the process of converting a sequence of Unicode codepoints into positioned glyphs — selecting the right glyph forms, applying ligatures, kerning, and reordering as required by the script. It is a critical step in any text rendering pipeline, especially for complex scripts like Arabic, Devanagari, and Thai.
HarfBuzz is the industry-standard text shaping engine used by Firefox, Chrome, Android, and many other platforms. HarfRust is a faithful pure-Rust port of HarfBuzz, and this project aims to make that engine accessible from Python.
- Standalone Python package — a proper, independently usable Python library for text shaping, not just a test utility.
- Two-tier API — a high-level
shape()function for quick one-shot shaping, and a lower-level object API (Font,Buffer,GlyphBuffer) for full control over the shaping pipeline. - Pythonic interface — string-based construction for configuration types (
Direction("rtl"),Feature("+kern"),Variation("wght=700")), iteration over glyph results, and clear error messages. - HarfBuzz test compatibility — ability to run against HarfBuzz's
.testsregression format, enabling direct comparison between the two engines.
Wheels are not yet published. To build from source you need a Rust toolchain (>= 1.85) and Python (>= 3.11):
git clone https://github.com/hasanzakeri/harfrust-python.git
cd harfrust-python
pip install maturin
maturin develop --releaseThe package is imported as pyharfrust:
import pyharfrust
print(pyharfrust.__version__)PEP 561 type stubs (__init__.pyi, py.typed) ship with the package, so editors and type checkers see the full API.
Two ways to shape a string. Both produce the same output.
from pyharfrust import shape
result = shape("path/to/font.ttf", "Hello World", "")
# "[H=0+733|e=1+598|l=2+336|l=3+336|o=4+631|space=5+272|W=6+871|o=7+631|r=8+380|l=9+336|d=10+629]"The third argument accepts the same flags as the hb-shape CLI:
shape("font.ttf", "AB", "--features=+kern,-liga --direction=ltr")For full CLI parity, run_from_args(argv) accepts the same argument list as hb-shape — argv[0] is the program name, the rest are flags:
from pyharfrust import run_from_args
run_from_args(["hb-shape", "--font-file=font.ttf", "--features=+kern", "Hello"])For repeated shaping, font configuration, or access to per-glyph metadata, use the object API:
from pyharfrust import Buffer, Feature, Font
font = Font("path/to/font.ttf")
buf = Buffer()
buf.add_str("Hello World")
buf.guess_segment_properties() # infers direction/script/language
glyphs = font.shape(buf, features=[Feature("+kern")])
for info, pos in glyphs:
print(f"glyph={info.glyph_id} cluster={info.cluster} "
f"advance=({pos.x_advance},{pos.y_advance}) "
f"offset=({pos.x_offset},{pos.y_offset})")The serialize() method produces the same string format as the high-level shape() function:
print(glyphs.serialize(font))All configuration types accept either a string or their structured form. Strings parse with the same syntax as hb-shape:
from pyharfrust import Direction, Feature, Language, Script, Variation
Direction("ltr") # or Direction.LTR
Script("Latn") # 4-letter ISO 15924 tag
Language("en-US")
Feature("+kern") # enable; "-liga" disables; "kern[3:5]=2" applies a range
Variation("wght=700") # variable-font axis settingfrom pyharfrust import Font, Variation
font = Font("variable.ttf")
font.set_variations([Variation("wght=700"), Variation("wdth=85")])
# or
font.set_variations("wght=700,wdth=85")
# Reset to defaults:
font.set_variations([])Buffers are consumed by shape(). Recycle them via GlyphBuffer.clear():
from pyharfrust import Buffer, Font
font = Font("font.ttf")
buf = Buffer()
buf.add_str("First")
buf.guess_segment_properties()
glyphs = font.shape(buf)
# Reuse the same allocation for a new shaping call:
buf = glyphs.clear()
buf.add_str("Second")
buf.guess_segment_properties()
glyphs = font.shape(buf)GlyphBuffer.clear() consumes the glyph buffer: any further access to the original glyphs instance — including a second clear() — raises ValueError. The same applies to a Buffer once it has been passed to shape().
RuntimeError— font cannot be loaded or parsed.ValueError— invalid string input (Direction("xyz"),Feature("=")), unset buffer direction at shape time, or use of an already-consumed buffer.TypeError— wrong argument types (e.g. assigning a string toBuffer.direction).
- PyO3 + maturin — the standard modern toolchain for building Rust extensions for Python.
- Owned-container pattern — the Python
Fontobject owns all its backing data, with transient Rust borrows scoped to individual method calls. This cleanly bridges Rust's lifetime system and Python's garbage-collected memory model. - Standalone project — not a member of the harfrust Cargo workspace, allowing an independent release cadence and CI configuration.
This project is in early development. See the development plan for the phased roadmap.
MIT