lib/elixir_latex/attachment.ex

defmodule ElixirLatex.Attachment do
  @type body :: iodata | nil
  @type filename :: binary | nil
  @type extension :: atom | binary | nil
  @type mimetype :: binary | nil

  @type t :: %__MODULE__{
          body: body,
          filename: filename,
          extension: extension,
          mimetype: mimetype
        }

  defstruct body: nil, filename: nil, extension: nil, mimetype: nil

  @pattern ~r/^data:([a-z]+\/[a-z]+)(;base64)?,?/

  def from_data_url(data_url) when is_binary(data_url) do
    with {:ok, mimetype, binary} <- parse_data_url(data_url) do
      %__MODULE__{
        body: binary,
        extension: get_extension(mimetype),
        mimetype: mimetype,
        filename: random_filename()
      }
    end
  end

  @spec is_valid_data_url(data_url :: term) :: boolean
  def is_valid_data_url(data_url) when is_binary(data_url) do
    parse_data_url(data_url) != :error
  end

  def is_valid_data_url(_), do: false

  @spec parse_data_url(data_url :: binary) :: {:ok, binary, binary} | :error
  def parse_data_url(data_url) when is_binary(data_url) do
    case Regex.scan(@pattern, data_url) do
      [[match, mimetype, ";base64"]] ->
        base64 = remove_match(data_url, match)
        {:ok, binary} = Base.decode64(base64)
        {:ok, mimetype, binary}

      [[match, mimetype]] ->
        uri_encoded = remove_match(data_url, match)
        binary = URI.decode(uri_encoded)
        {:ok, mimetype, binary}

      _ ->
        :error
    end
  end

  defp remove_match(data_url, match) do
    data_range = Range.new(String.length(match), -1)

    String.slice(data_url, data_range)
    |> String.trim_trailing()
  end

  def random_filename do
    :crypto.strong_rand_bytes(10)
    |> Base.encode16(case: :lower)
  end

  def get_extension(mimetype) do
    mimetype
    |> MIME.extensions()
    |> List.first()
  end
end