diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1adc2c8..38f6d92 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,15 +13,15 @@ jobs: test: strategy: matrix: - elixir: ['1.8', '1.9', '1.10', '1.11'] + elixir: ["1.8", "1.9", "1.10", "1.11"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions/setup-elixir@v1 + - uses: erlef/setup-elixir@v1 with: - otp-version: '22.3.4' + otp-version: "22.3.4" elixir-version: ${{ matrix.elixir }} - uses: actions-rs/toolchain@v1 diff --git a/.gitignore b/.gitignore index bbe9c14..3dfcd72 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ erl_crash.dump imageflow_ex-*.tar native/imageflow_ex/target/ +priv/ diff --git a/lib/imageflow/graph.ex b/lib/imageflow/graph.ex index 51a90f8..f53f674 100644 --- a/lib/imageflow/graph.ex +++ b/lib/imageflow/graph.ex @@ -90,6 +90,18 @@ defmodule Imageflow.Graph do |> append_node(%{decode: %{io_id: io_id}}) end + @doc """ + Appends a string to be decoded + """ + @spec decode_string(t, binary) :: t + def decode_string(%{io_count: io_count} = graph, string) do + io_id = io_count + 1 + + graph + |> add_input(io_id, {:bytes, string}) + |> append_node(%{decode: %{io_id: io_id}}) + end + @doc """ Specifies a destination file for the current branch of the pipeline @@ -112,11 +124,42 @@ defmodule Imageflow.Graph do Check the official [encoding documentation](https://docs.imageflow.io/json/encode.html) to see the parameters available to each encoder """ @spec encode_to_file(t, binary, binary | atom, map) :: t - def encode_to_file(%{io_count: io_count} = graph, path, encoder \\ :png, opts \\ %{}) do + def encode_to_file(graph, path, encoder \\ :png, opts \\ %{}) do + add_encode_node(graph, {:file, path}, encoder, opts) + end + + @doc """ + Returns the converted image as a string. + + No further processing operations should be appended at the current branch after this call. + + The last two arguments specify the encoder and optional encoding parameters. + + The following parameters are valid encoders: + * `:jpg`: Alias to `:mozjpeg` + * `:jpeg`: Alias to `:mozjpeg` + * `:png`: Alias to `:lodepng` + * `:webp: Alias to `:webplossless + * `:mozjpeg` + * `:gif` + * `:lodepng`: Lossless PNG + * `:pngquant`: Lossy PNG + * `:webplossy`: Lossy WebP + * `:webplossless`: Lossless WebP + + Check the official [encoding documentation](https://docs.imageflow.io/json/encode.html) to see the parameters available to each encoder + """ + + @spec encode_to_string(t, binary | atom, map) :: t + def encode_to_string(graph, encoder \\ :png, opts \\ %{}) do + add_encode_node(graph, :bytes, encoder, opts) + end + + defp add_encode_node(%{io_count: io_count} = graph, output_type, encoder, opts) do io_id = io_count + 1 graph - |> add_output(io_id, {:file, path}) + |> add_output(io_id, output_type) |> append_node(%{encode: %{io_id: io_id, preset: preset_for(encoder, opts)}}) end @@ -312,6 +355,8 @@ defmodule Imageflow.Graph do GraphRunner.run(graph) end + def get_results(job, graph), do: GraphRunner.get_results(job, graph) + defp add_input(%{io_count: io_count, inputs: inputs} = graph, io_id, value) do %{graph | io_count: io_count + 1, inputs: Map.put(inputs, io_id, value)} end diff --git a/lib/imageflow/graph_runner.ex b/lib/imageflow/graph_runner.ex index d125d71..bfc3ec8 100644 --- a/lib/imageflow/graph_runner.ex +++ b/lib/imageflow/graph_runner.ex @@ -1,14 +1,36 @@ defmodule Imageflow.GraphRunner do - alias Imageflow.{Graph, Native} + alias Imageflow.{Graph, Native, Result} def run(%Graph{} = graph) do with {:ok, job} <- Native.create(), :ok <- add_inputs(job, graph.inputs), :ok <- add_outputs(job, graph.outputs), :ok <- send_task(job, graph), - :ok <- save_outputs(job, graph.outputs), - :ok <- Native.destroy(job) do - :ok + :ok <- save_outputs(job, graph.outputs) do + {:ok, job, graph} + end + end + + def get_results(job, %Graph{outputs: outputs} = graph) do + outputs + |> Enum.reduce_while({:ok, []}, fn {id, value}, {:ok, _acc} -> + case value do + :bytes -> + case Native.get_output_buffer(job, id) do + {:ok, results} -> {:cont, {:ok, :binary.list_to_bin(results)}} + {:error, _} = error -> {:halt, error} + end + + {:file, path} -> + {:cont, {:ok, path}} + end + end) + |> case do + {:ok, results} -> + {:ok, %Result{job: job, graph: graph, output: results}} + + error -> + error end end @@ -18,6 +40,7 @@ defmodule Imageflow.GraphRunner do {id, value}, :ok -> case value do {:file, path} -> Native.add_input_file(job, id, path) + {:bytes, blob} -> Native.add_input_buffer(job, id, blob) end |> case do :ok -> {:cont, :ok} @@ -44,6 +67,8 @@ defmodule Imageflow.GraphRunner do {id, value}, :ok -> case value do {:file, path} -> Native.save_output_to_file(job, id, path) + # skip + :bytes -> :ok end |> case do :ok -> {:cont, :ok} diff --git a/lib/imageflow/native.ex b/lib/imageflow/native.ex index 5a3728e..a38b913 100644 --- a/lib/imageflow/native.ex +++ b/lib/imageflow/native.ex @@ -19,7 +19,7 @@ defmodule Imageflow.Native do {:ok, resp} = Native.message("v0.1/get_image_info", %{io_id: 0}) """ - alias Imageflow.NIF + alias Imageflow.{Graph, NIF} @type t :: %__MODULE__{} @type native_ret_t :: :ok | {:error, binary} @@ -62,12 +62,12 @@ defmodule Imageflow.Native do NIF.job_save_output_to_file(id, io_id, path) end - @spec get_output_buffer(t, number) :: {:ok, binary} | {:error, binary} + @spec get_output_buffer(t, number) :: {:ok, iolist} | {:error, binary} def get_output_buffer(%__MODULE__{id: id} = _job, io_id) do NIF.job_get_output_buffer(id, io_id) end - @spec message(t, binary, binary) :: {:ok, any} | {:error, binary} + @spec message(t, binary, Graph.t()) :: {:ok, any} | {:error, binary} def message(%__MODULE__{id: id}, method, message) do with {:ok, resp} <- NIF.job_message(id, method, Jason.encode!(message)) do {:ok, Jason.decode!(resp)} diff --git a/lib/imageflow/result.ex b/lib/imageflow/result.ex new file mode 100644 index 0000000..6ecb84f --- /dev/null +++ b/lib/imageflow/result.ex @@ -0,0 +1,3 @@ +defmodule Imageflow.Result do + defstruct job: nil, graph: nil, output: nil +end diff --git a/test/imageflow/graph_test.exs b/test/imageflow/graph_test.exs index 14605bb..1b92630 100644 --- a/test/imageflow/graph_test.exs +++ b/test/imageflow/graph_test.exs @@ -9,7 +9,7 @@ defmodule Imageflow.GraphTest do end end - describe "decode_file/1" do + describe "decode_file/2" do test "appends a new input" do graph = Graph.new() |> Graph.decode_file("file.png") @@ -23,7 +23,23 @@ defmodule Imageflow.GraphTest do end end - describe "encode_file/1" do + describe "decode_string/2" do + test "appends a new input" do + {:ok, string} = File.read("test/fixtures/elixir-logo.jpg") + graph = Graph.new() |> Graph.decode_string(string) + + assert %{io_count: 1, inputs: %{1 => {:bytes, _string}}} = graph + end + + test "appends a file decoding operation" do + {:ok, string} = File.read("test/fixtures/elixir-logo.jpg") + graph = Graph.new() |> Graph.decode_string(string) + + assert %{nodes: %{1 => %{decode: %{io_id: 1}}}} = graph + end + end + + describe "encode_to_file/2" do test "appends a new output" do graph = Graph.new() |> Graph.encode_to_file("file.png") @@ -91,6 +107,74 @@ defmodule Imageflow.GraphTest do end end + describe "encode_to_string/2" do + test "appends a new output" do + graph = Graph.new() |> Graph.encode_to_string() + + assert %{io_count: 1, outputs: %{1 => :bytes}} = graph + end + + test "appends a file encoding operation" do + graph = Graph.new() |> Graph.encode_to_string() + + assert %{nodes: %{1 => %{encode: %{io_id: 1}}}} = graph + end + + test "allows appending jpg outputs" do + graph = Graph.new() |> Graph.encode_to_string(:jpg) + + assert %{nodes: %{1 => %{encode: %{io_id: 1, preset: %{mozjpeg: %{quality: 90}}}}}} = graph + end + + test "allows appending jpg outputs with custom parameters" do + graph = Graph.new() |> Graph.encode_to_string(:jpg, %{quality: 10}) + + assert %{nodes: %{1 => %{encode: %{io_id: 1, preset: %{mozjpeg: %{quality: 10}}}}}} = graph + end + + test "allows appending png outputs" do + graph = Graph.new() |> Graph.encode_to_string(:png) + + assert %{ + nodes: %{ + 1 => %{encode: %{io_id: 1, preset: %{lodepng: %{maximum_deflate: false}}}} + } + } = graph + end + + test "allows appending png outputs with custom parameters" do + graph = Graph.new() |> Graph.encode_to_string(:png, %{maximum_deflate: true}) + + assert %{ + nodes: %{1 => %{encode: %{io_id: 1, preset: %{lodepng: %{maximum_deflate: true}}}}} + } = graph + end + + test "allows appending gif outputs" do + graph = Graph.new() |> Graph.encode_to_string(:gif) + + assert %{nodes: %{1 => %{encode: %{io_id: 1, preset: :gif}}}} = graph + end + + test "allows appending gif outputs with custom parameters" do + graph = Graph.new() |> Graph.encode_to_string(:gif, %{a: :b}) + + assert %{nodes: %{1 => %{encode: %{io_id: 1, preset: :gif}}}} = graph + end + + test "allows appending webp outputs" do + graph = Graph.new() |> Graph.encode_to_string(:webp) + + assert %{nodes: %{1 => %{encode: %{io_id: 1, preset: :webplossless}}}} = graph + end + + test "allows appending webp outputs with custom parameters" do + graph = Graph.new() |> Graph.encode_to_string(:webp, %{a: :b}) + + assert %{nodes: %{1 => %{encode: %{io_id: 1, preset: :webplossless}}}} = graph + end + end + describe "constrain/4" do test "appends a constrain operation" do graph = Graph.new() |> Graph.constrain(10, 20) diff --git a/test/integration/graph_test.exs b/test/integration/graph_test.exs index d3c4a35..ff61f3a 100644 --- a/test/integration/graph_test.exs +++ b/test/integration/graph_test.exs @@ -1,21 +1,23 @@ defmodule Imageflow.Integration.GraphTest do use ExUnit.Case - alias Imageflow.{Graph, Native} + alias Imageflow.{Graph, Native, Result} @input_path "test/fixtures/elixir-logo.jpg" @output_path "/tmp/output.png" test "can pipe multiple operations" do - assert :ok = - Graph.new() - |> Graph.decode_file(@input_path) - |> Graph.constrain(20, 20) - |> Graph.rotate_270() - |> Graph.transpose() - |> Graph.color_filter("invert") - |> Graph.encode_to_file(@output_path) - |> Graph.run() + run_result = + Graph.new() + |> Graph.decode_file(@input_path) + |> Graph.constrain(20, 20) + |> Graph.rotate_270() + |> Graph.transpose() + |> Graph.color_filter("invert") + |> Graph.encode_to_file(@output_path) + |> Graph.run() + + assert match?({:ok, _job, _graph}, run_result) end test "can generate multiple images" do @@ -47,12 +49,28 @@ defmodule Imageflow.Integration.GraphTest do end test "can handle multiple operations" do - assert :ok = - Graph.new() - |> Graph.decode_file(@input_path) - |> Graph.flip_vertical() - |> Graph.transpose() - |> Graph.encode_to_file("/tmp/rotated.png") - |> Graph.run() + run_result = + Graph.new() + |> Graph.decode_file(@input_path) + |> Graph.flip_vertical() + |> Graph.transpose() + |> Graph.encode_to_file("/tmp/rotated.png") + |> Graph.run() + + assert match?({:ok, _job, _graph}, run_result) + end + + test "can encode to file" do + {:ok, job, graph} = + Graph.new() + |> Graph.decode_file(@input_path) + |> Graph.flip_vertical() + |> Graph.transpose() + |> Graph.encode_to_string() + |> Graph.run() + + {:ok, %Result{} = results} = Graph.get_results(job, graph) + + assert is_bitstring(results.output) end end