lib/format_parser/image.ex

defmodule FormatParser.Image do
  alias __MODULE__

  @moduledoc """
  An Image struct and functions.

  The Image struct contains the fields format, width_px, height_px, intrinsics and nature.
  """

  defstruct [:format, :width_px, :height_px, nature: :image, intrinsics: %{}]

  @doc """
  Parses a file and extracts some information from it.

  Takes a `binary file` as argument.

  Returns a struct which contains all information that has been extracted from the file if the file is recognized.

  Returns the following tuple if file not recognized: `{:error, file}`.

  """
  def parse({:error, file}) when is_binary(file) do
    parse_image(file)
  end

  def parse(file) when is_binary(file) do
    parse_image(file)
  end

  def parse(result) do
    result
  end

  # credo:disable-for-lines:18 Credo.Check.Refactor.CyclomaticComplexity
  defp parse_image(file) do
    case file do
      <<0x89, "PNG", 0x0D, 0x0A, 0x1A, 0x0A, x::binary>> -> parse_png(x)
      <<"BM", x::binary>> -> parse_bmp(x)
      <<"GIF89a", x::binary>> -> parse_gif(x)
      <<"GIF87a", x::binary>> -> parse_gif(x)
      <<0xFF, 0xD8, 0xFF, x::binary>> -> parse_jpeg(x)
      <<"II", 0x2A, 0x00, x::binary>> -> parse_tif(x)
      <<"MM", 0x00, 0x2A, x::binary>> -> parse_tif(x, true)
      <<0x00, 0x00, 0x01, 0x00, x::binary>> -> parse_ico(x)
      <<0x00, 0x00, 0x02, 0x00, x::binary>> -> parse_cur(x)
      <<"8BPS", x::binary>> -> parse_psd(x)
      <<0x97, "JB2", 0x0D, 0x0A, 0x1A, 0x0A, x::binary>> -> parse_jb2(x)
      <<"gimp xcf", x::binary>> -> parse_xcf(x)
      <<0x76, 0x2F, 0x31, 0x01, x::binary>> -> parse_exr(x)
      <<"RIFF", _::binary-size(4), "WEBP", x::binary>> -> parse_webp(x)
      _ -> {:error, file}
    end
  end

  defp parse_exr(<<_::binary>>) do
    %Image{format: :exr}
  end

  defp parse_xcf(<<_::binary>>) do
    %Image{format: :xcf}
  end

  defp parse_jb2(<<_::binary>>) do
    %Image{format: :jb2}
  end

  defp parse_webp(<<_::binary>>) do
    %Image{format: :webp}
  end

  defp parse_psd(<<_::size(80), height::size(32), width::size(32), _::binary>>) do
    %Image{format: :psd, width_px: width, height_px: height}
  end

  defp parse_ico(
         <<_::size(16), width::size(8), height::size(8), num_color_palette::size(8), 0x00,
           color_planes::size(16), bits_per_pixel::size(16), _::binary>>
       ) do
    width_px = if width == 0, do: 256, else: width
    height_px = if height == 0, do: 256, else: height

    intrinsics = %{
      num_color_palette: num_color_palette,
      color_planes: color_planes,
      bits_per_pixel: bits_per_pixel
    }

    %Image{
      format: :ico,
      width_px: width_px,
      height_px: height_px,
      intrinsics: intrinsics
    }
  end

  defp parse_cur(
         <<_::size(16), width::size(8), height::size(8), num_color_palette::size(8), 0x00,
           hotspot_horizontal_coords::size(16), hotspot_vertical_coords::size(16), _::binary>>
       ) do
    width_px = if width == 0, do: 256, else: width
    height_px = if height == 0, do: 256, else: height

    intrinsics = %{
      num_color_palette: num_color_palette,
      hotspot_horizontal_coords: hotspot_horizontal_coords,
      hotspot_vertical_coords: hotspot_vertical_coords
    }

    %Image{
      format: :cur,
      width_px: width_px,
      height_px: height_px,
      intrinsics: intrinsics
    }
  end

  defp parse_tif(<<ifd0_offset::little-integer-size(32), x::binary>>) do
    ifd_0 = parse_ifd0(x, shift(ifd0_offset, 8), false)
    width = ifd_0[256].value
    height = ifd_0[257].value

    make =
      parse_make_tag(
        x,
        shift(ifd_0[271][:value], 8),
        shift(ifd_0[271][:length], 0)
      )

    model =
      parse_make_tag(
        x,
        shift(ifd_0[272][:value], 8),
        shift(ifd_0[272][:length], 0)
      )

    date_time =
      parse_make_tag(
        x,
        shift(ifd_0[306][:value], 8),
        shift(ifd_0[306][:length], 0)
      )

    intrinsics = %{
      preview_offset: ifd_0[273].value,
      preview_byte_count: ifd_0[279].value,
      model: model,
      date_time: date_time
    }

    cond do
      Regex.match?(~r/canon.+/i, make) ->
        %Image{
          format: :cr2,
          width_px: width,
          height_px: height,
          intrinsics: intrinsics
        }

      Regex.match?(~r/nikon.+/i, make) ->
        %Image{
          format: :nef,
          width_px: width,
          height_px: height,
          intrinsics: intrinsics
        }

      make == "" ->
        %Image{format: :tif, width_px: width, height_px: height}
    end
  end

  defp parse_tif(<<ifd0_offset::big-integer-size(32), x::binary>>, _) do
    ifd_0 = parse_ifd0(x, shift(ifd0_offset, 8), true)
    width = ifd_0[256].value
    height = ifd_0[257].value

    make =
      parse_make_tag(
        x,
        shift(ifd_0[271][:value], 8),
        shift(ifd_0[271][:length], 0)
      )

    if Regex.match?(~r/nikon.+/i, make) do
      %Image{format: :nef}
    else
      %Image{format: :tif, width_px: width, height_px: height}
    end
  end

  defp parse_ifd0(<<x::binary>>, offset, big_endian) when big_endian == false do
    <<_::size(offset), ifdc::little-integer-size(16), rest::binary>> = x
    ifds_sizes = ifdc * 12 * 8
    <<ifd_set::size(ifds_sizes), _::binary>> = rest
    parse_ifds(<<ifd_set::size(ifds_sizes)>>, big_endian, %{})
  end

  defp parse_ifd0(<<x::binary>>, offset, big_endian) when big_endian == true do
    <<_::size(offset), ifd_count::size(16), rest::binary>> = x
    ifds_sizes = ifd_count * 12 * 8
    <<ifd_set::size(ifds_sizes), _::binary>> = rest
    parse_ifds(<<ifd_set::size(ifds_sizes)>>, big_endian, %{})
  end

  defp parse_ifds(<<>>, _, accumulator), do: accumulator

  defp parse_ifds(<<x::binary>>, big_endian, accumulator) do
    ifd = parse_ifd(<<x::binary>>, big_endian)
    parse_ifds(ifd.ifd_left, big_endian, Map.merge(ifd, accumulator))
  end

  defp parse_ifd(
         <<tag::little-integer-size(16), _::little-integer-size(16),
           length::little-integer-size(32), value::little-integer-size(32), ifd_left::binary>>,
         big_endian
       )
       when big_endian == false do
    %{tag => %{tag: tag, length: length, value: value}, ifd_left: ifd_left}
  end

  defp parse_ifd(
         <<tag::size(16), type::size(16), length::size(32), value::size(32), ifd_left::binary>>,
         big_endian
       )
       when big_endian == true and type != 3 do
    %{tag => %{tag: tag, length: length, value: value}, ifd_left: ifd_left}
  end

  defp parse_ifd(
         <<tag::size(16), type::size(16), length::size(32), value::size(32), ifd_left::binary>>,
         big_endian
       )
       when big_endian == true and type == 3 do
    <<value::size(16), _::binary>> = <<value::size(32)>>
    %{tag => %{tag: tag, length: length, value: value}, ifd_left: ifd_left}
  end

  defp shift(offset, _) when is_nil(offset), do: 0
  defp shift(offset, byte), do: (offset - byte) * 8

  defp parse_make_tag(<<x::binary>>, offset, len) do
    <<_::size(offset), make_tag::size(len), _::binary>> = x
    <<make_tag::size(len)>>
  end

  defp parse_gif(<<width::little-integer-size(16), height::little-integer-size(16), _::binary>>) do
    %Image{format: :gif, width_px: width, height_px: height}
  end

  defp parse_jpeg(<<_::binary>>) do
    %Image{format: :jpg}
  end

  defp parse_bmp(
         <<_::size(128), width::little-integer-size(32), height::little-integer-size(32),
           _::binary>>
       ) do
    %Image{format: :bmp, width_px: width, height_px: height}
  end

  defp parse_png(
         <<_::size(32), "IHDR", width::size(32), height::size(32), bit_depth, color_type,
           compression_method, filter_method, interlace_method, crc::size(32), _::binary>>
       ) do
    intrinsics = %{
      bit_depth: bit_depth,
      color_type: color_type,
      compression_method: compression_method,
      filter_method: filter_method,
      interlace_method: interlace_method,
      crc: crc
    }

    %Image{
      format: :png,
      width_px: width,
      height_px: height,
      intrinsics: intrinsics
    }
  end
end