lib/pdf/external_font.ex

defmodule Pdf.ExternalFont do
  @moduledoc false
  @derive {Inspect, only: [:name, :family_name, :weight, :italic_angle]}
  defstruct name: nil,
            font_file: nil,
            dictionary: 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: 0,
            x_height: nil,
            fixed_pitch: false,
            bbox: nil,
            widths: [],
            glyph_widths: %{},
            glyphs: %{},
            kern_pairs: []

  import Pdf.Utils

  alias Pdf.Font.Metrics
  alias Pdf.{Array, Dictionary}

  @stream_start "\nstream\n"
  @stream_end "\nendstream"

  def load(path) do
    font_metrics =
      path
      |> File.stream!()
      |> Enum.reduce(%Metrics{}, fn line, metrics ->
        Metrics.process_line(String.replace_suffix(line, "\n", ""), metrics)
      end)

    font_file_name = Path.rootname(path) <> ".pfb"
    font_file = File.read!(font_file_name)

    part1 = (:binary.match(font_file, "eexec") |> elem(0)) + 6
    part2 = (:binary.match(font_file, "00000000") |> elem(0)) - part1
    part3 = byte_size(font_file) - part1 - part2

    widths = Metrics.widths(font_metrics)
    last_char = font_metrics.first_char + length(widths) - 1

    %__MODULE__{
      name: font_metrics.name,
      full_name: font_metrics.full_name,
      family_name: font_metrics.family_name,
      weight: font_metrics.weight,
      italic_angle: font_metrics.italic_angle,
      encoding: font_metrics.encoding,
      first_char: font_metrics.first_char,
      last_char: last_char,
      ascender: font_metrics.ascender,
      descender: font_metrics.descender,
      cap_height: font_metrics.cap_height || 0,
      x_height: font_metrics.x_height,
      fixed_pitch: font_metrics.fixed_pitch,
      bbox: font_metrics.bbox,
      widths: widths,
      glyph_widths: Metrics.map_widths(font_metrics),
      glyphs: font_metrics.glyphs,
      kern_pairs: font_metrics.kern_pairs,
      font_file: font_file,
      dictionary: font_file_dictionary(part1, part2, part3)
    }
  end

  def font_dictionary(font, id, desc_id) do
    Dictionary.new()
    |> Dictionary.put("Type", n("Font"))
    |> Dictionary.put("Subtype", n("Type1"))
    |> Dictionary.put("Name", n("F#{id}"))
    |> Dictionary.put("BaseFont", n(font.name))
    |> Dictionary.put("FirstChar", font.first_char)
    |> Dictionary.put("LastChar", font.last_char)
    |> Dictionary.put("Widths", Array.new(Enum.map(font.widths, &to_string/1)))
    |> Dictionary.put("Encoding", n("WinAnsiEncoding"))
    |> Dictionary.put("FontDescriptor", desc_id)
  end

  def font_descriptor_dictionary(font, ff_id) do
    flags = 32

    Dictionary.new()
    |> Dictionary.put("Type", n("FontDescriptor"))
    |> Dictionary.put("Flags", flags)
    |> Dictionary.put("FontName", n(font.name))
    |> Dictionary.put("StemV", 100)
    |> Dictionary.put("Ascent", font.ascender / 1)
    |> Dictionary.put("Descent", font.descender / 1)
    |> Dictionary.put("FontBBox", Array.new(Tuple.to_list(font.bbox)))
    |> Dictionary.put("ItalicAngle", font.italic_angle)
    |> Dictionary.put("CapHeight", font.cap_height / 1)
    |> Dictionary.put("FontFile", ff_id)
  end

  def font_file_dictionary(part1, part2, part3) do
    Dictionary.new()
    |> Dictionary.put("Length1", part1)
    |> Dictionary.put("Length2", part2)
    |> Dictionary.put("Length3", part3)
    |> Dictionary.put("Length", part1 + part2 + part3)
  end

  def size(%__MODULE__{} = font) do
    # size_of(font.dictionary) + byte_size(font.font_file) + byte_size(@stream_start <> @stream_end)
    byte_size(to_iolist(font) |> Enum.join())
  end

  @doc """
  Returns the width of the specific character

  Examples:

    iex> ExternalFont.width(font, "A")
    123
  """
  def width(font, <<char_code::integer>> = str) when is_binary(str) do
    width(font, char_code)
  end

  def width(font, char_code) do
    font.glyph_widths[char_code] || 0
  end

  def kern_text(_font, ""), do: [""]

  def kern_text(font, <<first::integer, second::integer, rest::binary>>) do
    font.kern_pairs
    |> Enum.find(fn {f, s, _amount} -> f == first && s == second end)
    |> case do
      {f, _s, amount} ->
        [<<f>>, -amount | kern_text(font, <<second::integer, rest::binary>>)]

      nil ->
        [head | tail] = kern_text(font, <<second::integer, rest::binary>>)
        [<<first::integer, head::binary>> | tail]
    end
  end

  def kern_text(_font, <<_::integer>> = char), do: [char]

  def to_iolist(%__MODULE__{} = font) do
    Pdf.Export.to_iolist([
      font.dictionary,
      @stream_start,
      font.font_file,
      @stream_end
    ])
  end

  defimpl Pdf.Size do
    def size_of(%Pdf.ExternalFont{} = font), do: Pdf.ExternalFont.size(font)
  end

  defimpl Pdf.Export do
    def to_iolist(%Pdf.ExternalFont{} = font), do: Pdf.ExternalFont.to_iolist(font)
  end
end