lib/exiffer/jpeg.ex

defmodule Exiffer.JPEG do
  @moduledoc """
  Documentation for `Exiffer.JPEG`.
  """

  require Logger

  alias Exiffer.Binary
  alias Exiffer.JPEG.Header.{APP1, APP4, COM, Data, EOI, JFIF, SOF0, SOS}
  import Exiffer.Logging, only: [integer: 1]

  @enforce_keys ~w(headers)a
  defstruct ~w(headers)a

  defimpl Jason.Encoder  do
    @spec encode(%Exiffer.JPEG{}, Jason.Encode.opts()) :: String.t()
    def encode(entry, opts) do
      Jason.Encode.map(
        %{
          module: "Exiffer.JPEG",
          headers: entry.headers
        },
        opts
      )
    end
  end

  @magic <<0xff, 0xd8>>

  def magic, do: @magic

  def new(%{data: <<@magic, _rest::binary>>} = buffer) do
    buffer = Exiffer.Buffer.skip(buffer, 2)
    Logger.debug "JPEG.new/1 - setting initial byte order to :big"
    Binary.set_byte_order(:big)
    {%{} = buffer, headers} = headers(buffer, [])
    {%__MODULE__{headers: Enum.reverse(headers)}, buffer}
  end

  def binary(%__MODULE__{} = jpeg) do
    Logger.info "Exiffer.JPEG creating binary"
    Exiffer.Serialize.binary(jpeg.headers)
  end

  def text(%__MODULE__{} = jpeg) do
    Exiffer.Serialize.text(jpeg.headers)
  end

  def write(%__MODULE__{} = jpeg, io_device) do
    Logger.info "Exiffer.JPEG writing binary"
    :ok = IO.binwrite(io_device, @magic)
    :ok = Exiffer.Serialize.write(jpeg.headers, io_device)
  end

  defp headers(buffer, headers)

  defp headers(%{data: <<0xff, 0xe1, _rest::binary>>} = buffer, headers) do
    Logger.debug "Reading APP1 header at #{integer(buffer.position)}"
    {:ok, app1, buffer} = APP1.new(buffer)
    headers(buffer, [app1 | headers])
  end

  defp headers(%{data: <<0xff, 0xe4, _rest::binary>>} = buffer, headers) do
    Logger.debug "Reading APP4 header at #{integer(buffer.position)}"
    {:ok, app4, buffer} = APP4.new(buffer)
    headers(buffer, [app4 | headers])
  end

  defp headers(%{data: <<0xff, 0xfe, _rest::binary>>} = buffer, headers) do
    Logger.debug "Reading COM header at #{integer(buffer.position)}"
    {:ok, comment, buffer} = COM.new(buffer)
    headers(buffer, [comment | headers])
  end

  defp headers(%{data: <<0xff, 0xe0, _length::binary-size(2), "JFIF", 0x00, _rest::binary>>} = buffer, headers) do
    Logger.debug "Reading JFIF header at #{integer(buffer.position)}"
    {:ok, jfif, buffer} = JFIF.new(buffer)
    headers(buffer, [jfif | headers])
  end

  defp headers(%{data: <<0xff, 0xc0, _rest::binary>>} = buffer, headers) do
    Logger.debug "Reading SOF0 header at #{integer(buffer.position)}"
    {:ok, sof0, buffer} = SOF0.new(buffer)
    headers(buffer, [sof0 | headers])
  end

  defp headers(%{data: <<0xff, 0xd9, _rest::binary>>} = buffer, headers) do
    Logger.debug "Reading EOI header at #{integer(buffer.position)}"
    {:ok, eoi, buffer} = EOI.new(buffer)
    {buffer, [eoi | headers]}
  end

  defp headers(%{data: <<0xff, 0xda, _rest::binary>>} = buffer, headers) do
    Logger.debug "Reading SOS header at #{integer(buffer.position)}"
    {:ok, sos, buffer} = SOS.new(buffer)
    headers(buffer, [sos | headers])
  end

  defp headers(%{} = buffer, headers) do
    Logger.debug "Reading generic data header at #{integer(buffer.position)}"
    {:ok, header, buffer} = Data.new(buffer)
    headers(buffer, [header | headers])
  end

  defimpl Exiffer.Serialize do
    def write(%Exiffer.JPEG{} = jpeg, io_device) do
      Exiffer.JPEG.write(jpeg, io_device)
    end

    def binary(jpeg) do
      Exiffer.JPEG.binary(jpeg)
    end

    def text(%Exiffer.JPEG{} = jpeg) do
      Exiffer.JPEG.text(jpeg)
    end
  end
end