Vivid.OpenType (vivid v1.0.0)

Copy Markdown View Source

Reads OpenType fonts - both the TrueType outlines in a .ttf and the PostScript ones in a .otf - and says clearly why it can't when it can't.

Examples

iex> Path.join(:code.priv_dir(:vivid), "fonts/roboto-subset.ttf")
...> |> Vivid.OpenType.load!()
...> |> Vivid.Font.line("lo", 16)
...> |> to_string()
"@@@@@@@@@@@@@@\n" <>
"@  @@@@@@@@@@@\n" <>
"@  @@@@@@@@@@@\n" <>
"@  @@@@@@@@@@@\n" <>
"@  @@@@   @@@@\n" <>
"@  @@@      @@\n" <>
"@  @@       @@\n" <>
"@  @   @@@   @\n" <>
"@  @   @@@   @\n" <>
"@  @   @@@@  @\n" <>
"@  @   @@@   @\n" <>
"@  @@   @@   @\n" <>
"@  @@       @@\n" <>
"@  @@@     @@@\n" <>
"@@@@@@@@@@@@@@\n"

Text is where antialiasing earns its keep, and at this size it's the difference between legible and not. The same two letters again, drawn into a frame taking four samples per pixel, so a partly covered pixel comes out as something between ink and paper rather than having to choose.

iex> font = Vivid.OpenType.load!(Path.join(:code.priv_dir(:vivid), "fonts/roboto-subset.ttf"))
...> frame = Vivid.Frame.init(14, 15, Vivid.RGBA.white())
...> text =
...>   font
...>   |> Vivid.Font.line("lo", 16)
...>   |> Vivid.Transform.center(frame)
...>   |> Vivid.Transform.apply()
...> frame
...> |> Vivid.Frame.push(text, Vivid.RGBA.black())
...> |> Vivid.Frame.samples(4)
...> |> to_string()
"@@@@@@@@@@@@@@\n" <>
"@+*@@@@@@@@@@@\n" <>
"@  @@@@@@@@@@@\n" <>
"@  @@@@@@@@@@@\n" <>
"@  @@@@@@@@@@@\n" <>
"@  @@@:   =@@@\n" <>
"@  @@. .:  -@@\n" <>
"@  @+ :@@%  @@\n" <>
"@  @. *@@@: +@\n" <>
"@  @  @@@@: +@\n" <>
"@  @. #@@@: +@\n" <>
"@  @- :@@@  @@\n" <>
"@  @@. .=. :@@\n" <>
"@  @@%.   :@@@\n" <>
"@@@@@@@@@@@@@@\n"

What's supported

Glyph outlines from either the glyf table or a CFF table, a character map from a format 4 or format 12 cmap subtable, and advance widths from hmtx. Format 12 is what reaches codepoints above U+FFFF, so a font's emoji and its rarer CJK characters are drawable where the font has them. TrueType composite glyphs - which is what every accented character is - are assembled from their components, and CFF charstrings are interpreted including their subroutines.

A variable font loads and renders at its default instance, normally the regular weight, because a variable font is an ordinary font whose glyf table holds that instance. It can't be varied, but it isn't refused and doesn't look wrong.

WOFF files are read too. A WOFF is an ordinary font with each table individually zlib compressed, which :zlib - preloaded in erts, so no dependency - inflates back into exactly the tables the rest of this module already reads.

What isn't, and what happens instead

CID-keyed CFF fonts, CFF2, bitmap-only fonts and WOFF2 containers are all detected and reported by name rather than failing on a bad match. WOFF2 is the awkward one: it's compressed with Brotli, which can't be decompressed in pure Elixir, so it needs converting ahead of time.

Kerning is not applied. Nearly all modern fonts keep their kerning in GPOS rather than in the legacy kern table - of 697 fonts surveyed, 88% had GPOS and 4% had kern - so reading kern would help almost nobody, and reading GPOS is a much larger job.

Summary

Functions

Read the font at path.

Read the font at path, raising ArgumentError if it can't be read.

Functions

load(path)

@spec load(Path.t()) :: {:ok, Vivid.Font.t()} | {:error, String.t()}

Read the font at path.

Returns {:error, reason} with a reason worth showing a user, rather than raising, for anything from a missing file to a font this library can't read.

Examples

iex> {:ok, font} = Vivid.OpenType.load(Path.join(:code.priv_dir(:vivid), "fonts/roboto-subset.ttf"))
...> font.units_per_em
2048

iex> Vivid.OpenType.load("/no/such/font.ttf")
{:error, "couldn't read /no/such/font.ttf: no such file or directory"}

iex> Vivid.OpenType.load(Path.join(:code.priv_dir(:vivid), "hershey/rowmans.jhf"))
{:error, "not a font this library recognises"}

The same font as a WOFF draws the same thing. A WOFF is the same font, with each of its tables individually zlib compressed, so once they're inflated there's nothing left to tell apart.

iex> dir = :code.priv_dir(:vivid)
...> sfnt = Vivid.OpenType.load!(Path.join(dir, "fonts/roboto-subset.ttf"))
...> woff = Vivid.OpenType.load!(Path.join(dir, "fonts/roboto-subset.woff"))
...> to_string(Vivid.Font.line(woff, "lo", 16)) == to_string(Vivid.Font.line(sfnt, "lo", 16))
true

load!(path)

@spec load!(Path.t()) :: Vivid.Font.t()

Read the font at path, raising ArgumentError if it can't be read.

Example

iex> Path.join(:code.priv_dir(:vivid), "fonts/roboto-subset.ttf")
...> |> Vivid.OpenType.load!()
...> |> Vivid.Font.glyph(?A)
...> |> Vivid.TrueType.Glyph.advance()
1336