lib/pdf/images/jpeg.ex

defmodule Pdf.Images.JPEG do
  @moduledoc false

  import Pdf.Utils

  alias Pdf.{Array, Dictionary, Image}

  defstruct bit_depth: nil,
            height: nil,
            width: nil,
            color_type: nil,
            image_data: <<>>

  @doc ~S"""
  Decodes an image and returns the bit depth, height, width and colour color_type

  Examples:

      > Pdf.Image.JPEG.decode("path/to/image.jpg")
      %Pdf.Image.JPEG{bit_depth: 8, height: 75, width: 100, color_type: 3}
  """
  def decode(image_data) do
    parse_jpeg(image_data)
  end

  defp parse_jpeg(<<255, 216, rest::binary>>), do: parse_jpeg(rest)

  [192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]
  |> Enum.each(fn code ->
    defp parse_jpeg(<<255, unquote(code), _length::unsigned-integer-size(16), rest::binary>>),
      do: parse_image_data(rest)
  end)

  defp parse_jpeg(<<255, _code, length::unsigned-integer-size(16), rest::binary>>) do
    {:ok, data} = chomp(rest, length - 2)
    parse_jpeg(data)
  end

  def parse_image_data(
        <<bits, height::unsigned-integer-size(16), width::unsigned-integer-size(16), color_type,
          _rest::binary>>
      ) do
    %__MODULE__{bit_depth: bits, height: height, width: width, color_type: color_type}
  end

  def parse_image_data(_, _), do: {:error, :parse_error}

  defp chomp(data, length) do
    data = :erlang.binary_part(data, {length, byte_size(data) - length})
    {:ok, data}
  end

  def prepare_image(image_data) do
    %__MODULE__{bit_depth: bit_depth, height: height, width: width, color_type: color_type} =
      decode(image_data)

    build_dictionary(%Image{
      bits: bit_depth,
      height: height,
      width: width,
      color_type: color_type,
      data: image_data,
      size: byte_size(image_data)
    })
  end

  def build_dictionary(%Image{} = image) do
    %{bits: bit_depth, width: width, height: height, color_type: color_type, size: size} = image

    image_dic =
      Dictionary.new(%{
        "Type" => n("XObject"),
        "Subtype" => n("Image"),
        "ColorSpace" => get_colorspace(color_type),
        "BitsPerComponent" => bit_depth,
        "Width" => width,
        "Height" => height,
        "Length" => size,
        "Filter" => Array.new([n("DCTDecode")])
      })

    image_dic =
      if color_type == 4 do
        # Invert colours, See :4.8.4 of the spec
        Dictionary.put(image_dic, "Decode", Array.new([1, 0, 1, 0, 1, 0, 1, 0]))
      else
        image_dic
      end

    %{image | dictionary: image_dic}
  end

  defp get_colorspace(0), do: n("DeviceGray")
  defp get_colorspace(1), do: n("DeviceGray")
  defp get_colorspace(2), do: n("DeviceGray")
  defp get_colorspace(3), do: n("DeviceRGB")
  defp get_colorspace(4), do: n("DeviceCMYK")
  defp get_colorspace(_), do: raise("Unsupported number of JPG color_type")
end