63 releases (30 breaking)

new 0.30.0 Nov 1, 2024
0.28.1 Jul 25, 2024
0.26.0 Mar 20, 2024
0.21.0 Dec 16, 2023
0.0.2 Oct 14, 2022

#632 in Parser implementations

Download history 303/week @ 2024-07-12 312/week @ 2024-07-19 270/week @ 2024-07-26 209/week @ 2024-08-02 86/week @ 2024-08-09 290/week @ 2024-08-16 242/week @ 2024-08-23 370/week @ 2024-08-30 269/week @ 2024-09-06 209/week @ 2024-09-13 72/week @ 2024-09-20 596/week @ 2024-09-27 586/week @ 2024-10-04 592/week @ 2024-10-11 291/week @ 2024-10-18 399/week @ 2024-10-25

2,135 downloads per month
Used in 12 crates (11 directly)

MIT/Apache

3.5MB
85K SLoC

write-fonts

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


lib.rs:

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

When modifying a font, you will typically start by reading the font data using read-fonts. When you come to write the font out, however, you will quickly discover that tables generated by read-fonts have different types to those required by write-fonts. This is because read-fonts types borrow their data from an underlying backing store, but write-fonts types need to own their data. In order to modify a table parsed by read-fonts, you will need to convert it to a write-fonts type. This can be done with the FromTableRef and ToOwnedTable traits: bring these traits into scope and call .to_owned_table() or .to_owned_obj() on the table or subtable you want to modify. For example:

use read_fonts::{FontRef, TableProvider};
use write_fonts::{from_obj::ToOwnedTable, tables::head::Head, types::LongDateTime};
// ...
let mut head: Head = font.head().expect("missing 'head' table").to_owned_table();
head.modified = seconds_since_font_epoch();

(For a full example, see below.)

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
[dependencies]
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.

Examples

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::{
    from_obj::ToOwnedTable,
    tables::head::Head,
    types::LongDateTime,
    FontBuilder,
};
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()
    .add_table(&head)
    .unwrap() // errors if we can't compile 'head', unlikely here
    .copy_missing_tables(font)
    .build();
std::fs::write("mynewfont.ttf", &new_bytes).unwrap();

Dependencies

~1.7–2.5MB
~50K SLoC