Vivid.CFF (vivid v1.0.0)

Copy Markdown View Source

Reads the Compact Font Format table carried by OpenType fonts with PostScript outlines - the ones whose files begin with OTTO.

Roughly one font in seven uses these rather than TrueType outlines: of 697 fonts surveyed on a development machine, 94 were CFF, which is every Adobe-derived family among them.

CFF stores almost everything in two structures. An INDEX is a count, a table of offsets, and the data those offsets divide up. A DICT is a run of operands followed by the operator they belong to, which is backwards from most formats and the main thing to keep in mind when reading this module.

Glyphs themselves are Type 2 charstrings, interpreted by Vivid.CFF.Charstring.

Summary

Functions

Parse a DICT into a map of operator to its operands.

Split an INDEX into its objects, returning them and whatever follows.

Parse a CFF table.

Types

t()

@type t() :: %Vivid.CFF{charstrings: tuple(), gsubrs: tuple(), subrs: tuple()}

Functions

dict(binary)

@spec dict(binary()) :: %{required(integer() | {integer(), integer()}) => [number()]}

Parse a DICT into a map of operator to its operands.

Operands come before the operator they belong to. Operators are a single byte, except that a byte of 12 escapes into a second byte, in which case the key is a two element tuple.

Example

139 encodes zero, and 17 is the operator saying where the charstrings are.

iex> Vivid.CFF.dict(<<139, 17>>)
%{17 => [0]}

index(rest)

@spec index(binary()) :: {[binary()], binary()}

Split an INDEX into its objects, returning them and whatever follows.

Examples

iex> Vivid.CFF.index(<<2::16, 1::8, 1::8, 3::8, 5::8, "ab", "cd", "rest">>)
{["ab", "cd"], "rest"}

An empty INDEX is just a zero count.

iex> Vivid.CFF.index(<<0::16, "rest">>)
{[], "rest"}

parse(cff)

@spec parse(binary()) :: {:ok, t()} | {:error, String.t()}

Parse a CFF table.

Example

A minimal font, built up a piece at a time. The Top DICT is the only part carrying anything: operand 21, then operator 17, meaning "the charstrings are 21 bytes in" - which is exactly where they land once the four INDEXes before them are counted.

iex> header = <<1, 0, 4, 1>>
...> name_index = <<0::16>>
...> top_dict = <<29::8, 21::32, 17::8>>
...> top_dict_index = <<1::16, 1::8, 1::8, byte_size(top_dict) + 1::8>> <> top_dict
...> string_index = <<0::16>>
...> gsubr_index = <<0::16>>
...> charstring = <<139, 139, 21, 14>>
...> charstrings = <<1::16, 1::8, 1::8, byte_size(charstring) + 1::8>> <> charstring
...> {:ok, parsed} =
...>   Vivid.CFF.parse(
...>     header <> name_index <> top_dict_index <> string_index <> gsubr_index <> charstrings
...>   )
...> tuple_size(parsed.charstrings)
1