HarfRust Text Shaping Engine

repository·main·Indexed 43 days ago

https://github.com/harfbuzz/harfrust

A Rust port of the HarfBuzz text shaping engine, optimized for the Rust font ecosystem and compatible with HarfBuzz v14.3.1. It leverages read-fonts for parsing and provides a C API (harfrust_capi) that mirrors the HarfBuzz shaping API. HarfRust is designed for multi-threaded environments and offers a command-line utility, hr-shape, for text shaping.

Tokens
31.8K
Snippets
114
Records
156
Agent score
56%

What's inside HarfRust

  1. Overview of HarfRust

    main

    HarfRust is a Rust port of the HarfBuzz text shaping engine. It is designed to provide text shaping capabilities within the Rust ecosystem, specifically targeting compatibility with the skrifa ecosystem by using read-fonts for font parsing. It currently matches HarfBuzz [v14.3.1].

    Key characteristics:

    • Safety: The library is completely safe; there is no unsafe code in the library or most of its dependencies (excluding bytemuck).
    • Performance: It is generally less than 25% slower than the original HarfBuzz on common fonts.
    • Conformance: While it passes most HarfBuzz tests and fuzzing suites, it may return errors on malformed fonts where HarfBuzz would typically use a fallback/dummy shaper.
  2. Manage object lifetimes and reference counting

    main

    HarfRust objects use reference counting:

    • Constructors (hr_*_create) return a new reference that the caller owns.
    • hr_*_reference increments the reference count.
    • hr_*_destroy decrements the reference count.

    Error Handling Behavior:

    • hr_*_create functions never return NULL. On failure, they return an "immortal empty object".
    • hr_*_or_fail variants return NULL on failure.
    • Getters accept NULL and behave as if passed an empty object. This allows for chaining calls without constant null checks; errors will manifest as empty shaping results rather than crashes.

    Immutability: Objects can be made immutable using hr_*_make_immutable. Once frozen, all subsequent setter calls are ignored.

  3. Threading model and safety in HarfRust

    main

    HarfRust is designed for multi-threaded environments, but requires following specific ownership rules:

    Thread-Safe Operations

    • Shared Objects: Blobs, faces, and fonts can be used for shaping across multiple threads simultaneously. Reference counts are atomic, and internal caches (per-face plan cache, user data, lazily loaded tables) are protected by locks or atomics.
    • Thread-Safe Callbacks: Unlike HarfBuzz, table callbacks (used in hr_face_create_for_tables) and font callbacks must be thread-safe, as they may be invoked from any thread that first touches a specific table.

    Thread-Unsafe Operations (Ownership Rules)

    • Buffers: A buffer belongs to exactly one thread. Shaping writes to the buffer, so it must not be shared.
    • Modifying Fonts/Callbacks: You must not modify a font (e.g., via hr_font_set_scale, hr_font_set_variations, or hr_font_set_funcs) or a callback set while other threads are shaping with them.
    • Best Practice: Populate and configure fonts and callbacks before sharing them across threads. Use hr_font_make_immutable or hr_font_funcs_make_immutable to prevent accidental races.
  4. Important differences between HarfRust and HarfBuzz

    main

    When using HarfRust instead of the original C++ HarfBuzz, be aware of the following architectural and behavioral changes:

    • Scaling: HarfRust has no font size property. Shaping is always performed using UnitsPerEm. You must scale the resulting glyph metrics manually to your desired font size.
    • Font Loading: Font loading and parsing are handled by read-fonts rather than internal HarfBuzz logic.
    • No External Integrations: HarfRust does not integrate with external system libraries. It does not support:
      • Font-loading via FreeType, CoreText, or Uniscribe/DirectWrite.
      • ICU or GLib Unicode functions.
      • graphite2 library support.
    • No Graphite Support: Support for graphite fonts is not included.
    • Feature Parity: Experimental HarfBuzz features (such as most of the boring-expansion-spec) are not yet supported.
  5. Compare benchmark results using JSON output

    main

    To compare performance across different runs, export the results to JSON using --benchmark_out and use the comparison script provided in the benchmark subproject.

    # Export results
    $ build/perf/benchmark-shape --benchmark_out=results.json
    
    # Compare two JSON files (replace 1.8.4 with your actual version)
    $ subprojects/benchmark-1.8.4/tools/compare.py benchmarks BEFORE.json AFTER.json
  6. Use a local HarfRust or Fontations version

    main

    By default, HarfBuzz pulls HarfRust from the GitHub main branch. You can modify src/rust/Cargo.toml to use a specific branch or a local path.

    Using a local HarfRust path

    In src/rust/Cargo.toml:

    [dependencies]
    harfrust = { path = "../path/to/harfrust", optional = true }

    Using a local Fontations path

    If using a local HarfRust, you must also update the HarfRust Cargo.toml to point to your local fontations path under the read-fonts dependency:

    read-fonts = { path = "../fontations/read-fonts", default-features = false, features = ["libm"] }

    Forcing a rebuild

    HarfBuzz will not automatically rebuild HarfRust or Fontations if you change their source code. To force a rebuild of the shaper, touch the HarfBuzz rust shaper file:

    $ touch src/rust/shaper.rs && ninja -C build
    # Example modification in src/rust/Cargo.toml
    [dependencies]
    harfrust = { path = "../../../harfrust", optional = true }
  7. Use HarfRust as a drop-in replacement for HarfBuzz

    main

    You can use include/hr-hb.h to map HarfBuzz names directly to HarfRust counterparts. By including <hr-hb.h> instead of <hb.h>, existing HarfBuzz shaping code can often be ported by simply renaming hb_ to hr_ (via the header's macros).

    Important Constraints:

    • No Co-existence: You cannot include both HarfBuzz and the compatibility header in the same translation unit, as the macros would conflict.
    • Shaping Only: This compatibility layer only covers the shaping API. Other HarfBuzz features (like drawing, subsetting, or layout introspection) will cause compilation errors.
    #include <hr-hb.h>
    
    hb_blob_t   *blob   = hb_blob_create_from_file("font.ttf");
    hb_face_t   *face   = hb_face_create(blob, 0);
    hb_font_t   *font   = hb_font_create(face);
    hb_buffer_t *buffer = hb_buffer_create();
    
    hb_buffer_add_utf8(buffer, "Hello", -1, 0, -1);
    hb_buffer_guess_segment_properties(buffer);
    hb_shape(font, buffer, NULL, 0);
  8. Build Ragel with Rust support

    main

    To generate Rust code from Ragel scripts, you must use the latest development branch of Ragel, as the stable version does not support native Rust generation. Building Ragel requires first building colm as a dependency. On macOS, ensure you have automake, autoconf, and libtool installed via Homebrew.

    Follow these steps to build the dependencies:

    1. Build colm: Clone the repository, run the autogen script, configure with a custom prefix, and install.
    2. Build ragel: Clone the repository, run the autogen script, and configure using the --with-colm flag pointing to your colm installation path.
    # Install macOS dependencies
    brew install automake autoconf libtool
    
    # 1. Build colm
    git clone https://github.com/adrian-thurston/colm
    cd colm
    ./autogen.sh
    ./configure --prefix=/path/to/colm/install
    make
    make install
    
    cd ..
    
    # 2. Build ragel
    git clone https://github.com/adrian-thurston/ragel
    ./autogen.sh
    ./configure --prefix=/path/to/ragel/install --with-colm=/path/to/colm/install
    make
    make install
  9. Regenerate Rust-side shared Unicode/OT tables

    main

    To regenerate the shared Unicode and OpenType (OT) tables used on the Rust side, use the wrapper makefile located in the scripts directory.

    By default, the makefile assumes the HarfBuzz repository is located at ~/harfbuzz. If your HarfBuzz installation is in a different location, override this using the HARFBUZZ_DIR environment variable.

    # Using default path (~/harfbuzz)
    make -f update-unicode-tables.mk
    
    # Using a custom HarfBuzz path
    make -f update-unicode-tables.mk HARFBUZZ_DIR=/path/to/harfbuzz
  10. Run and filter HarfBuzz benchmark tests

    main

    The build/perf/benchmark-shape tool runs shaping benchmarks against compiled backends.

    Useful CLI Flags

    • --benchmark_filter=REGEX: Selects specific benchmarks. Use --benchmark_filter=harfrust to run only HarfRust benchmarks, or --benchmark_filter='/(ot|harfrust)' to compare HarfRust against the standard HarfBuzz shaper (ot).
    • --benchmark_out=FILE: Writes results to a JSON file for comparison.
    • --benchmark_repetitions=NUM: Runs benchmarks multiple times for more stable results.

    Running against custom fonts

    To benchmark a specific font and text file, pass them as arguments to the tool: build/perf/benchmark-shape <FONT_FILE> <TEXT_FILE> [FLAGS]

    # Run only HarfRust benchmarks
    $ build/perf/benchmark-shape --benchmark_filter=harfrust
    
    # Compare HarfRust and the standard 'ot' shaper
    $ build/perf/benchmark-shape --benchmark_filter='/(ot|harfrust)'
    
    # Benchmark a specific font and text file
    $ build/perf/benchmark-shape Gulzar-Regular.ttf perf/texts/fa-words.txt --benchmark_filter='(ot|harfrust)'
  11. Generate shaping tests with gen-shaping-tests.py

    main

    The gen-shaping-tests.py script generates shaping tests. It assumes the HarfBuzz repository is located at ~/harfbuzz by default.

    If the script detects that ~/harfbuzz/builddir exists but ~/harfbuzz/builddir/util/hb-shape is missing, it will automatically attempt to run ninja -C ~/harfbuzz/builddir before proceeding.

    # Generate tests using default HarfBuzz path
    python3 ./gen-shaping-tests.py
    
    # Generate tests specifying a custom HarfBuzz path
    python3 ./gen-shaping-tests.py /path/to/harfbuzz