lib/pdf/font/metrics.ex

defmodule Pdf.Font.Metrics do
  @moduledoc false

  defmodule Glyph do
    @moduledoc false
    defstruct name: nil,
              char_code: 0x00,
              width: 0,
              bbox: []
  end

  defstruct name: nil,
            full_name: nil,
            family_name: nil,
            weight: nil,
            italic_angle: nil,
            encoding: nil,
            first_char: 0,
            last_char: 255,
            ascender: nil,
            descender: nil,
            cap_height: nil,
            x_height: nil,
            fixed_pitch: false,
            bbox: nil,
            widths: [],
            glyphs: %{},
            kern_pairs: []

  def widths(metrics, encoding \\ Pdf.Encoding.WinAnsi) do
    Enum.map(encoding.characters(), fn {_, _, name} ->
      case metrics.glyphs[name] do
        nil -> 0
        %{width: width} -> width
      end
    end)
  end

  def map_widths(font) do
    Pdf.Encoding.WinAnsi.characters()
    |> Enum.map(fn {_, char, name} ->
      width =
        case font.glyphs[name] do
          nil ->
            0

          %{width: width} ->
            width
        end

      {char, width}
    end)
    |> Map.new()
  end

  def process_line(<<"FontName ", data::binary>>, metrics), do: %{metrics | name: data}
  def process_line(<<"FullName ", data::binary>>, metrics), do: %{metrics | full_name: data}
  def process_line(<<"FamilyName ", data::binary>>, metrics), do: %{metrics | family_name: data}

  def process_line(<<"Weight ", data::binary>>, metrics) do
    %{metrics | weight: String.to_atom(String.downcase(data))}
  end

  def process_line(<<"ItalicAngle ", data::binary>>, metrics) do
    case Float.parse(data) do
      {angle, ""} -> %{metrics | italic_angle: angle}
      _ -> metrics
    end
  end

  def process_line(<<"EncodingScheme ", data::binary>>, metrics), do: %{metrics | encoding: data}

  def process_line(<<"CapHeight ", data::binary>>, metrics),
    do: %{metrics | cap_height: String.to_integer(data)}

  def process_line(<<"XHeight ", data::binary>>, metrics),
    do: %{metrics | x_height: String.to_integer(data)}

  def process_line(<<"Ascender ", data::binary>>, metrics),
    do: %{metrics | ascender: String.to_integer(data)}

  def process_line(<<"Descender ", data::binary>>, metrics),
    do: %{metrics | descender: String.to_integer(data)}

  def process_line("IsFixedPitch true", metrics),
    do: %{metrics | fixed_pitch: true}

  def process_line(<<"FontBBox ", data::binary>>, metrics) do
    bbox =
      data
      |> String.split(" ", trim: true)
      |> Enum.map(fn f -> Float.parse(f) |> elem(0) end)
      |> List.to_tuple()

    %{metrics | bbox: bbox}
  end

  def process_line(<<"C ", _rest::binary>> = line, %{glyphs: glyphs} = metrics) do
    glyph = parse_glyph(line)

    if Pdf.Encoding.WinAnsi.from_name(glyph.name) do
      %{metrics | glyphs: Map.put(glyphs, glyph.name, glyph)}
    else
      metrics
    end
  end

  def process_line(<<"KPX ", data::binary>>, %{kern_pairs: kern_pairs} = metrics) do
    case String.split(data) do
      [first, second, amount] ->
        first_char_code = Pdf.Encoding.WinAnsi.from_name(first)
        second_char_code = Pdf.Encoding.WinAnsi.from_name(second)

        if first_char_code && second_char_code do
          {amount, _} = Integer.parse(amount)
          %{metrics | kern_pairs: [{first_char_code, second_char_code, amount} | kern_pairs]}
        else
          metrics
        end

      _ ->
        metrics
    end
  end

  def process_line(_line, metrics), do: metrics

  defp parse_glyph(line) do
    line
    |> String.trim()
    |> String.split(~r/\s*;\s*/)
    |> parse_glyph(%Glyph{})
  end

  defp parse_glyph([], glyph), do: glyph

  defp parse_glyph(["" | tail], glyph),
    do: parse_glyph(tail, glyph)

  defp parse_glyph([<<"C ", _rest::binary>> | tail], glyph),
    do: parse_glyph(tail, glyph)

  defp parse_glyph([<<"N ", name::binary>> | tail], glyph) do
    char_code = Pdf.Encoding.WinAnsi.from_name(name)
    parse_glyph(tail, %{glyph | name: name, char_code: char_code})
  end

  defp parse_glyph([<<"WX ", width::binary>> | tail], glyph) do
    glyph =
      case Integer.parse(width) do
        {width, _} -> %{glyph | width: width}
        :error -> glyph
      end

    parse_glyph(tail, glyph)
  end

  defp parse_glyph([<<"B ", data::binary>> | tail], glyph) do
    bbox =
      data
      |> String.split(" ", trim: true)
      |> Enum.map(fn f -> Float.parse(f) |> elem(0) end)
      |> Enum.map(fn f -> :erlang.float_to_binary(f, decimals: 1) end)

    %{glyph | bbox: bbox}

    parse_glyph(tail, glyph)
  end

  defp parse_glyph([_ | tail], glyph),
    do: parse_glyph(tail, glyph)
end