lib/pdf/font.ex

defmodule Pdf.Font do
  @moduledoc false

  import Pdf.Utils
  # alias Pdf.Font.Metrics
  alias Pdf.{Array, Dictionary, Font}

  @derive {Inspect, only: [:name, :family_name, :weight, :italic_angle]}
  defstruct name: nil,
            full_name: nil,
            family_name: nil,
            weight: nil,
            italic_angle: nil,
            encoding: nil,
            first_char: nil,
            last_char: nil,
            ascender: nil,
            descender: nil,
            cap_height: nil,
            x_height: nil,
            fixed_pitch: nil,
            bbox: nil,
            widths: nil,
            glyph_widths: nil,
            glyphs: nil,
            kern_pairs: nil

  def to_dictionary(font, 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", 32)
    |> Dictionary.put("LastChar", font.last_char)
    |> Dictionary.put("Widths", Array.new(Enum.drop(font.widths, 32)))
    |> Dictionary.put("Encoding", n("WinAnsiEncoding"))
  end

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

  Examples:

    iex> Font.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

  @doc ~S"""
  Returns the width of the string in font units (1/1000 of font scale factor)
  """
  def text_width(font, string), do: text_width(font, string, [])

  def text_width(font, string, opts) when is_list(opts) do
    normalized_string =
      Pdf.Text.normalize_string(
        string,
        Keyword.get(opts, :encoding_replacement_character, :raise)
      )

    string_width = calculate_string_width(font, normalized_string)

    kerning_adjustments =
      if Keyword.get(opts, :kerning, false) do
        Font.kern_text(font, normalized_string)
        |> Enum.reject(&is_binary/1)
        |> Enum.reduce(0, &Kernel.+/2)
      else
        0
      end

    string_width - kerning_adjustments
  end

  @doc ~S"""
  Returns the width of a string in points (72 points = 1 inch)
  """
  def text_width(font, string, font_size) when is_number(font_size) do
    text_width(font, string, font_size, [])
  end

  def text_width(font, string, font_size, opts) when is_number(font_size) do
    width = text_width(font, string, opts)
    width * font_size / 1000
  end

  defp calculate_string_width(_font, ""), do: 0

  defp calculate_string_width(font, <<char::integer, rest::binary>>) do
    Font.width(font, char) + calculate_string_width(font, rest)
  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 kern_text(_font, ""), do: [""]

  def kern_text(%Pdf.ExternalFont{} = font, text) do
    Pdf.ExternalFont.kern_text(font, text)
  end

  def kern_text(font, text) do
    font.kern_text(text)
  end

  @doc """
  Lookup a font by family name and attributes [bold: true, italic: true]
  """
  def matches_attributes(font, attrs) do
    bold = Keyword.get(attrs, :bold, false)
    italic = Keyword.get(attrs, :italic, false)

    cond do
      bold && !italic && font.weight == :bold && font.italic_angle == 0 -> true
      bold && italic && font.weight == :bold && font.italic_angle != 0 -> true
      !bold && !italic && font.weight != :bold && font.italic_angle == 0 -> true
      !bold && italic && font.weight != :bold && font.italic_angle != 0 -> true
      true -> false
    end
  end
end