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
2,135 downloads per month
Used in 12 crates
(11 directly)
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