lib/exiffer/header/APP1/EXIF.ex

defmodule Exiffer.Header.APP1.EXIF do
  @moduledoc """
  Documentation for `Exiffer.Header.APP1.EXIF`.
  """

  alias Exiffer.{Binary, Buffer}
  alias Exiffer.IFDBlock
  require Logger

  @exif_header "Exif\0\0"
  @tiff_header_marker <<0x00, 0x2a>>
  @big_endian_marker "MM"

  @enforce_keys ~w(byte_order ifd_block)a
  defstruct ~w(byte_order ifd_block)a

  def new(%{data: <<length_bytes::binary-size(2), @exif_header::binary, _rest::binary>>} = buffer) do
    exif_start = buffer.position
    buffer = Buffer.skip(buffer, 2 + String.length(@exif_header))
    length = Binary.big_endian_to_integer(length_bytes)
    {byte_order_marker, buffer} = Buffer.consume(buffer, 2)
    byte_order = if byte_order_marker == @big_endian_marker, do: :big, else: :little
    Logger.debug "APP1.EXIF.new/1 - setting byte order to :#{byte_order}"
    previous_byte_order = Binary.byte_order()
    Binary.set_byte_order(byte_order)
    tiff_header_marker = Binary.big_endian_to_current(@tiff_header_marker)
    {<<^tiff_header_marker::binary-size(2), ifd_header_offset_binary::binary-size(4)>>, buffer} = Buffer.consume(buffer, 6)
    ifd_header_offset = Binary.to_integer(ifd_header_offset_binary)
    offset = exif_start + ifd_header_offset
    {ifd_block, buffer} = IFDBlock.new(buffer, offset)
    exif = %__MODULE__{byte_order: byte_order, ifd_block: ifd_block}
    exif_end = exif_start + length
    Logger.debug "APP1.EXIF.new/1 read completed, seeking to 0x#{Integer.to_string(exif_end, 16)}"
    buffer = Buffer.seek(buffer, exif_end)
    Logger.debug "APP1.EXIF.new/1 - resetting byte order to previous value: :#{previous_byte_order}"
    Binary.set_byte_order(previous_byte_order)
    {exif, buffer}
  end

  def binary(%__MODULE__{} = exif) do
    Logger.debug "APP1.EXIF.binary/1 - setting byte order to #{exif.byte_order}"
    previous_byte_order = Binary.byte_order()
    Binary.set_byte_order(exif.byte_order)
    tiff_header_marker = Binary.big_endian_to_current(@tiff_header_marker)
    ifd_block = IFDBlock.binary(exif.ifd_block)
    byte_order = if exif.byte_order == :big, do: "MM", else: "II"
    first_ifd_offset_binary = Binary.int32u_to_current(8)
    length = 2 + byte_size(@exif_header) + byte_size(byte_order) + byte_size(tiff_header_marker) + byte_size(first_ifd_offset_binary) + byte_size(ifd_block)
    length_binary = Binary.int16u_to_big_endian(length)
    Logger.debug "APP1.EXIF.binary/1 - resetting byte order to previous value: :#{previous_byte_order}"
    Binary.set_byte_order(previous_byte_order)
    <<
      0xff, 0xe1,
      length_binary::binary,
      @exif_header::binary,
      byte_order::binary,
      tiff_header_marker::binary,
      first_ifd_offset_binary::binary,
      ifd_block::binary
    >>
  end

  def puts(%__MODULE__{} = exif) do
    IO.puts "File"
    IO.puts "----"
    byte_order = if exif.byte_order == :big, do: "Big endian", else: "Little endian"
    IO.puts "Byte order: #{byte_order}"
    IO.puts "General"
    IO.puts "-------"
    IFDBlock.puts(exif.ifd_block)
    :ok
  end

  def write(%__MODULE__{} = exif, io_device) do
    Logger.debug "Writing EXIF header"
    binary = binary(exif)
    :ok = IO.binwrite(io_device, binary)
  end

  defimpl Exiffer.Serialize do
    def write(exif, io_device) do
      Exiffer.Header.APP1.EXIF.write(exif, io_device)
    end

    def binary(exif) do
      Exiffer.Header.APP1.EXIF.binary(exif)
    end

    def puts(exif) do
      Exiffer.Header.APP1.EXIF.puts(exif)
    end
  end
end