defmodule Vix.Vips.OperationHelper do @moduledoc false alias Vix.Nif alias Vix.Type alias Vix.GObject.GParamSpec def input_to_nif_terms(args, in_pspec) do Enum.map( args, fn {name, value} -> param_spec = Map.get(in_pspec, List.to_atom(name)) {name, Type.to_nif_term(value, param_spec)} end ) end def output_to_erl_terms(nif_out_args, required_out_pspec, optional_out_pspec) do res = nif_out_args |> Enum.reduce({[], []}, fn {name, value}, {required, optional} -> param = List.to_atom(name) cond do Map.has_key?(required_out_pspec, param) -> param_spec = Map.get(required_out_pspec, param) value = Type.to_erl_term(value, param_spec) {[value | required], optional} Map.has_key?(optional_out_pspec, param) -> param_spec = Map.get(optional_out_pspec, param) value = Type.to_erl_term(value, param_spec) {required, [{param, value} | optional]} true -> raise "Invalid operation output" end end) case res do {[], []} -> :ok # if it is single value then unwrap and return as single term {[term], []} -> {:ok, term} {required, optional} -> out = [optional | required] |> Enum.reverse() |> List.to_tuple() {:ok, out} end end def prepare_doc(desc, required_in, optional_in, required_out, optional_out) do doc_required_in = Enum.map_join(required_in, "\n", fn pspec -> " * #{pspec.param_name} - #{pspec.desc}" end) """ #{String.capitalize(to_string(desc))} ## Arguments #{doc_required_in} #{optional_in_doc(optional_in)} #{output_values_doc(required_out, optional_out)} """ end def reject_unsupported_operations(op_name) do {_desc, args} = vips_operation_arguments(op_name) Enum.any?(args, fn %{flags: flags, value_type: value_type} -> # we do not support mutable operations yet. Skip operations which use un-supported types as arguments :vips_argument_modify in flags || value_type == "VipsSource" || value_type == "VipsTarget" || value_type == "VipsBlob" end) end def operation_args(name) do {desc, args} = vips_operation_arguments(name) args = Enum.reject(args, fn %{flags: flags} -> # skip required deprecated arguments, but allow optional deprecated arguments. # This is similar to ruby-vips. :vips_argument_required in flags && :vips_argument_deprecated in flags end) {input, rest} = Enum.split_with(args, fn %{flags: flags} -> :vips_argument_input in flags end) {output, _rest} = Enum.split_with(rest, fn %{flags: flags} -> :vips_argument_output in flags end) {required_input, optional_input} = Enum.split_with(input, fn %{flags: flags} -> :vips_argument_required in flags end) {required_output, optional_output} = Enum.split_with(output, fn %{flags: flags} -> :vips_argument_required in flags end) # TODO: support VipsInterpolate and other types optional_input = Enum.filter(optional_input, &Type.supported?/1) required_input = Enum.sort_by(required_input, & &1.priority) required_output = Enum.sort_by(required_output, & &1.priority) {desc, required_input, optional_input, required_output, optional_output} end def function_name(name), do: to_string(name) |> String.downcase() |> String.to_atom() def type_name(name) do to_string(name) |> Macro.underscore() |> String.to_atom() |> Macro.var(__MODULE__) end def output_typespec(required, optional) do case {required, optional} do {[], []} -> quote do :ok | {:error, term()} end {[pspec], []} -> quote do {:ok, unquote(typespec(pspec))} | {:error, term()} end {pspec_list, optional} -> optional_out = optional_args_typespec(optional) quote do {:ok, {unquote_splicing(typespec(pspec_list)), unquote(optional_out)}} | {:error, term()} end end end def bang_output_typespec(required, optional) do case {required, optional} do {[], []} -> quote do :ok | no_return() end {[pspec], []} -> quote do unquote(typespec(pspec)) | no_return() end {pspec_list, optional} -> optional_out = optional_args_typespec(optional) quote do {unquote_splicing(typespec(pspec_list)), unquote(optional_out)} | no_return() end end end def func_typespec(func_name, required_in, optional_in, required_out, optional_out) do quote do unquote(func_name)( unquote_splicing(typespec(required_in)), unquote(optional_args_typespec(optional_in)) ) :: unquote(output_typespec(required_out, optional_out)) end end def bang_func_typespec(func_name, required_in, optional_in, required_out, optional_out) do quote do unquote(func_name)( unquote_splicing(typespec(required_in)), unquote(optional_args_typespec(optional_in)) ) :: unquote(bang_output_typespec(required_out, optional_out)) end end defp vips_operation_arguments(name) do {description, args} = Nif.nif_vips_operation_get_arguments(name) args = Enum.map(args, fn {name, spec_details, priority, flags} -> {desc, spec_type, value_type, data} = spec_details %GParamSpec{ param_name: List.to_atom(name), desc: desc, spec_type: to_string(spec_type), value_type: to_string(value_type), data: data, priority: priority, flags: flags } end) {description, args} end defp default(pspec) do if Type.default(pspec) == :unsupported do "" else "Default: `#{inspect(Type.default(pspec))}`" end end defp optional_in_doc([]), do: "" defp optional_in_doc(optional_in) do optional_args = Enum.map_join(optional_in, "\n", fn pspec -> "* #{pspec.param_name} - #{pspec.desc}. #{default(pspec)}" end) """ ## Optional #{optional_args} """ end def output_values_doc([], []), do: "" def output_values_doc([_], []), do: "" def output_values_doc(required_out, optional_out) do required_out_values = Enum.map_join(required_out, "\n", fn pspec -> "* #{pspec.param_name} - #{pspec.desc}. (`#{Macro.to_string(typespec(pspec))}`)" end) optional_out_values = Enum.map_join(optional_out, "\n", fn pspec -> "* #{pspec.param_name} - #{pspec.desc}. (`#{Macro.to_string(typespec(pspec))}`)" end) """ ## Returns Ordered values in the returned tuple #{required_out_values} ## Additional Last value of the the output tuple is a keyword list of additional optional output values #{optional_out_values} """ end defp typespec(%GParamSpec{spec_type: "GParamEnum"} = pspec) do type_name(pspec.value_type) end defp typespec(%GParamSpec{spec_type: "GParamFlags"} = pspec) do type_name(pspec.value_type) end defp typespec(pspec_list) when is_list(pspec_list) do Enum.map(pspec_list, &typespec/1) end defp typespec(pspec), do: Type.typespec(pspec) defp optional_args_typespec(optional) do Enum.map(optional, fn pspec -> {pspec.param_name, typespec(pspec)} end) end end defmodule Vix.Vips.Operation do @moduledoc """ Vips Operations NOTE: Vips operation functions are generated using vips-introspection, so the bindings are up-to-date with vips version installed. Documentation in the hexdocs might *not* match for you. """ import Vix.Vips.OperationHelper alias Vix.Nif defmodule Error do defexception [:message] end Vix.Nif.nif_vips_enum_list() |> Enum.map(fn {name, enum} -> {enum_str_list, _} = Enum.unzip(enum) @type unquote(type_name(name)) :: unquote(Enum.reduce(enum_str_list, &{:|, [], [&1, &2]})) end) Vix.Nif.nif_vips_flag_list() |> Enum.map(fn {name, flag} -> {flag_str_list, _} = Enum.unzip(flag) @type unquote(type_name(name)) :: list(unquote(Enum.reduce(flag_str_list, &{:|, [], [&1, &2]}))) end) Nif.nif_vips_operation_list() |> Enum.uniq() |> Enum.reject(&reject_unsupported_operations/1) |> Enum.map(fn name -> {desc, required_in, optional_in, required_out, optional_out} = operation_args(name) func_name = function_name(name) func_params = Enum.map(required_in, &Macro.var(&1.param_name, __MODULE__)) in_pspec = Map.new(required_in ++ optional_in, &{&1.param_name, &1}) required_out_pspec = Map.new(required_out, &{&1.param_name, &1}) optional_out_pspec = Map.new(optional_out, &{&1.param_name, &1}) func_required_in_values = Enum.map(required_in, fn pspec -> {Atom.to_charlist(pspec.param_name), Macro.var(pspec.param_name, __MODULE__)} end) @doc """ #{prepare_doc(desc, required_in, optional_in, required_out, optional_out)} """ @spec unquote( func_typespec( func_name, required_in, optional_in, required_out, optional_out ) ) def unquote(func_name)(unquote_splicing(func_params), optional \\ []) do func_optional_in_values = Enum.map(optional, fn {name, value} -> {Atom.to_charlist(name), value} end) in_args = unquote(func_required_in_values) ++ func_optional_in_values nif_in_args = input_to_nif_terms(in_args, unquote(Macro.escape(in_pspec))) result = Vix.Nif.nif_vips_operation_call( unquote(name), nif_in_args ) case result do {:ok, nif_out_args} -> output_to_erl_terms( nif_out_args, unquote(Macro.escape(required_out_pspec)), unquote(Macro.escape(optional_out_pspec)) ) {:error, term} -> {:error, term} end end bang_func_name = function_name(String.to_atom("#{name}!")) @doc """ Same as `#{func_name}/#{length(func_params) + 1}`, except it returns only the value (not a tuple) and raises on error. """ @spec unquote( bang_func_typespec( bang_func_name, required_in, optional_in, required_out, optional_out ) ) def unquote(bang_func_name)(unquote_splicing(func_params), optional \\ []) do case __MODULE__.unquote(func_name)(unquote_splicing(func_params), optional) do :ok -> :ok {:ok, result} -> result {:error, reason} when is_list(reason) -> raise Error, message: to_string(reason) {:error, reason} -> raise Error, message: inspect(reason) end end end) end