53 releases (22 breaking)

0.22.1 Feb 13, 2024
0.21.2 Jan 15, 2024
0.21.0 Dec 16, 2023
0.19.0 Nov 28, 2023
0.0.2 Oct 14, 2022

#552 in Text processing

Download history 192/week @ 2023-10-31 531/week @ 2023-11-07 573/week @ 2023-11-14 376/week @ 2023-11-21 576/week @ 2023-11-28 562/week @ 2023-12-05 858/week @ 2023-12-12 141/week @ 2023-12-19 189/week @ 2023-12-26 64/week @ 2024-01-02 378/week @ 2024-01-09 175/week @ 2024-01-16 140/week @ 2024-01-23 582/week @ 2024-01-30 333/week @ 2024-02-06 709/week @ 2024-02-13

1,769 downloads per month
Used in fea-rs


67K SLoC


This crate contains types for creating and editing font-files.


Writing and modifying OpenType tables

This crate provides a collection of types correlating to what is described in the OpenType spec, along with the logic to serialize these types into binary font tables. It is a companion to read-fonts, which provides efficient zero-allocation parsing of these types. It is intended to be used as the basis for font engineering tools such as compilers.

'write' versus 'read' types

Both write-fonts and read-fonts make heavy use of code generation, and they have a similar structure, where a tables module contains a submodule for each supported table, and that module contains items for each table, record, flagset or enum described in the spec. This means that there are (for instance) two distinct ValueRecord types, one defined in read_fonts::tables::gpos, and one defined in write_fonts::tables::gpos.

The reason for the distinct types is that it allows us to dramatically simplify the scope of read-fonts; the types in that crate are generally just typed views into raw slices of bytes and cannot be modified, whereas the types in write-fonts are generally familiar Rust structs and enums.

Loading and modifying fonts

Although write-fonts does not contain any parsing logic, it does offer the FromTableRef and ToOwnedTable traits (similar to std's From & Into) for converting from a read_fonts type to its write-fonts equivalent. This means that you can read an existing font table into write-fonts; under the hood we will use read-fonts to parse the font, and then convert that to the write-fonts version. In general you do not need to think about this conversion yourself; tables implement the FontRead trait from read-fonts, which handles the reading + conversion logic for you.

When loading and modifying fonts, you will likely need to interact with both write-fonts and read-fonts directly. To avoid having to manage both of these dependencies, there is a "read" feature on write-fonts that reexports read-fonts as read at the crate root:

# Cargo.toml
write-fonts = { version = "*", features = ["read"] }
// main.rs
use write_fonts::read::FontRef;

Writing subtables

A font table commonly contains some set of subtables which are referenced in the font binary as offsets relative to the position (within the file) of the parent table; and these subtables can themselves contain subtables, and so on. We refer to the entire structure of tables as the 'table graph'. A consequence of this structure is that compiling a table is not as simple as just sequentially writing out the bytes of each field; it also involves computing an ordering for the subtables, determining their position in the final binary, and correctly writing that position in the appropriate location in any tables that reference that subtable.

As most subtable positions (offsets) are stored as 16-bit integers, it is possible in certain cases that offsets overflow. The task of finding a suitable ordering for each table in the table graph is called "table packing". write-fonts handles the packing of tables at serialization time, based on the hb-repacker implementation from HarfBuzz.


Create an 'hhea' table

use write_fonts::{tables::hhea::Hhea, types::{FWord, UfWord}};

let my_table = Hhea {
    ascender: FWord::new(700),
    descender: FWord::new(-195),
    line_gap: FWord::new(0),
    advance_width_max: UfWord::new(1200),
    min_left_side_bearing: FWord::new(-80),
    min_right_side_bearing: FWord::new(-420),
    x_max_extent: FWord::new(1122),
    caret_slope_rise: 1,
    caret_slope_run: 0,
    caret_offset: 0,
    number_of_long_metrics: 301,

let _bytes = write_fonts::dump_table(&my_table).expect("failed to write bytes");

Read/modify/write an existing font

use read_fonts::{FontRef, TableProvider};
use write_fonts::{
let font_bytes = std::fs::read(path_to_my_font_file).unwrap();
let font = FontRef::new(&font_bytes).expect("failed to read font data");
let mut head: Head = font.head().expect("missing 'head' table").to_owned_table();
head.modified  = seconds_since_font_epoch();
let new_bytes = FontBuilder::new()
    .unwrap() // errors if we can't compile 'head', unlikely here
std::fs::write("mynewfont.ttf", &new_bytes).unwrap();


~34K SLoC