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
@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
@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