lib/y4m/header_parser.ex

defmodule Y4m.HeaderParser do
  @moduledoc """
  Parser to deserialize y4m file header according to the specification found here:
  https://wiki.multimedia.cx/index.php/YUV4MPEG2
  """

  # TODO: explore options to infer color_space from file length
  @default_values %{color_space: :C420}

  @doc """
  Deserialize first line in y4m file.
  """
  @spec parse(binary) :: {:ok, any()} | {:error, atom}
  def parse(header_line) do
    # split the header line into list of strings
    case String.split(header_line) do
      ["YUV4MPEG2" | rest] ->
        case continue(rest, @default_values) do
          %{} = props -> {:ok, props}
          {:error, reason} -> {:error, reason}
        end

      _ ->
        {:error, :missing_yuvmpeg2_header}
    end
  end

  defp continue([], props), do: props

  defp continue([<<"W", width::binary>> | t], props) do
    case Integer.parse(width) do
      {width, _} -> continue(t, Map.put(props, :width, width))
      :error -> {:error, :invalid_width}
    end
  end

  defp continue([<<"H", height::binary>> | t], props) do
    case Integer.parse(height) do
      {height, _} -> continue(t, Map.put(props, :height, height))
      :error -> {:error, :invalid_height}
    end
  end

  defp continue([<<"F", frame_rate::binary>> | t], props) do
    with [nom, den] <- String.split(frame_rate, ":"),
         [{nom, rest}, {den, rest}] <- Enum.map([nom, den], &Integer.parse/1) do
      continue(t, Map.put(props, :frame_rate, [nom, den]))
    else
      _ -> {:error, :invalid_frame_rate}
    end
  end

  defp continue([<<"I", interlacing::binary>> | t], props) do
    case interlacing do
      "p" -> continue(t, Map.put(props, :interlacing, :progressive))
      "t" -> continue(t, Map.put(props, :interlacing, :top_field_first))
      "b" -> continue(t, Map.put(props, :interlacing, :bottom_field_first))
      "m" -> continue(t, Map.put(props, :interlacing, :mixed))
      _ -> {:error, :invalid_interlacing}
    end
  end

  defp continue([<<"A", aspect_ratio::binary>> | t], props) do
    with [nom, den] <- String.split(aspect_ratio, ":"),
         [{nom, rest}, {den, rest}] <- Enum.map([nom, den], &Integer.parse/1) do
      continue(t, Map.put(props, :aspect_ratio, [nom, den]))
    else
      _ -> {:error, :invalid_aspect_ratio}
    end
  end

  defp continue([<<"C", color_space::binary>> | t], props) do
    case color_space |> String.downcase() do
      "420jpeg" -> {:error, :unsupported_color_space}
      "420paldv" -> {:error, :unsupported_color_space}
      "420mpeg2" -> continue(t, Map.put(props, :color_space, :C420MPEG2))
      "420" -> continue(t, Map.put(props, :color_space, :C420))
      "422" -> continue(t, Map.put(props, :color_space, :C422))
      "444" -> continue(t, Map.put(props, :color_space, :C444))
      "mono" -> continue(t, Map.put(props, :color_space, :Cmono))
      _ -> {:error, :invalid_color_space}
    end
  end

  defp continue([<<"X", param::binary>> | t], props) do
    case param |> String.split("=") do
      [name, value] ->
        params = Map.get(props, :params, [])
        continue(t, Map.put(props, :params, params ++ [[name, value]]))

      _ ->
        # ignore invalid property
        continue(t, props)
    end
  end
end