lib/waffle/processor.ex

defmodule Waffle.Processor do
  @moduledoc ~S"""
  Apply transformation to files.

  Waffle can be used to facilitate transformations of uploaded files
  via any system executable.  Some common operations you may want to
  take on uploaded files include resizing an uploaded avatar with
  ImageMagick or extracting a still image from a video with FFmpeg.

  To transform an image, the definition module must define a
  `transform/2` function which accepts a version atom and a tuple
  consisting of the uploaded file and corresponding scope.

  This transform handler accepts the version atom, as well as the
  file/scope argument, and is responsible for returning one of the
  following:

    * `:noaction` - The original file will be stored as-is.

    * `:skip` - Nothing will be stored for the provided version.

    * `{executable, args}` - The `executable` will be called with
      `System.cmd` with the format
      `#{original_file_path} #{args} #{transformed_file_path}`.

    * `{executable, fn(input, output) -> args end}` If your executable
      expects arguments in a format other than the above, you may
      supply a function to the conversion tuple which will be invoked
      to generate the arguments. The arguments can be returned as a
      string (e.g. – `" #{input} -strip -thumbnail 10x10 #{output}"`)
      or a list (e.g. – `[input, "-strip", "-thumbnail", "10x10",
      output]`) for even more control.

    * `{executable, args, output_extension}` - If your transformation
      changes the file extension (eg, converting to `png`), then the
      new file extension must be explicit.

    * `fn version, file -> {:ok, file} end` - Implement custom
      transformation as elixir function,
      [read more about custom transformations](custom_transformation.livemd)

    * `{transform/2, fn version, file -> :png end}` - A custom
      transformation converting a file into a different extension

  ## ImageMagick transformations

  As images are one of the most commonly uploaded filetypes, Waffle
  has a recommended integration with ImageMagick's `convert` tool for
  manipulation of images.  Each definition module may specify as many
  versions as desired, along with the corresponding transformation for
  each version.

  The expected return value of a `transform` function call must either
  be `:noaction`, in which case the original file will be stored
  as-is, `:skip`, in which case nothing will be stored, or `{:convert,
  transformation}` in which the original file will be processed via
  ImageMagick's `convert` tool with the corresponding transformation
  parameters.

  The following example stores the original file, as well as a squared
  100x100 thumbnail version which is stripped of comments (eg, GPS
  coordinates):

      defmodule Avatar do
        use Waffle.Definition

        @versions [:original, :thumb]

        def transform(:thumb, _) do
          {:convert, "-strip -thumbnail 100x100^ -gravity center -extent 100x100"}
        end
      end

  Other examples:

      # Change the file extension through ImageMagick's `format` parameter:
      {:convert, "-strip -thumbnail 100x100^ -gravity center -extent 100x100 -format png", :png}

      # Take the first frame of a gif and process it into a square jpg:
      {:convert, fn(input, output) -> "#{input}[0] -strip -thumbnail 100x100^ -gravity center -extent 100x100 -format jpg #{output}", :jpg}

  For more information on defining your transformation, please consult
  [ImageMagick's convert
  documentation](http://www.imagemagick.org/script/convert.php).

  > **Note**: Keep this transformation function simple and deterministic based on the version, file name, and scope object. The `transform` function is subsequently called during URL generation, and the transformation is scanned for the output file format.  As such, if you conditionally format the image as a `png` or `jpg` depending on the time of day, you will be displeased with the result of Waffle's URL generation.

  > **System Resources**: If you are accepting arbitrary uploads on a public site, it may be prudent to add system resource limits to prevent overloading your system resources from malicious or nefarious files.  Since all processing is done directly in ImageMagick, you may pass in system resource restrictions through the [-limit](http://www.imagemagick.org/script/command-line-options.php#limit) flag.  One such example might be: `-limit area 10MB -limit disk 100MB`.

  ## FFmpeg transformations

  Common transformations of uploaded videos can be also defined
  through your definition module:

      # To take a thumbnail from a video:
      {:ffmpeg, fn(input, output) -> "-i #{input} -f jpg #{output}" end, :jpg}

      # To convert a video to an animated gif
      {:ffmpeg, fn(input, output) -> "-i #{input} -f gif #{output}" end, :gif}

  ## Complex Transformations

  `Waffle` requires the output of your transformation to be located at
  a predetermined path.  However, the transformation may be done
  completely outside of `Waffle`. For fine-grained transformations,
  you should create an executable wrapper in your $PATH (eg. bash
  script) which takes these proper arguments, runs your
  transformation, and then moves the file into the correct location.

  For example, to use `soffice` to convert a doc to an html file, you
  should place the following bash script in your $PATH:

      #!/usr/bin/env sh

      # `soffice` doesn't allow for output file path option, and waffle can't find the
      # temporary file to process and copy. This script has a similar argument list as
      # what waffle expects. See https://github.com/stavro/arc/issues/77.

      set -e
      set -o pipefail

      function convert {
          soffice \
              --headless \
              --convert-to html \
              --outdir $TMPDIR \
              "$1"
      }

      function filter_new_file_name {
          awk -F$TMPDIR '{print $2}' \
          | awk -F" " '{print $1}' \
          | awk -F/ '{print $2}'
      }

      converted_file_name=$(convert "$1" | filter_new_file_name)

      cp $TMPDIR/$converted_file_name "$2"
      rm $TMPDIR/$converted_file_name

  And perform the transformation as such:

      def transform(:html, _) do
        {:soffice_wrapper, fn(input, output) -> [input, output] end, :html}
      end

  """
  alias Waffle.Transformations.Convert

  def process(definition, version, {file, scope}) do
    transform = definition.transform(version, {file, scope})
    apply_transformation(file, transform, version)
  end

  @spec apply_transformation(
          Waffle.File.t(),
          (Waffle.File.t() -> {:ok, Waffle.File.t()} | {:error, String.t()}),
          atom()
        ) :: {:ok, Waffle.File.t()} | {:error, String.t()}

  defp apply_transformation(_, :skip, _), do: {:ok, nil}
  defp apply_transformation(file, :noaction, _), do: {:ok, file}
  # Deprecated
  defp apply_transformation(file, {:noaction}, _), do: {:ok, file}

  defp apply_transformation(file, func, version) when is_function(func), do: func.(version, file)

  defp apply_transformation(file, {func, _}, version) when is_function(func),
    do: func.(version, file)

  defp apply_transformation(file, {cmd, conversion}, _) do
    Convert.apply(cmd, file, conversion)
  end

  defp apply_transformation(file, {cmd, conversion, extension}, _) do
    Convert.apply(cmd, file, conversion, extension)
  end
end