lib/pdf/export.ex

defprotocol Pdf.Export do
  @moduledoc false

  def to_iolist(object)
end

defimpl Pdf.Export, for: List do
  def to_iolist([]), do: []
  def to_iolist(list), do: Enum.map(list, &Pdf.Export.to_iolist/1)
end

defimpl Pdf.Export, for: BitString do
  def to_iolist(string), do: string
end

defimpl Pdf.Export, for: Integer do
  def to_iolist(number), do: Pdf.Export.to_iolist(Integer.to_string(number))
end

defimpl Pdf.Export, for: Float do
  def to_iolist(number),
    do: Pdf.Export.to_iolist(:erlang.float_to_binary(number, [:compact, decimals: 4]))
end

defimpl Pdf.Export, for: Date do
  def to_iolist(date) do
    [
      "(D:",
      to_string(date.year),
      pad_datepart(date.month),
      pad_datepart(date.day),
      ")"
    ]
  end

  defp pad_datepart(part), do: part |> to_string |> String.pad_leading(2, "0")
end

defimpl Pdf.Export, for: DateTime do
  def to_iolist(date) do
    [
      "(D:",
      to_string(date.year),
      pad_datepart(date.month),
      pad_datepart(date.day),
      pad_datepart(date.hour),
      pad_datepart(date.minute),
      pad_datepart(date.second),
      timezone_info(date),
      ")"
    ]
  end

  defp pad_datepart(part), do: part |> to_string |> String.pad_leading(2, "0")

  defp timezone_info(%{utc_offset: 0}), do: "Z"
  defp timezone_info(%{utc_offset: offset}) when offset > 0, do: ["+", format_offset(offset)]
  defp timezone_info(%{utc_offset: offset}) when offset < 0, do: ["-", format_offset(abs(offset))]

  defp format_offset(offset) do
    minutes = round(offset / 60)
    hours = round(minutes / 60)
    [pad_datepart(hours), "'", pad_datepart(rem(minutes, 60))]
  end
end

defimpl Pdf.Export, for: Tuple do
  def to_iolist({:string, string}), do: ["(", string, ")"]

  def to_iolist({:name, name}), do: ["/", name]

  def to_iolist({:object, number, generation}),
    do: [Integer.to_string(number), " ", Integer.to_string(generation), " R"]

  def to_iolist({:command, []}), do: []

  def to_iolist({:command, [head | tail]}),
    do: [Pdf.Export.to_iolist(head), Enum.map(tail, &[" ", Pdf.Export.to_iolist(&1)])]

  def to_iolist({:command, command}), do: command
end