├── test ├── test_helper.exs ├── images │ ├── black.jpg │ ├── boats.tif │ ├── gradient.png │ ├── puppies.jpg │ ├── puppies.raw │ ├── alpha_band.png │ ├── conv_puppies.jpg │ ├── affine_puppies.jpg │ ├── black_on_white.jpg │ ├── gravity_puppies.jpg │ ├── invert_puppies.jpg │ └── black_on_white_hflip.jpg ├── vix │ ├── vips │ │ ├── type_test.exs │ │ ├── interpolate_test.exs │ │ ├── blob_test.exs │ │ ├── ref_string_test.exs │ │ ├── operation │ │ │ └── helper_test.exs │ │ ├── livebook_render_test.exs │ │ ├── array_test.exs │ │ ├── mutable_image_test.exs │ │ ├── operation_test.exs │ │ └── access_test.exs │ ├── g_object │ │ └── string_test.exs │ ├── vips_test.exs │ ├── nif_test.exs │ ├── foreign_test.exs │ └── operator_test.exs └── support │ └── images.ex ├── .formatter.exs ├── lib └── vix │ ├── vips │ ├── operation │ │ └── error.ex │ ├── blob.ex │ ├── source.ex │ ├── target.ex │ ├── ref_string.ex │ ├── enum.ex │ ├── flag.ex │ ├── foreign.ex │ ├── interpolate.ex │ ├── array.ex │ ├── mutable_operation.ex │ ├── mutable_image.ex │ └── operation.ex │ ├── g_object │ ├── boolean.ex │ ├── string.ex │ ├── int.ex │ ├── uint64.ex │ ├── double.ex │ └── g_param_spec.ex │ ├── type.ex │ ├── tensor.ex │ ├── source_pipe.ex │ ├── vips.ex │ └── target_pipe.ex ├── c_src ├── vips_interpolate.h ├── g_object │ ├── g_type.h │ ├── g_param_spec.h │ ├── g_boxed.h │ ├── g_value.h │ ├── g_object.h │ ├── g_type.c │ ├── g_boxed.c │ ├── g_object.c │ └── g_param_spec.c ├── pipe.h ├── vips_interpolate.c ├── vips_foreign.h ├── vips_boxed.h ├── vips_operation.h ├── vips_image.h ├── .clang-format ├── utils.h ├── Makefile ├── utils.c └── vix.c ├── bench ├── from_enum.exs ├── op.exs ├── mix.exs ├── to_stream.exs ├── from_binary.exs ├── stream.exs └── mix.lock ├── flake.nix ├── .gitignore ├── LICENSE ├── Makefile ├── scripts ├── download_toolchains.sh └── mirror_toolchains.sh ├── flake.lock ├── livebooks └── picture-language.livemd ├── .github └── workflows │ ├── precompile.yaml │ └── ci.yaml ├── mix.lock ├── mix.exs ├── README.md ├── DEVELOPMENT.md └── .credo.exs /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :warning) 2 | ExUnit.start() 3 | -------------------------------------------------------------------------------- /test/images/black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/black.jpg -------------------------------------------------------------------------------- /test/images/boats.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/boats.tif -------------------------------------------------------------------------------- /test/images/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/gradient.png -------------------------------------------------------------------------------- /test/images/puppies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/puppies.jpg -------------------------------------------------------------------------------- /test/images/puppies.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/puppies.raw -------------------------------------------------------------------------------- /test/images/alpha_band.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/alpha_band.png -------------------------------------------------------------------------------- /test/images/conv_puppies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/conv_puppies.jpg -------------------------------------------------------------------------------- /test/images/affine_puppies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/affine_puppies.jpg -------------------------------------------------------------------------------- /test/images/black_on_white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/black_on_white.jpg -------------------------------------------------------------------------------- /test/images/gravity_puppies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/gravity_puppies.jpg -------------------------------------------------------------------------------- /test/images/invert_puppies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/invert_puppies.jpg -------------------------------------------------------------------------------- /test/images/black_on_white_hflip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akash-akya/vix/HEAD/test/images/black_on_white_hflip.jpg -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /lib/vix/vips/operation/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.Operation.Error do 2 | @moduledoc false 3 | 4 | defexception [:message] 5 | end 6 | -------------------------------------------------------------------------------- /test/vix/vips/type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.TypeTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Type 5 | 6 | test "typespec" do 7 | assert {:integer, [], []} == Type.typespec("gint") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /c_src/vips_interpolate.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_VIPS_INTERPOLATE_H 2 | #define VIX_VIPS_INTERPOLATE_H 3 | 4 | #include "erl_nif.h" 5 | 6 | ERL_NIF_TERM nif_interpolate_new(ErlNifEnv *env, int argc, 7 | const ERL_NIF_TERM argv[]); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /test/vix/vips/interpolate_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.InterpolateTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Vips.Interpolate 5 | 6 | test "new interpolate" do 7 | assert {:ok, %Interpolate{ref: ref}} = Interpolate.new("nearest") 8 | 9 | {:ok, gtype} = Vix.Nif.nif_g_type_from_instance(ref) 10 | assert Vix.Nif.nif_g_type_name(gtype) == {:ok, "VipsInterpolateNearest"} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/vix/vips/blob_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.BlobTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Vips.Blob 5 | 6 | test "to_nif_term" do 7 | blob = Blob.to_nif_term(<<1, 2, 3, 4>>, nil) 8 | 9 | {:ok, gtype} = Vix.Nif.nif_g_type_from_instance(blob) 10 | assert Vix.Nif.nif_g_type_name(gtype) == {:ok, "VipsBlob"} 11 | end 12 | 13 | test "to_erl_term" do 14 | blob = Blob.to_nif_term(<<1, 2, 3, 4>>, nil) 15 | assert <<1, 2, 3, 4>> == Blob.to_erl_term(blob) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/vix/g_object/string_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.GObject.StringTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "to_nif_term" do 5 | io_list = Vix.GObject.String.to_nif_term("sample", nil) 6 | assert IO.iodata_to_binary(io_list) == "sample\0" 7 | 8 | io_list = Vix.GObject.String.to_nif_term("ಉನಿಕೋಡ್", nil) 9 | assert IO.iodata_to_binary(io_list) == "ಉನಿಕೋಡ್\0" 10 | 11 | assert_raise ArgumentError, "expected UTF-8 binary string", fn -> 12 | Vix.GObject.String.to_nif_term(<<0xFF::16>>, nil) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/vix/vips/blob.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.Blob do 2 | alias Vix.Type 3 | @moduledoc false 4 | 5 | @behaviour Type 6 | @opaque t() :: reference() 7 | 8 | @impl Type 9 | def typespec do 10 | quote do 11 | binary() 12 | end 13 | end 14 | 15 | @impl Type 16 | def default(nil), do: :unsupported 17 | 18 | @impl Type 19 | def to_nif_term(value, _data) do 20 | Vix.Nif.nif_vips_blob(value) 21 | end 22 | 23 | @impl Type 24 | def to_erl_term(value) do 25 | {:ok, bin} = Vix.Nif.nif_vips_blob_to_erl_binary(value) 26 | bin 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /c_src/g_object/g_type.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_G_TYPE_H 2 | #define VIX_G_TYPE_H 3 | 4 | #include "erl_nif.h" 5 | #include 6 | 7 | extern ErlNifResourceType *G_TYPE_RT; 8 | 9 | /* Not really need, since GType is mostly an int */ 10 | typedef struct _GTypeResource { 11 | GType type; 12 | } GTypeResource; 13 | 14 | ERL_NIF_TERM nif_g_type_from_instance(ErlNifEnv *env, int argc, 15 | const ERL_NIF_TERM argv[]); 16 | 17 | ERL_NIF_TERM nif_g_type_name(ErlNifEnv *env, int argc, 18 | const ERL_NIF_TERM argv[]); 19 | 20 | int nif_g_type_init(ErlNifEnv *env); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /lib/vix/g_object/boolean.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.GObject.Boolean do 2 | alias Vix.Type 3 | @moduledoc false 4 | @behaviour Type 5 | 6 | @impl Type 7 | def typespec do 8 | quote do 9 | boolean() 10 | end 11 | end 12 | 13 | @impl Type 14 | def default(default), do: default 15 | 16 | @impl Type 17 | def to_nif_term(value, _data) do 18 | case value do 19 | value when is_boolean(value) -> 20 | value 21 | 22 | value -> 23 | raise ArgumentError, message: "expected boolean. given: #{inspect(value)}" 24 | end 25 | end 26 | 27 | @impl Type 28 | def to_erl_term(value), do: value 29 | end 30 | -------------------------------------------------------------------------------- /test/vix/vips/ref_string_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.RefStringTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Vips.RefString 5 | 6 | test "to_nif_term" do 7 | string = RefString.to_nif_term("vix is awesome!", nil) 8 | 9 | {:ok, gtype} = Vix.Nif.nif_g_type_from_instance(string) 10 | assert Vix.Nif.nif_g_type_name(gtype) == {:ok, "VipsRefString"} 11 | 12 | assert_raise ArgumentError, fn -> 13 | RefString.to_nif_term(<<255>>, nil) 14 | end 15 | end 16 | 17 | test "to_erl_term" do 18 | string = RefString.to_nif_term("vix is awesome!", nil) 19 | assert "vix is awesome!" == RefString.to_erl_term(string) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /c_src/g_object/g_param_spec.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_G_PARAM_SPEC_H 2 | #define VIX_G_PARAM_SPEC_H 3 | 4 | #include "erl_nif.h" 5 | #include 6 | #include 7 | 8 | extern ErlNifResourceType *G_PARAM_SPEC_RT; 9 | 10 | typedef struct _GParamSpecResource { 11 | GParamSpec *pspec; 12 | } GParamSpecResource; 13 | 14 | ERL_NIF_TERM g_param_spec_to_erl_term(ErlNifEnv *env, GParamSpec *pspec); 15 | 16 | bool erl_term_to_g_param_spec(ErlNifEnv *env, ERL_NIF_TERM term, 17 | GParamSpec **pspec); 18 | 19 | ERL_NIF_TERM g_param_spec_details(ErlNifEnv *env, GParamSpec *pspec); 20 | 21 | int nif_g_param_spec_init(ErlNifEnv *env); 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /c_src/pipe.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_PIPE_H 2 | #define VIX_PIPE_H 3 | 4 | #include "erl_nif.h" 5 | 6 | extern ErlNifResourceType *G_OBJECT_RT; 7 | 8 | ERL_NIF_TERM nif_pipe_open(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]); 9 | 10 | ERL_NIF_TERM nif_write(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]); 11 | 12 | ERL_NIF_TERM nif_read(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]); 13 | 14 | ERL_NIF_TERM nif_source_new(ErlNifEnv *env, int argc, 15 | const ERL_NIF_TERM argv[]); 16 | 17 | ERL_NIF_TERM nif_target_new(ErlNifEnv *env, int argc, 18 | const ERL_NIF_TERM argv[]); 19 | 20 | int nif_pipe_init(ErlNifEnv *env); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /test/vix/vips_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.VipsTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Vips 5 | alias Vix.Vips.Image 6 | 7 | import Vix.Support.Images 8 | 9 | test "tracked_get_mem/0" do 10 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 11 | {:ok, _bin} = Image.write_to_buffer(im, ".png") 12 | 13 | usage = Vips.tracked_get_mem() 14 | assert is_integer(usage) && usage > 0 15 | end 16 | 17 | test "tracked_get_mem_highwater/0" do 18 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 19 | {:ok, _bin} = Image.write_to_buffer(im, ".png") 20 | 21 | usage = Vips.tracked_get_mem_highwater() 22 | assert is_integer(usage) && usage > 0 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/vix/nif_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.NifTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Nif 5 | 6 | import Vix.Support.Images 7 | 8 | test "nif_image_new_from_file" do 9 | path = img_path("puppies.jpg") 10 | {:ok, im} = Nif.nif_image_new_from_file(path) 11 | assert Nif.nif_g_object_type_name(im) == "VipsImage" 12 | end 13 | 14 | test "nif_image_write_area_to_binary" do 15 | path = img_path("puppies.jpg") 16 | {:ok, im} = Nif.nif_image_new_from_file(path) 17 | 18 | assert {:ok, {binary, 10 = width, 30 = height, 2 = bands, 0}} = 19 | Nif.nif_image_write_area_to_binary(im, [0, 2, 10, 30, 0, 2]) 20 | 21 | assert IO.iodata_length(binary) == width * height * bands 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/vix/vips/operation/helper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.Operation.HelperTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Vips.Image 5 | alias Vix.Vips.Operation.Helper 6 | 7 | import Vix.Support.Images 8 | 9 | test "operation_call" do 10 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 11 | 12 | assert {:ok, out} = 13 | Helper.operation_call( 14 | "gravity", 15 | [im, :VIPS_COMPASS_DIRECTION_CENTRE, 650, 500], 16 | extend: :VIPS_EXTEND_COPY 17 | ) 18 | 19 | out_path = Briefly.create!(extname: ".jpg") 20 | :ok = Image.write_to_file(out, out_path) 21 | 22 | assert_files_equal(img_path("gravity_puppies.jpg"), out_path) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /c_src/g_object/g_boxed.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_G_BOXED_H 2 | #define VIX_G_BOXED_H 3 | 4 | #include "erl_nif.h" 5 | #include 6 | #include 7 | 8 | extern ErlNifResourceType *G_BOXED_RT; 9 | 10 | typedef struct _GBoxedResource { 11 | GType boxed_type; 12 | gpointer boxed_ptr; 13 | } GBoxedResource; 14 | 15 | bool erl_term_to_g_boxed(ErlNifEnv *env, ERL_NIF_TERM term, gpointer *ptr); 16 | 17 | bool erl_term_boxed_type(ErlNifEnv *env, ERL_NIF_TERM term, GType *type); 18 | 19 | ERL_NIF_TERM nif_g_boxed_unref(ErlNifEnv *env, int argc, 20 | const ERL_NIF_TERM argv[]); 21 | 22 | ERL_NIF_TERM boxed_to_erl_term(ErlNifEnv *env, gpointer ptr, GType type); 23 | 24 | int nif_g_boxed_init(ErlNifEnv *env); 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /c_src/g_object/g_value.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_G_VALUE_H 2 | #define VIX_G_VALUE_H 3 | 4 | #include "../utils.h" 5 | #include "erl_nif.h" 6 | 7 | #include 8 | 9 | VixResult set_g_value_from_erl_term(ErlNifEnv *env, GParamSpec *pspec, 10 | ERL_NIF_TERM term, GValue *gvalue); 11 | 12 | VixResult get_erl_term_from_g_object_property(ErlNifEnv *env, GObject *obj, 13 | const char *name, 14 | GParamSpec *pspec); 15 | 16 | VixResult g_value_to_erl_term(ErlNifEnv *env, GValue gvalue); 17 | 18 | VixResult erl_term_to_g_value(ErlNifEnv *env, GType type, ERL_NIF_TERM term, 19 | GValue *gvalue); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /lib/vix/vips/source.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.Source do 2 | @moduledoc false 3 | 4 | alias Vix.Type 5 | alias __MODULE__ 6 | 7 | @behaviour Type 8 | 9 | @type t() :: %Source{ref: reference} 10 | 11 | defstruct [:ref] 12 | 13 | @impl Type 14 | def typespec do 15 | quote do 16 | unquote(__MODULE__).t() 17 | end 18 | end 19 | 20 | @impl Type 21 | def default(nil), do: :unsupported 22 | 23 | @impl Type 24 | def to_nif_term(source, _data) do 25 | case source do 26 | %Source{ref: ref} -> 27 | ref 28 | 29 | value -> 30 | raise ArgumentError, message: "expected Vix.Vips.Source given: #{inspect(value)}" 31 | end 32 | end 33 | 34 | @impl Type 35 | def to_erl_term(ref), do: %Source{ref: ref} 36 | end 37 | -------------------------------------------------------------------------------- /lib/vix/vips/target.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.Target do 2 | @moduledoc false 3 | 4 | alias Vix.Type 5 | alias __MODULE__ 6 | 7 | @behaviour Type 8 | 9 | @type t() :: %Target{ref: reference()} 10 | 11 | defstruct [:ref] 12 | 13 | @impl Type 14 | def typespec do 15 | quote do 16 | unquote(__MODULE__).t() 17 | end 18 | end 19 | 20 | @impl Type 21 | def default(nil), do: :unsupported 22 | 23 | @impl Type 24 | def to_nif_term(target, _data) do 25 | case target do 26 | %Target{ref: ref} -> 27 | ref 28 | 29 | value -> 30 | raise ArgumentError, message: "expected Vix.Vips.Source given: #{inspect(value)}" 31 | end 32 | end 33 | 34 | @impl Type 35 | def to_erl_term(ref), do: %Target{ref: ref} 36 | end 37 | -------------------------------------------------------------------------------- /bench/from_enum.exs: -------------------------------------------------------------------------------- 1 | alias Vix.Vips.Image 2 | require Logger 3 | 4 | img = Path.join(__DIR__, "../test/images/puppies.jpg") 5 | name = Path.basename(__ENV__.file, ".exs") 6 | 7 | Benchee.run( 8 | %{ 9 | "from_file" => fn -> 10 | {:ok, image} = Image.new_from_file(img) 11 | :ok = Image.write_to_file(image, "file.png") 12 | end, 13 | "from_enum" => fn -> 14 | {:ok, image} = 15 | File.stream!(img, [], 1024 * 10) 16 | |> Image.new_from_enum() 17 | 18 | :ok = Image.write_to_file(image, "enum.png") 19 | end 20 | }, 21 | parallel: 4, 22 | warmup: 2, 23 | time: 20, 24 | formatters: [ 25 | {Benchee.Formatters.HTML, file: Path.expand("output/#{name}.html", __DIR__)}, 26 | Benchee.Formatters.Console 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /lib/vix/g_object/string.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.GObject.String do 2 | alias Vix.Type 3 | @moduledoc false 4 | @behaviour Type 5 | 6 | @impl Type 7 | def typespec do 8 | quote do 9 | String.t() 10 | end 11 | end 12 | 13 | @impl Type 14 | def default(default), do: default 15 | 16 | @impl Type 17 | def to_nif_term(str, _data) do 18 | if String.valid?(str) do 19 | [str, <<"\0">>] 20 | else 21 | raise ArgumentError, message: "expected UTF-8 binary string" 22 | end 23 | end 24 | 25 | @impl Type 26 | def to_erl_term(value) do 27 | if String.valid?(value) do 28 | value 29 | else 30 | # TODO: remove after debugging 31 | raise ArgumentError, "value from NIF is not a valid UTF-8 string" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /c_src/g_object/g_object.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_G_OBJECT_H 2 | #define VIX_G_OBJECT_H 3 | 4 | #include "erl_nif.h" 5 | #include 6 | #include 7 | 8 | extern ErlNifResourceType *G_OBJECT_RT; 9 | 10 | typedef struct _GObjectResource { 11 | GObject *obj; 12 | } GObjectResource; 13 | 14 | ERL_NIF_TERM g_object_to_erl_term(ErlNifEnv *env, GObject *obj); 15 | 16 | ERL_NIF_TERM nif_g_object_type_name(ErlNifEnv *env, int argc, 17 | const ERL_NIF_TERM argv[]); 18 | 19 | ERL_NIF_TERM nif_g_object_unref(ErlNifEnv *env, int argc, 20 | const ERL_NIF_TERM argv[]); 21 | 22 | bool erl_term_to_g_object(ErlNifEnv *env, ERL_NIF_TERM term, GObject **obj); 23 | 24 | int nif_g_object_init(ErlNifEnv *env); 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /bench/op.exs: -------------------------------------------------------------------------------- 1 | alias Vix.Vips.Operation 2 | alias Vix.Vips.Image 3 | require Logger 4 | 5 | img = Path.join(__DIR__, "../test/images/puppies.jpg") 6 | name = Path.basename(__ENV__.file, ".exs") 7 | 8 | Benchee.run( 9 | %{ 10 | "Vix" => fn -> 11 | {:ok, img} = Image.new_from_file(img) 12 | 13 | :ok = 14 | Operation.resize!(img, 2) 15 | |> Image.write_to_file("vix.png") 16 | end, 17 | "Mogrify" => fn -> 18 | Mogrify.open(img) 19 | |> Mogrify.resize("200%") 20 | |> Mogrify.format("png") 21 | |> Mogrify.save(path: "mogrify.png") 22 | end 23 | }, 24 | parallel: 4, 25 | warmup: 5, 26 | time: 30, 27 | formatters: [ 28 | {Benchee.Formatters.HTML, file: Path.expand("output/#{name}.html", __DIR__)}, 29 | Benchee.Formatters.Console 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /bench/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule VixBench.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :vix_bench, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | aliases: aliases() 12 | ] 13 | end 14 | 15 | defp aliases() do 16 | [ 17 | "bench.op": ["run op.exs"], 18 | "bench.stream": ["run stream.exs"], 19 | "bench.from_enum": ["run from_enum.exs"], 20 | "bench.to_stream": ["run to_stream.exs"], 21 | "bench.from_binary": ["run from_binary.exs"] 22 | ] 23 | end 24 | 25 | defp deps do 26 | [ 27 | {:benchee, "~> 1.0"}, 28 | {:benchee_html, "~> 1.0"}, 29 | {:vix, "~> 0.1", path: "../", override: true}, 30 | {:mogrify, "~> 0.8"} 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /bench/to_stream.exs: -------------------------------------------------------------------------------- 1 | alias Vix.Vips.Image 2 | require Logger 3 | 4 | img = Path.join(__DIR__, "../test/images/puppies.jpg") 5 | name = Path.basename(__ENV__.file, ".exs") 6 | 7 | Benchee.run( 8 | %{ 9 | "write_to_file" => fn -> 10 | {:ok, image} = Image.new_from_file(img) 11 | :ok = Image.write_to_file(image, "file.png") 12 | end, 13 | "write_to_stream" => fn -> 14 | {:ok, image} = Image.new_from_file(img) 15 | 16 | :ok = 17 | image 18 | |> Image.write_to_stream(".png") 19 | |> Stream.into(File.stream!("stream.png")) 20 | |> Stream.run() 21 | end 22 | }, 23 | parallel: 4, 24 | warmup: 2, 25 | time: 20, 26 | formatters: [ 27 | {Benchee.Formatters.HTML, file: Path.expand("output/#{name}.html", __DIR__)}, 28 | Benchee.Formatters.Console 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /test/vix/vips/livebook_render_test.exs: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Kino.Render) do 2 | defmodule Vix.Vips.LivebookRenderTest do 3 | use ExUnit.Case, async: true 4 | 5 | alias Vix.Vips.Image 6 | 7 | import Vix.Support.Images 8 | 9 | test "Rendering livebook metadata and image" do 10 | Application.ensure_all_started(:kino) 11 | 12 | assert {:ok, %Image{ref: _ref} = image} = Image.new_from_file(img_path("puppies.jpg")) 13 | 14 | assert %{ 15 | type: :grid, 16 | boxed: false, 17 | columns: 1, 18 | gap: 8, 19 | outputs: [ 20 | %{content: _, mime_type: "image/png", type: :image}, 21 | %{export: false, type: :js} 22 | ] 23 | } = Kino.Render.to_livebook(image) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /bench/from_binary.exs: -------------------------------------------------------------------------------- 1 | alias Vix.Vips.Image 2 | require Logger 3 | 4 | jpg_img_path = Path.join(__DIR__, "../test/images/puppies.jpg") 5 | raw_img_path = Path.join(__DIR__, "../test/images/puppies.raw") 6 | name = Path.basename(__ENV__.file, ".exs") 7 | 8 | {:ok, im} = Image.new_from_file(jpg_img_path) 9 | bin = File.read!(raw_img_path) 10 | 11 | Benchee.run( 12 | %{ 13 | "from_binary" => fn -> 14 | {:ok, image} = 15 | Image.new_from_binary( 16 | bin, 17 | Image.width(im), 18 | Image.height(im), 19 | Image.bands(im), 20 | Image.format(im) 21 | ) 22 | 23 | :ok = Image.write_to_file(image, "from_binary.png") 24 | end 25 | }, 26 | parallel: 4, 27 | warmup: 2, 28 | time: 20, 29 | formatters: [ 30 | {Benchee.Formatters.HTML, file: Path.expand("output/#{name}.html", __DIR__)}, 31 | Benchee.Formatters.Console 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /bench/stream.exs: -------------------------------------------------------------------------------- 1 | alias Vix.Vips.Image 2 | require Logger 3 | 4 | img = Path.join(__DIR__, "../test/images/puppies.jpg") 5 | name = Path.basename(__ENV__.file, ".exs") 6 | 7 | Benchee.run( 8 | %{ 9 | "from enum" => fn -> 10 | {:ok, image} = 11 | File.stream!(img, [], 1024 * 10) 12 | |> Image.new_from_enum() 13 | 14 | :ok = 15 | image 16 | |> Image.write_to_file("from_enum.png") 17 | end, 18 | "to enum" => fn -> 19 | {:ok, image} = Image.new_from_file(img) 20 | 21 | :ok = 22 | image 23 | |> Image.write_to_stream(".png") 24 | |> Stream.into(File.stream!("to_enum.png")) 25 | |> Stream.run() 26 | end 27 | }, 28 | parallel: 4, 29 | warmup: 2, 30 | time: 20, 31 | formatters: [ 32 | {Benchee.Formatters.HTML, file: Path.expand("output/#{name}.html", __DIR__)}, 33 | Benchee.Formatters.Console 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /lib/vix/vips/ref_string.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.RefString do 2 | alias Vix.Type 3 | @moduledoc false 4 | 5 | @behaviour Type 6 | @opaque t() :: reference() 7 | 8 | @impl Type 9 | def typespec do 10 | quote do 11 | binary() 12 | end 13 | end 14 | 15 | @impl Type 16 | def default(nil), do: :unsupported 17 | 18 | @impl Type 19 | def to_nif_term(str, _data) do 20 | if String.valid?(str) do 21 | Vix.Nif.nif_vips_ref_string([str, <<"\0">>]) 22 | else 23 | raise ArgumentError, message: "expected UTF-8 binary string" 24 | end 25 | end 26 | 27 | @impl Type 28 | def to_erl_term(value) do 29 | {:ok, value} = Vix.Nif.nif_vips_ref_string_to_erl_binary(value) 30 | 31 | if String.valid?(value) do 32 | value 33 | else 34 | # TODO: remove after debugging 35 | raise ArgumentError, "value from NIF is not a valid UTF-8 string" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Elixir Development Environment"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | inherit (nixpkgs.lib) optional; 13 | pkgs = import nixpkgs { inherit system; }; 14 | 15 | sdk = with pkgs; 16 | lib.optionals stdenv.isDarwin 17 | (with darwin.apple_sdk.frameworks; [ 18 | # needed for compilation 19 | pkgs.libiconv 20 | AppKit 21 | Foundation 22 | CoreFoundation 23 | CoreServices 24 | ]); 25 | 26 | in { 27 | devShell = pkgs.mkShell { 28 | buildInputs = 29 | [ pkgs.elixir sdk ]; 30 | }; 31 | }); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /test/support/images.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Support.Images do 2 | @moduledoc false 3 | 4 | import ExUnit.Assertions 5 | alias Vix.Vips.Image 6 | 7 | @images_path Path.join(__DIR__, "../images") 8 | 9 | def assert_files_equal(expected, result) do 10 | assert File.read!(expected) == File.read!(result) 11 | end 12 | 13 | def img_path(name) do 14 | Path.join(@images_path, name) 15 | end 16 | 17 | def assert_images_equal(a, b) do 18 | case Vix.Vips.Operation.relational(a, b, :VIPS_OPERATION_RELATIONAL_EQUAL) do 19 | {:ok, img} -> 20 | {min, _additional_output} = Vix.Vips.Operation.min!(img, size: 1) 21 | assert min == 255.0, "Images are not equal" 22 | 23 | {:error, reason} -> 24 | flunk("Failed to compare images, error: #{inspect(reason)}") 25 | end 26 | end 27 | 28 | def shape(image) do 29 | {Image.width(image), Image.height(image), Image.bands(image)} 30 | end 31 | 32 | def range_has_step do 33 | Map.has_key?(1..2, :step) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | eips-*.tar 24 | 25 | /.ccls-cache 26 | 27 | /*.jpg 28 | 29 | /*.png 30 | 31 | .dir-locals.el 32 | 33 | # Ignore compilation files 34 | 35 | /priv 36 | /obj 37 | 38 | *.o 39 | 40 | # Bench 41 | /bench/_build/ 42 | /bench/deps/ 43 | /bench/output/ 44 | /bench/out_*.png 45 | 46 | # Ignore pre-compiler checksum file 47 | checksum.exs 48 | 49 | # C Dependency Files 50 | *.d 51 | 52 | /cache -------------------------------------------------------------------------------- /lib/vix/g_object/int.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.GObject.Int do 2 | alias Vix.Type 3 | @moduledoc false 4 | @behaviour Type 5 | 6 | @impl Type 7 | def typespec do 8 | quote do 9 | integer() 10 | end 11 | end 12 | 13 | @impl Type 14 | def default({_min, _max, default}), do: default 15 | 16 | @impl Type 17 | def to_nif_term(value, data) do 18 | case value do 19 | value when is_integer(value) -> 20 | validate_number_limits!(value, data) 21 | value 22 | 23 | value -> 24 | raise ArgumentError, message: "expected integer. given: #{inspect(value)}" 25 | end 26 | end 27 | 28 | @impl Type 29 | def to_erl_term(value), do: value 30 | 31 | defp validate_number_limits!(_value, nil), do: :ok 32 | 33 | defp validate_number_limits!(value, {min, max, _default}) do 34 | if max && value > max do 35 | raise ArgumentError, "value must be <= #{max}" 36 | end 37 | 38 | if min && value < min do 39 | raise ArgumentError, "value must be >= #{min}" 40 | end 41 | 42 | :ok 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/vix/g_object/uint64.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.GObject.UInt64 do 2 | alias Vix.Type 3 | @moduledoc false 4 | @behaviour Type 5 | 6 | @impl Type 7 | def typespec do 8 | quote do 9 | non_neg_integer() 10 | end 11 | end 12 | 13 | @impl Type 14 | def default({_min, _max, default}), do: default 15 | 16 | @impl Type 17 | def to_nif_term(value, data) do 18 | case value do 19 | value when is_integer(value) and value >= 0 -> 20 | validate_number_limits!(value, data) 21 | value 22 | 23 | value -> 24 | raise ArgumentError, message: "expected unsigned integer. given: #{inspect(value)}" 25 | end 26 | end 27 | 28 | @impl Type 29 | def to_erl_term(value), do: value 30 | 31 | defp validate_number_limits!(_value, nil), do: :ok 32 | 33 | defp validate_number_limits!(value, {min, max, _default}) do 34 | if max && value > max do 35 | raise ArgumentError, "value must be <= #{max}" 36 | end 37 | 38 | if min && value < min do 39 | raise ArgumentError, "value must be >= #{min}" 40 | end 41 | 42 | :ok 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Akash Hiremath 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /c_src/vips_interpolate.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "g_object/g_object.h" 5 | #include "utils.h" 6 | #include "vips_interpolate.h" 7 | 8 | ERL_NIF_TERM nif_interpolate_new(ErlNifEnv *env, int argc, 9 | const ERL_NIF_TERM argv[]) { 10 | ASSERT_ARGC(argc, 1); 11 | 12 | ERL_NIF_TERM ret; 13 | ErlNifTime start; 14 | char name[1024] = {0}; 15 | VipsInterpolate *interpolate = NULL; 16 | 17 | start = enif_monotonic_time(ERL_NIF_USEC); 18 | 19 | if (!get_binary(env, argv[0], name, 1024)) { 20 | ret = raise_badarg(env, "interpolate name must be a valid string"); 21 | goto exit; 22 | } 23 | 24 | interpolate = vips_interpolate_new(name); 25 | 26 | if (!interpolate) { 27 | error("Failed to get interpolate for %s. error: %s", name, 28 | vips_error_buffer()); 29 | vips_error_clear(); 30 | ret = make_error(env, "Failed to create VipsInterpolate for given name"); 31 | goto exit; 32 | } 33 | 34 | ret = make_ok(env, g_object_to_erl_term(env, (GObject *)interpolate)); 35 | 36 | exit: 37 | notify_consumed_timeslice(env, start, enif_monotonic_time(ERL_NIF_USEC)); 38 | return ret; 39 | } 40 | -------------------------------------------------------------------------------- /lib/vix/g_object/double.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.GObject.Double do 2 | alias Vix.Type 3 | @moduledoc false 4 | @behaviour Type 5 | 6 | @impl Type 7 | def typespec do 8 | quote do 9 | float() 10 | end 11 | end 12 | 13 | @impl Type 14 | def default({_min, _max, default}), do: default 15 | 16 | @impl Type 17 | def to_nif_term(value, data) do 18 | case value do 19 | value when is_number(value) -> 20 | value = normalize(value) 21 | validate_number_limits!(value, data) 22 | value 23 | 24 | value -> 25 | raise ArgumentError, message: "expected integer or double. given: #{inspect(value)}" 26 | end 27 | end 28 | 29 | @impl Type 30 | def to_erl_term(value), do: value 31 | 32 | def normalize(num) when is_float(num), do: num 33 | def normalize(num) when is_integer(num), do: num * 1.0 34 | 35 | defp validate_number_limits!(_value, nil), do: :ok 36 | 37 | defp validate_number_limits!(value, {min, max, _default}) do 38 | if max && value > max do 39 | raise ArgumentError, "value must be <= #{max}" 40 | end 41 | 42 | if min && value < min do 43 | raise ArgumentError, "value must be >= #{min}" 44 | end 45 | 46 | :ok 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /c_src/vips_foreign.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_VIPS_FOREIGN_H 2 | #define VIX_VIPS_FOREIGN_H 3 | 4 | #include "erl_nif.h" 5 | 6 | ERL_NIF_TERM nif_foreign_find_load_buffer(ErlNifEnv *env, int argc, 7 | const ERL_NIF_TERM argv[]); 8 | 9 | ERL_NIF_TERM nif_foreign_find_save_buffer(ErlNifEnv *env, int argc, 10 | const ERL_NIF_TERM argv[]); 11 | 12 | ERL_NIF_TERM nif_foreign_find_load(ErlNifEnv *env, int argc, 13 | const ERL_NIF_TERM argv[]); 14 | 15 | ERL_NIF_TERM nif_foreign_find_save(ErlNifEnv *env, int argc, 16 | const ERL_NIF_TERM argv[]); 17 | 18 | ERL_NIF_TERM nif_foreign_find_load_source(ErlNifEnv *env, int argc, 19 | const ERL_NIF_TERM argv[]); 20 | 21 | ERL_NIF_TERM nif_foreign_find_save_target(ErlNifEnv *env, int argc, 22 | const ERL_NIF_TERM argv[]); 23 | 24 | ERL_NIF_TERM nif_foreign_get_suffixes(ErlNifEnv *env, int argc, 25 | const ERL_NIF_TERM argv[]); 26 | 27 | ERL_NIF_TERM nif_foreign_get_loader_suffixes(ErlNifEnv *env, int argc, 28 | const ERL_NIF_TERM argv[]); 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Silence directory change messages 2 | MAKEFLAGS += --no-print-directory 3 | 4 | # Default target 5 | all: compile 6 | 7 | # Main compilation target 8 | compile: 9 | @$(MAKE) -C c_src all 10 | 11 | # Mix compilation (called by elixir_make) 12 | calling_from_make: 13 | mix compile 14 | 15 | # Clean targets 16 | clean: 17 | @$(MAKE) -C c_src clean 18 | 19 | clean_precompiled_libvips: 20 | @$(MAKE) -C c_src clean_precompiled_libvips 21 | 22 | deep_clean: clean_precompiled_libvips 23 | 24 | # Development targets 25 | test: 26 | mix test 27 | 28 | format: 29 | mix format 30 | 31 | lint: 32 | mix credo 33 | 34 | dialyz: 35 | mix dialyxir 36 | 37 | # Help target 38 | help: 39 | @echo "Available targets:" 40 | @echo " all/compile - Build the project" 41 | @echo " clean - Clean build artifacts" 42 | @echo " clean_precompiled_libvips - Clean precompiled libvips" 43 | @echo " deep_clean - Full clean including precompiled libs" 44 | @echo " test - Run tests" 45 | @echo " format - Format Elixir code" 46 | @echo " lint - Run Credo linter" 47 | @echo " dialyz - Run Dialyzer type checking" 48 | @echo " help - Show this help" 49 | 50 | .PHONY: all compile clean clean_precompiled_libvips deep_clean calling_from_make test format lint dialyz help 51 | -------------------------------------------------------------------------------- /scripts/download_toolchains.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Download musl toolchains with mirror fallback 5 | TOOLCHAIN_VERSION="11.2.1" 6 | MIRROR_URL="https://github.com/akash-akya/vix/releases/download/toolchains-v${TOOLCHAIN_VERSION}" 7 | FALLBACK_URL="https://more.musl.cc/${TOOLCHAIN_VERSION}/x86_64-linux-musl" 8 | 9 | download_and_extract() { 10 | local filename=$1 11 | local mirror_url="${MIRROR_URL}/${filename}" 12 | local fallback_url="${FALLBACK_URL}/${filename}" 13 | 14 | echo "Downloading $filename..." 15 | 16 | # Try mirror first 17 | if curl -fL --connect-timeout 30 --max-time 300 "$mirror_url" | tar -xz; then 18 | echo "✓ Downloaded $filename from mirror" 19 | return 0 20 | fi 21 | 22 | echo "Mirror failed, trying fallback..." 23 | 24 | # Fallback to original source with retry 25 | if curl -s --retry 3 --connect-timeout 30 --max-time 300 "$fallback_url" | tar -xz; then 26 | echo "✓ Downloaded $filename from fallback" 27 | return 0 28 | fi 29 | 30 | echo "✗ Failed to download $filename from both mirror and fallback" 31 | return 1 32 | } 33 | 34 | echo "Setting up musl cross-compilation toolchains..." 35 | 36 | # Download required toolchains 37 | download_and_extract "x86_64-linux-musl-cross.tgz" 38 | download_and_extract "aarch64-linux-musl-cross.tgz" 39 | 40 | echo "✓ All toolchains downloaded successfully" 41 | -------------------------------------------------------------------------------- /test/vix/foreign_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.ForeignTest do 2 | use ExUnit.Case, async: true 3 | alias Vix.Vips.Foreign 4 | import Vix.Support.Images 5 | 6 | test "find_load_buffer" do 7 | path = img_path("puppies.jpg") 8 | assert {:ok, "VipsForeignLoadJpegBuffer"} = Foreign.find_load_buffer(File.read!(path)) 9 | end 10 | 11 | test "find_save_buffer" do 12 | assert {:ok, "VipsForeignSaveJpegBuffer"} = Foreign.find_save_buffer("puppies.jpg") 13 | end 14 | 15 | test "find_load" do 16 | path = img_path("puppies.jpg") 17 | assert {:ok, "VipsForeignLoadJpegFile"} = Foreign.find_load(path) 18 | end 19 | 20 | test "find_save" do 21 | assert {:ok, "VipsForeignSaveJpegFile"} = Foreign.find_save("puppies.jpg") 22 | end 23 | 24 | test "find_load_source" do 25 | bin = File.read!(img_path("puppies.jpg")) 26 | 27 | assert {pipe, source} = Vix.SourcePipe.new() 28 | assert :ok = Vix.SourcePipe.write(pipe, bin) 29 | assert :ok = Vix.SourcePipe.stop(pipe) 30 | 31 | assert {:ok, "VipsForeignLoadJpegSource"} = Foreign.find_load_source(source) 32 | end 33 | 34 | test "find_save_target" do 35 | assert {:ok, "VipsForeignSaveJpegTarget"} = Foreign.find_save_target(".jpg") 36 | assert {:error, "Failed to find saver for the target"} = Foreign.find_save_target(".pdf") 37 | assert {:error, "Failed to find saver for the target"} = Foreign.find_save_target(".tiff") 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /c_src/vips_boxed.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_VIPS_BOXED_H 2 | #define VIX_VIPS_BOXED_H 3 | 4 | #include "erl_nif.h" 5 | 6 | ERL_NIF_TERM nif_int_array(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]); 7 | 8 | ERL_NIF_TERM nif_double_array(ErlNifEnv *env, int argc, 9 | const ERL_NIF_TERM argv[]); 10 | 11 | ERL_NIF_TERM nif_image_array(ErlNifEnv *env, int argc, 12 | const ERL_NIF_TERM argv[]); 13 | 14 | ERL_NIF_TERM nif_vips_blob(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]); 15 | 16 | ERL_NIF_TERM nif_vips_ref_string(ErlNifEnv *env, int argc, 17 | const ERL_NIF_TERM argv[]); 18 | 19 | ERL_NIF_TERM nif_vips_int_array_to_erl_list(ErlNifEnv *env, int argc, 20 | const ERL_NIF_TERM argv[]); 21 | 22 | ERL_NIF_TERM nif_vips_double_array_to_erl_list(ErlNifEnv *env, int argc, 23 | const ERL_NIF_TERM argv[]); 24 | 25 | ERL_NIF_TERM nif_vips_image_array_to_erl_list(ErlNifEnv *env, int argc, 26 | const ERL_NIF_TERM argv[]); 27 | 28 | ERL_NIF_TERM nif_vips_blob_to_erl_binary(ErlNifEnv *env, int argc, 29 | const ERL_NIF_TERM argv[]); 30 | 31 | ERL_NIF_TERM nif_vips_ref_string_to_erl_binary(ErlNifEnv *env, int argc, 32 | const ERL_NIF_TERM argv[]); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /lib/vix/vips/enum.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.EnumHelper do 2 | @moduledoc false 3 | 4 | def __before_compile__(env) do 5 | for {name, enum} <- Vix.Nif.nif_vips_enum_list() do 6 | def_vips_enum(name, enum, env) 7 | end 8 | 9 | quote do 10 | end 11 | end 12 | 13 | def def_vips_enum(name, enum, env) do 14 | module_name = Module.concat(Vix.Vips.Enum, name) 15 | {enum_str_list, _} = Enum.unzip(enum) 16 | 17 | spec = Enum.reduce(enum_str_list, &{:|, [], [&1, &2]}) 18 | 19 | contents = 20 | quote do 21 | # Internal module 22 | @moduledoc false 23 | @type t() :: unquote(spec) 24 | 25 | alias Vix.Type 26 | 27 | @behaviour Type 28 | 29 | @impl Type 30 | def typespec do 31 | quote do 32 | unquote(__MODULE__).t() 33 | end 34 | end 35 | 36 | @impl Type 37 | def default(default), do: default 38 | 39 | unquote( 40 | Enum.map(enum, fn {name, value} -> 41 | quote do 42 | @impl Type 43 | def to_nif_term(unquote(name), _data), do: unquote(value) 44 | 45 | @impl Type 46 | def to_erl_term(unquote(value)), do: unquote(name) 47 | end 48 | end) 49 | ) 50 | end 51 | 52 | Module.create(module_name, contents, line: env.line, file: env.file) 53 | end 54 | end 55 | 56 | defmodule Vix.Vips.Enum do 57 | @moduledoc false 58 | @before_compile Vix.Vips.EnumHelper 59 | end 60 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1718835956, 24 | "narHash": "sha256-wM9v2yIxClRYsGHut5vHICZTK7xdrUGfrLkXvSuv6s4=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "dd457de7e08c6d06789b1f5b88fc9327f4d96309", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-24.05", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /lib/vix/g_object/g_param_spec.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.GObject.GParamSpec do 2 | @moduledoc false 3 | @type t :: %{} 4 | 5 | defstruct [:param_name, :desc, :spec_type, :value_type, :data, :priority, :flags, :type] 6 | 7 | alias __MODULE__ 8 | 9 | def new(opt) do 10 | pspec = %GParamSpec{ 11 | param_name: opt.name, 12 | desc: opt.desc, 13 | spec_type: to_string(opt.spec_type), 14 | value_type: to_string(opt.value_type), 15 | data: opt.data, 16 | priority: opt.priority, 17 | flags: opt.flags, 18 | type: nil 19 | } 20 | 21 | %GParamSpec{pspec | type: type(pspec)} 22 | end 23 | 24 | def type(%GParamSpec{spec_type: "GParamEnum", value_type: value_type}) do 25 | {:enum, value_type} 26 | end 27 | 28 | def type(%GParamSpec{spec_type: "GParamFlags", value_type: value_type}) do 29 | {:flags, value_type} 30 | end 31 | 32 | # for array of enum, libvips does not pass required information to 33 | # properly expose it to elixir world with correct spec and 34 | # validation. libvips marks array of enum as array of int. 35 | # 36 | # To address this we try to recognize common type of enums 37 | # explicitly and handle casting in elixir side. 38 | def type(%GParamSpec{param_name: "mode", desc: "Array of VipsBlendMode " <> _}) do 39 | {:vips_array, "Enum.VipsBlendMode"} 40 | end 41 | 42 | def type(%GParamSpec{value_type: "VipsArray" <> nested_type}) do 43 | {:vips_array, nested_type} 44 | end 45 | 46 | def type(%GParamSpec{value_type: "VipsImage", flags: flags}) do 47 | if :vips_argument_modify in flags do 48 | "MutableVipsImage" 49 | else 50 | "VipsImage" 51 | end 52 | end 53 | 54 | def type(%GParamSpec{value_type: value_type}) do 55 | value_type 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/vix/vips/array_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.ArrayTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Vips.Array 5 | 6 | describe "Int" do 7 | test "to_nif_term" do 8 | obj = Array.Int.to_nif_term([1, 2, 3, 4], nil) 9 | 10 | {:ok, gtype} = Vix.Nif.nif_g_type_from_instance(obj) 11 | assert Vix.Nif.nif_g_type_name(gtype) == {:ok, "VipsArrayInt"} 12 | end 13 | 14 | test "to_erl_term" do 15 | obj = Array.Int.to_nif_term([1, 2, 3, 4], nil) 16 | assert [1, 2, 3, 4] == Array.Int.to_erl_term(obj) 17 | end 18 | end 19 | 20 | describe "Double" do 21 | test "to_nif_term" do 22 | obj = Array.Double.to_nif_term([1, 2, 3, 4.1], nil) 23 | 24 | {:ok, gtype} = Vix.Nif.nif_g_type_from_instance(obj) 25 | assert Vix.Nif.nif_g_type_name(gtype) == {:ok, "VipsArrayDouble"} 26 | end 27 | 28 | test "to_erl_term" do 29 | obj = Array.Double.to_nif_term([1, 2.46, 0.2, 400.00001], nil) 30 | # values are casted to double 31 | assert [1.0, 2.46, 0.2, 400.00001] == Array.Double.to_erl_term(obj) 32 | end 33 | end 34 | 35 | describe "Enum.VipsInterpretation" do 36 | test "to_nif_term" do 37 | obj = 38 | Array.Enum.VipsBlendMode.to_nif_term( 39 | [:VIPS_BLEND_MODE_IN, :VIPS_BLEND_MODE_DEST, :VIPS_BLEND_MODE_MULTIPLY], 40 | nil 41 | ) 42 | 43 | {:ok, gtype} = Vix.Nif.nif_g_type_from_instance(obj) 44 | assert Vix.Nif.nif_g_type_name(gtype) == {:ok, "VipsArrayInt"} 45 | end 46 | 47 | test "to_erl_term" do 48 | obj = Array.Int.to_nif_term([1, 2, 3], nil) 49 | 50 | assert [:VIPS_BLEND_MODE_SOURCE, :VIPS_BLEND_MODE_OVER, :VIPS_BLEND_MODE_IN] == 51 | Array.Enum.VipsBlendMode.to_erl_term(obj) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /scripts/mirror_toolchains.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Mirror script for Vix musl toolchains 5 | TOOLCHAIN_VERSION="11.2.1" 6 | BASE_URL="https://more.musl.cc/${TOOLCHAIN_VERSION}/x86_64-linux-musl" 7 | MIRROR_DIR="toolchains" 8 | 9 | # Required toolchains 10 | TOOLCHAINS=( 11 | "x86_64-linux-musl-cross.tgz" 12 | "aarch64-linux-musl-cross.tgz" 13 | ) 14 | 15 | echo "Creating toolchain mirror directory..." 16 | mkdir -p "$MIRROR_DIR" 17 | 18 | echo "Downloading musl toolchains..." 19 | 20 | for toolchain in "${TOOLCHAINS[@]}"; do 21 | echo "Downloading $toolchain..." 22 | url="${BASE_URL}/${toolchain}" 23 | output="${MIRROR_DIR}/${toolchain}" 24 | 25 | # Download with retry logic 26 | for attempt in {1..3}; do 27 | if curl -fL --connect-timeout 30 --max-time 600 "$url" -o "$output"; then 28 | echo "✓ Downloaded $toolchain successfully" 29 | # Verify the archive 30 | if tar -tzf "$output" >/dev/null 2>&1; then 31 | echo "✓ Verified $toolchain archive" 32 | break 33 | else 34 | echo "✗ Invalid archive for $toolchain, retrying..." 35 | rm -f "$output" 36 | fi 37 | else 38 | echo "✗ Failed to download $toolchain (attempt $attempt/3)" 39 | if [ $attempt -eq 3 ]; then 40 | echo "Failed to download $toolchain after 3 attempts" 41 | exit 1 42 | fi 43 | sleep 5 44 | fi 45 | done 46 | done 47 | 48 | echo "" 49 | echo "Download complete! Files in $MIRROR_DIR:" 50 | ls -lh "$MIRROR_DIR" 51 | 52 | echo "" 53 | echo "Next steps:" 54 | echo "1. Create a GitHub release in your vix repository" 55 | echo "2. Upload these files as release assets:" 56 | echo " - x86_64-linux-musl-cross.tgz" 57 | echo " - aarch64-linux-musl-cross.tgz" 58 | echo "3. Update build scripts to use: https://github.com/akash-akya/vix/releases/download/toolchains-v${TOOLCHAIN_VERSION}/" 59 | -------------------------------------------------------------------------------- /lib/vix/vips/flag.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.FlagHelper do 2 | @moduledoc false 3 | 4 | def __before_compile__(env) do 5 | for {name, flag} <- Vix.Nif.nif_vips_flag_list() do 6 | def_vips_flag(name, flag, env) 7 | end 8 | 9 | quote do 10 | end 11 | end 12 | 13 | def def_vips_flag(name, flag, env) do 14 | module_name = Module.concat(Vix.Vips.Flag, name) 15 | {flag_str_list, _} = Enum.unzip(flag) 16 | 17 | spec = Enum.reduce(flag_str_list, &{:|, [], [&1, &2]}) 18 | 19 | contents = 20 | quote do 21 | # Internal module 22 | @moduledoc false 23 | import Bitwise, only: [bor: 2] 24 | 25 | @type t() :: unquote(spec) 26 | 27 | alias Vix.Type 28 | 29 | @behaviour Type 30 | 31 | @impl Type 32 | def typespec do 33 | quote do 34 | list(unquote(__MODULE__).t()) 35 | end 36 | end 37 | 38 | @impl Type 39 | def default(default), do: default 40 | 41 | @impl Type 42 | def to_nif_term(flags, _data) do 43 | Enum.reduce(flags, 0, fn flag, value -> 44 | bor(value, to_nif_term(flag)) 45 | end) 46 | end 47 | 48 | @impl Type 49 | def to_erl_term(value) do 50 | Integer.to_string(value, 2) 51 | |> String.codepoints() 52 | |> Enum.map(fn v -> 53 | {v, ""} = Integer.parse(v, 2) 54 | erl_term(v) 55 | end) 56 | end 57 | 58 | unquote( 59 | Enum.map(flag, fn {name, value} -> 60 | quote do 61 | defp to_nif_term(unquote(name)), do: unquote(value) 62 | 63 | defp erl_term(unquote(value)), do: unquote(name) 64 | end 65 | end) 66 | ) 67 | end 68 | 69 | Module.create(module_name, contents, line: env.line, file: env.file) 70 | end 71 | end 72 | 73 | defmodule Vix.Vips.Flag do 74 | @moduledoc false 75 | @before_compile Vix.Vips.FlagHelper 76 | end 77 | -------------------------------------------------------------------------------- /lib/vix/vips/foreign.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.Foreign do 2 | @moduledoc false 3 | 4 | alias Vix.Nif 5 | 6 | @type operation_name :: String.t() 7 | 8 | @spec find_load_buffer(binary) :: {:ok, operation_name} | {:error, String.t()} 9 | def find_load_buffer(bin) do 10 | Nif.nif_foreign_find_load_buffer(bin) 11 | end 12 | 13 | @spec find_save_buffer(String.t()) :: {:ok, operation_name} | {:error, String.t()} 14 | def find_save_buffer(suffix) do 15 | Nif.nif_foreign_find_save_buffer(suffix) 16 | end 17 | 18 | @doc """ 19 | Returns Vips operation name which can load the passed file 20 | """ 21 | @spec find_load(String.t()) :: {:ok, operation_name} | {:error, String.t()} 22 | def find_load(filename) do 23 | Nif.nif_foreign_find_load(filename) 24 | end 25 | 26 | @doc """ 27 | Returns Vips operation name which can save an image to passed format 28 | """ 29 | @spec find_save(String.t()) :: {:ok, operation_name} | {:error, String.t()} 30 | def find_save(filename) do 31 | Nif.nif_foreign_find_save(filename) 32 | end 33 | 34 | @spec find_load_source(Vix.Vips.Source.t()) :: {:ok, operation_name} | {:error, String.t()} 35 | def find_load_source(%Vix.Vips.Source{ref: vips_source}) do 36 | Nif.nif_foreign_find_load_source(vips_source) 37 | end 38 | 39 | @spec find_save_target(String.t()) :: {:ok, operation_name} | {:error, String.t()} 40 | def find_save_target(suffix) do 41 | # TIFF files cannot be saved via pipe operations in libvips, despite having a save target. 42 | # This workaround prevents attempting unsupported pipe-based TIFF saves. 43 | if suffix in [".tif", ".tiff"] do 44 | {:error, "Failed to find saver for the target"} 45 | else 46 | Nif.nif_foreign_find_save_target(suffix) 47 | end 48 | end 49 | 50 | def get_suffixes do 51 | with {:ok, suffixes} <- Nif.nif_foreign_get_suffixes() do 52 | {:ok, Enum.uniq(suffixes)} 53 | end 54 | end 55 | 56 | def get_loader_suffixes do 57 | with {:ok, suffixes} <- Nif.nif_foreign_get_loader_suffixes() do 58 | {:ok, Enum.uniq(suffixes)} 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /bench/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, 3 | "benchee_html": {:hex, :benchee_html, "1.0.0", "5b4d24effebd060f466fb460ec06576e7b34a00fc26b234fe4f12c4f05c95947", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 1.0", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm", "5280af9aac432ff5ca4216d03e8a93f32209510e925b60e7f27c33796f69e699"}, 4 | "benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"}, 5 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 6 | "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, 7 | "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, 8 | "mogrify": {:hex, :mogrify, "0.9.1", "a26f107c4987477769f272bd0f7e3ac4b7b75b11ba597fd001b877beffa9c068", [:mix], [], "hexpm", "134edf189337d2125c0948bf0c228fdeef975c594317452d536224069a5b7f05"}, 9 | "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, 10 | } 11 | -------------------------------------------------------------------------------- /c_src/g_object/g_type.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../utils.h" 4 | 5 | #include "g_boxed.h" 6 | #include "g_object.h" 7 | #include "g_type.h" 8 | 9 | ErlNifResourceType *G_TYPE_RT; 10 | 11 | static ERL_NIF_TERM g_type_to_erl_term(ErlNifEnv *env, GType type) { 12 | GTypeResource *gtype_r; 13 | ERL_NIF_TERM term; 14 | 15 | gtype_r = enif_alloc_resource(G_TYPE_RT, sizeof(GTypeResource)); 16 | gtype_r->type = type; 17 | 18 | term = enif_make_resource(env, gtype_r); 19 | enif_release_resource(gtype_r); 20 | 21 | return term; 22 | } 23 | 24 | static bool erl_term_to_g_type(ErlNifEnv *env, ERL_NIF_TERM term, GType *type) { 25 | GTypeResource *gtype_r = NULL; 26 | if (enif_get_resource(env, term, G_TYPE_RT, (void **)>ype_r)) { 27 | (*type) = gtype_r->type; 28 | return true; 29 | } 30 | return false; 31 | } 32 | 33 | ERL_NIF_TERM nif_g_type_from_instance(ErlNifEnv *env, int argc, 34 | const ERL_NIF_TERM argv[]) { 35 | 36 | ASSERT_ARGC(argc, 1); 37 | 38 | ERL_NIF_TERM term; 39 | GObject *obj; 40 | GType type; 41 | 42 | if (erl_term_to_g_object(env, argv[0], &obj)) { 43 | term = g_type_to_erl_term(env, G_TYPE_FROM_INSTANCE(obj)); 44 | return make_ok(env, term); 45 | } else if (erl_term_boxed_type(env, argv[0], &type)) { 46 | term = g_type_to_erl_term(env, type); 47 | return make_ok(env, term); 48 | } else { 49 | return make_error(env, "Invalid GTypeInstance"); 50 | } 51 | } 52 | 53 | ERL_NIF_TERM nif_g_type_name(ErlNifEnv *env, int argc, 54 | const ERL_NIF_TERM argv[]) { 55 | ASSERT_ARGC(argc, 1); 56 | 57 | GType type; 58 | 59 | if (!erl_term_to_g_type(env, argv[0], &type)) 60 | return make_error(env, "Failed to get GType"); 61 | 62 | return make_ok(env, make_binary(env, g_type_name(type))); 63 | } 64 | 65 | static void g_type_dtor(ErlNifEnv *env, void *obj) { 66 | debug("GTypeResource dtor"); 67 | } 68 | 69 | int nif_g_type_init(ErlNifEnv *env) { 70 | G_TYPE_RT = enif_open_resource_type( 71 | env, NULL, "g_type_resource", (ErlNifResourceDtor *)g_type_dtor, 72 | ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); 73 | 74 | if (!G_TYPE_RT) { 75 | error("Failed to open g_type_resource"); 76 | return 1; 77 | } 78 | 79 | return 0; 80 | } 81 | -------------------------------------------------------------------------------- /lib/vix/type.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Type do 2 | @moduledoc false 3 | 4 | alias Vix.GObject 5 | alias Vix.Vips 6 | 7 | @callback typespec() :: term() 8 | 9 | @callback default(term) :: term() 10 | 11 | @callback to_nif_term(term, term) :: term() 12 | 13 | @callback to_erl_term(term) :: term() 14 | 15 | def typespec(type) do 16 | impl(type).typespec() 17 | end 18 | 19 | def default(type, data) do 20 | impl(type).default(data) 21 | end 22 | 23 | def to_nif_term(type, value, data) do 24 | impl(type).to_nif_term(value, data) 25 | end 26 | 27 | def to_erl_term(type, value) do 28 | case impl(type) do 29 | :unsupported -> value 30 | module -> module.to_erl_term(value) 31 | end 32 | end 33 | 34 | def supported?(type), do: impl(type) != :unsupported 35 | 36 | # TODO: 37 | # we check if enum_type is enum or not just by the name 38 | # convert type in nif itself, see `get_enum_as_atom` 39 | defp impl({:enum, enum_type}) do 40 | Module.concat(Vix.Vips.Enum, String.to_atom(enum_type)) 41 | end 42 | 43 | # TODO: convert type in nif itself, see `get_flags_as_atoms` 44 | defp impl({:flags, flag_type}) do 45 | Module.concat(Vix.Vips.Flag, String.to_atom(flag_type)) 46 | end 47 | 48 | defp impl("VipsArray" <> nested_type) when nested_type in ~w(Int Double Image) do 49 | Module.concat(Vix.Vips.Array, String.to_atom(nested_type)) 50 | end 51 | 52 | defp impl({:vips_array, nested_type}) 53 | when nested_type in ~w(Int Double Image Enum.VipsBlendMode) do 54 | Module.concat(Vix.Vips.Array, String.to_atom(nested_type)) 55 | end 56 | 57 | defp impl({_spec_type, _value_type}), do: :unsupported 58 | 59 | defp impl("gint"), do: GObject.Int 60 | defp impl("guint64"), do: GObject.UInt64 61 | defp impl("gdouble"), do: GObject.Double 62 | defp impl("gboolean"), do: GObject.Boolean 63 | defp impl("gchararray"), do: GObject.String 64 | defp impl("VipsRefString"), do: Vips.RefString 65 | defp impl("VipsBlob"), do: Vips.Blob 66 | defp impl("MutableVipsImage"), do: Vips.MutableImage 67 | defp impl("VipsImage"), do: Vips.Image 68 | defp impl("VipsSource"), do: Vips.Source 69 | defp impl("VipsTarget"), do: Vips.Target 70 | defp impl("VipsInterpolate"), do: Vips.Interpolate 71 | defp impl(_type), do: :unsupported 72 | end 73 | -------------------------------------------------------------------------------- /lib/vix/vips/interpolate.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.Interpolate do 2 | alias Vix.Type 3 | 4 | defstruct [:ref] 5 | 6 | alias __MODULE__ 7 | 8 | @moduledoc """ 9 | Make interpolators for operators like `affine` and `mapim`. 10 | """ 11 | 12 | alias Vix.Nif 13 | alias Vix.Type 14 | 15 | @behaviour Type 16 | 17 | @typedoc """ 18 | Represents an instance of VipsInterpolate 19 | """ 20 | @type t() :: %Interpolate{ref: reference()} 21 | 22 | @impl Type 23 | def typespec do 24 | quote do 25 | unquote(__MODULE__).t() 26 | end 27 | end 28 | 29 | @impl Type 30 | def default(nil), do: :unsupported 31 | 32 | @impl Type 33 | def to_nif_term(interpolate, _data) do 34 | case interpolate do 35 | %Interpolate{ref: ref} -> 36 | ref 37 | 38 | value -> 39 | raise ArgumentError, message: "expected Vix.Vips.Interpolate. given: #{inspect(value)}" 40 | end 41 | end 42 | 43 | @impl Type 44 | def to_erl_term(ref), do: %Interpolate{ref: ref} 45 | 46 | @doc """ 47 | Make a new interpolator by name. 48 | 49 | Make a new interpolator from the libvips class nickname. For example: 50 | 51 | ```elixir 52 | {:ok, interpolate} = Interpolate.new("bilindear") 53 | ``` 54 | 55 | You can get a list of all supported interpolators from the command-line with: 56 | 57 | ```shell 58 | $ vips -l interpolate 59 | ``` 60 | 61 | See for example `affine`. 62 | """ 63 | @spec new(String.t()) :: {:ok, __MODULE__.t()} | {:error, term()} 64 | def new(name) do 65 | if String.valid?(name) do 66 | Nif.nif_interpolate_new(name) 67 | |> wrap_type() 68 | else 69 | {:error, "expected UTF-8 binary string"} 70 | end 71 | end 72 | 73 | @doc """ 74 | Make a new interpolator by name. 75 | 76 | Make a new interpolator from the libvips class nickname. For example: 77 | 78 | ```elixir 79 | interpolate = Interpolate.new!("bilindear") 80 | ``` 81 | 82 | You can get a list of all supported interpolators from the command-line with: 83 | 84 | ```shell 85 | $ vips -l interpolate 86 | ``` 87 | 88 | See for example `affine`. 89 | """ 90 | @spec new!(String.t()) :: __MODULE__.t() 91 | def new!(name) do 92 | case new(name) do 93 | {:ok, interpolate} -> 94 | interpolate 95 | 96 | {:error, error} -> 97 | raise error 98 | end 99 | end 100 | 101 | defp wrap_type({:ok, ref}), do: {:ok, %Interpolate{ref: ref}} 102 | defp wrap_type(value), do: value 103 | end 104 | -------------------------------------------------------------------------------- /lib/vix/tensor.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Tensor do 2 | alias Vix.Vips.Image 3 | 4 | @moduledoc """ 5 | Struct to hold raw pixel data returned by the libvips along with metadata about the binary. 6 | 7 | Useful for interoperability between other libraries like [Nx](https://hexdocs.pm/nx/Nx.html), [Evision](https://github.com/cocoa-xu/evision/). 8 | 9 | See `Vix.Vips.Image.write_to_tensor/1` to convert an vix image to tensor. 10 | """ 11 | 12 | @typedoc """ 13 | Type of the image pixel when image is represented as Tensor. 14 | 15 | This type is useful for interoperability between different libraries. Type value is same as [`Nx.Type.t()`](https://hexdocs.pm/nx/Nx.Type.html) 16 | 17 | """ 18 | 19 | @type tensor_type() :: 20 | {:u, 8} 21 | | {:s, 8} 22 | | {:u, 16} 23 | | {:s, 16} 24 | | {:u, 32} 25 | | {:s, 32} 26 | | {:f, 32} 27 | | {:f, 64} 28 | 29 | @typedoc """ 30 | Struct to hold raw pixel data returned by the Libvips along with metadata about the binary. 31 | 32 | `:names` will always be `[:height, :width, :bands]` 33 | """ 34 | 35 | @type t() :: %__MODULE__{ 36 | data: binary(), 37 | shape: {non_neg_integer(), non_neg_integer(), non_neg_integer()}, 38 | names: list(), 39 | type: tensor_type() 40 | } 41 | 42 | defstruct data: nil, 43 | shape: {0, 0, 0}, 44 | names: [:height, :width, :bands], 45 | type: {} 46 | 47 | @doc """ 48 | Convert Vix image pixel format to [Nx tensor type](https://hexdocs.pm/nx/Nx.Type.html#t:t/0) 49 | 50 | Vix internally uses [libvips image 51 | format](https://www.libvips.org/API/current/VipsImage.html#VipsBandFormat). To 52 | ease the interoperability between Vix and other elixir libraries, we 53 | can use this function. 54 | 55 | """ 56 | @spec type(image :: Image.t()) :: tensor_type() | no_return() 57 | def type(image) do 58 | # TODO: should we support :VIPS_FORMAT_COMPLEX and :VIPS_FORMAT_DPCOMPLEX ? 59 | image |> Image.format() |> nx_type() 60 | end 61 | 62 | defp nx_type(:VIPS_FORMAT_UCHAR), do: {:u, 8} 63 | defp nx_type(:VIPS_FORMAT_CHAR), do: {:s, 8} 64 | defp nx_type(:VIPS_FORMAT_USHORT), do: {:u, 16} 65 | defp nx_type(:VIPS_FORMAT_SHORT), do: {:s, 16} 66 | defp nx_type(:VIPS_FORMAT_UINT), do: {:u, 32} 67 | defp nx_type(:VIPS_FORMAT_INT), do: {:s, 32} 68 | defp nx_type(:VIPS_FORMAT_FLOAT), do: {:f, 32} 69 | defp nx_type(:VIPS_FORMAT_DOUBLE), do: {:f, 64} 70 | 71 | defp nx_type(other), 72 | do: raise(ArgumentError, "Cannot convert this image type to binary. Found #{inspect(other)}") 73 | end 74 | -------------------------------------------------------------------------------- /test/vix/vips/mutable_image_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.MutableImageTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Vips.Image 5 | alias Vix.Vips.MutableImage 6 | 7 | import Vix.Support.Images 8 | 9 | test "update" do 10 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 11 | {:ok, mim} = MutableImage.new(im) 12 | 13 | assert :ok == MutableImage.update(mim, "orientation", 0) 14 | assert {:ok, 0} == MutableImage.get(mim, "orientation") 15 | end 16 | 17 | test "set" do 18 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 19 | {:ok, mim} = MutableImage.new(im) 20 | 21 | assert {:error, "No such field"} == MutableImage.get(mim, "new-field") 22 | assert :ok == MutableImage.set(mim, "new-field", :gdouble, 0) 23 | assert {:ok, 0.0} === MutableImage.get(mim, "new-field") 24 | end 25 | 26 | test "remove" do 27 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 28 | {:ok, mim} = MutableImage.new(im) 29 | 30 | assert {:ok, 1} == MutableImage.get(mim, "orientation") 31 | 32 | assert :ok == MutableImage.remove(mim, "orientation") 33 | assert {:error, "No such field"} == MutableImage.get(mim, "orientation") 34 | end 35 | 36 | test "set with invalid type" do 37 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 38 | {:ok, mim} = MutableImage.new(im) 39 | 40 | assert {:error, 41 | "invalid gtype. Supported types are [:gint, :guint, :gdouble, :gboolean, :gchararray, :VipsArrayInt, :VipsArrayDouble, :VipsArrayImage, :VipsRefString, :VipsBlob, :VipsImage, :VipsInterpolate]"} == 42 | MutableImage.set(mim, "orientation", "asdf", 0) 43 | end 44 | 45 | test "to_image" do 46 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 47 | {:ok, mim} = MutableImage.new(im) 48 | 49 | :ok = MutableImage.set(mim, "orientation", :gint, 0) 50 | assert {:ok, %Vix.Vips.Image{} = new_img} = MutableImage.to_image(mim) 51 | 52 | assert {:ok, 0} == Image.header_value(new_img, "orientation") 53 | end 54 | 55 | test "introspection" do 56 | {:ok, i} = Vix.Vips.Image.new_from_file(img_path("puppies.jpg")) 57 | 58 | assert {:ok, {_, 518}} = Vix.Vips.Image.mutate(i, fn m -> Vix.Vips.MutableImage.width(m) end) 59 | assert {:ok, {_, 389}} = Vix.Vips.Image.mutate(i, fn m -> Vix.Vips.MutableImage.height(m) end) 60 | assert {:ok, {_, 3}} = Vix.Vips.Image.mutate(i, fn m -> Vix.Vips.MutableImage.bands(m) end) 61 | 62 | assert {:ok, {_, false}} = 63 | Vix.Vips.Image.mutate(i, fn m -> Vix.Vips.MutableImage.has_alpha?(m) end) 64 | end 65 | 66 | test "that returning the mutated image is an acceptable callback return" do 67 | {:ok, i} = Vix.Vips.Image.new_from_file(img_path("puppies.jpg")) 68 | 69 | assert {:ok, _} = Vix.Vips.Image.mutate(i, & &1) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /c_src/vips_operation.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_VIPS_OPERATION_H 2 | #define VIX_VIPS_OPERATION_H 3 | 4 | #include "erl_nif.h" 5 | 6 | ERL_NIF_TERM nif_vips_operation_call(ErlNifEnv *env, int argc, 7 | const ERL_NIF_TERM argv[]); 8 | 9 | ERL_NIF_TERM nif_vips_operation_get_arguments(ErlNifEnv *env, int argc, 10 | const ERL_NIF_TERM argv[]); 11 | 12 | ERL_NIF_TERM nif_vips_operation_list(ErlNifEnv *env, int argc, 13 | const ERL_NIF_TERM argv[]); 14 | 15 | ERL_NIF_TERM nif_vips_enum_list(ErlNifEnv *env, int argc, 16 | const ERL_NIF_TERM argv[]); 17 | 18 | ERL_NIF_TERM nif_vips_flag_list(ErlNifEnv *env, int argc, 19 | const ERL_NIF_TERM argv[]); 20 | 21 | ERL_NIF_TERM nif_vips_cache_set_max(ErlNifEnv *env, int argc, 22 | const ERL_NIF_TERM argv[]); 23 | 24 | ERL_NIF_TERM nif_vips_cache_get_max(ErlNifEnv *env, int argc, 25 | const ERL_NIF_TERM argv[]); 26 | 27 | ERL_NIF_TERM nif_vips_concurrency_set(ErlNifEnv *env, int argc, 28 | const ERL_NIF_TERM argv[]); 29 | 30 | ERL_NIF_TERM nif_vips_concurrency_get(ErlNifEnv *env, int argc, 31 | const ERL_NIF_TERM argv[]); 32 | 33 | ERL_NIF_TERM nif_vips_cache_set_max_files(ErlNifEnv *env, int argc, 34 | const ERL_NIF_TERM argv[]); 35 | 36 | ERL_NIF_TERM nif_vips_cache_get_max_files(ErlNifEnv *env, int argc, 37 | const ERL_NIF_TERM argv[]); 38 | 39 | ERL_NIF_TERM nif_vips_cache_set_max_mem(ErlNifEnv *env, int argc, 40 | const ERL_NIF_TERM argv[]); 41 | 42 | ERL_NIF_TERM nif_vips_cache_get_max_mem(ErlNifEnv *env, int argc, 43 | const ERL_NIF_TERM argv[]); 44 | 45 | ERL_NIF_TERM nif_vips_leak_set(ErlNifEnv *env, int argc, 46 | const ERL_NIF_TERM argv[]); 47 | 48 | ERL_NIF_TERM nif_vips_tracked_get_mem(ErlNifEnv *env, int argc, 49 | const ERL_NIF_TERM argv[]); 50 | 51 | ERL_NIF_TERM nif_vips_tracked_get_mem_highwater(ErlNifEnv *env, int argc, 52 | const ERL_NIF_TERM argv[]); 53 | 54 | ERL_NIF_TERM nif_vips_shutdown(ErlNifEnv *env, int argc, 55 | const ERL_NIF_TERM argv[]); 56 | 57 | ERL_NIF_TERM nif_vips_version(ErlNifEnv *env, int argc, 58 | const ERL_NIF_TERM argv[]); 59 | 60 | ERL_NIF_TERM nif_vips_nickname_find(ErlNifEnv *env, int argc, 61 | const ERL_NIF_TERM argv[]); 62 | 63 | int nif_vips_operation_init(ErlNifEnv *env); 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /c_src/vips_image.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_VIPS_IMAGE_H 2 | #define VIX_VIPS_IMAGE_H 3 | 4 | #include "erl_nif.h" 5 | 6 | ERL_NIF_TERM nif_image_new_from_file(ErlNifEnv *env, int argc, 7 | const ERL_NIF_TERM argv[]); 8 | 9 | ERL_NIF_TERM nif_image_new_from_image(ErlNifEnv *env, int argc, 10 | const ERL_NIF_TERM argv[]); 11 | 12 | ERL_NIF_TERM nif_image_copy_memory(ErlNifEnv *env, int argc, 13 | const ERL_NIF_TERM argv[]); 14 | 15 | ERL_NIF_TERM nif_image_write_to_file(ErlNifEnv *env, int argc, 16 | const ERL_NIF_TERM argv[]); 17 | 18 | ERL_NIF_TERM nif_image_write_to_buffer(ErlNifEnv *env, int argc, 19 | const ERL_NIF_TERM argv[]); 20 | 21 | ERL_NIF_TERM nif_image_new(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]); 22 | 23 | ERL_NIF_TERM nif_image_new_temp_file(ErlNifEnv *env, int argc, 24 | const ERL_NIF_TERM argv[]); 25 | 26 | ERL_NIF_TERM nif_image_new_matrix_from_array(ErlNifEnv *env, int argc, 27 | const ERL_NIF_TERM argv[]); 28 | 29 | ERL_NIF_TERM nif_image_get_fields(ErlNifEnv *env, int argc, 30 | const ERL_NIF_TERM argv[]); 31 | 32 | ERL_NIF_TERM nif_image_get_header(ErlNifEnv *env, int argc, 33 | const ERL_NIF_TERM argv[]); 34 | 35 | ERL_NIF_TERM nif_image_get_as_string(ErlNifEnv *env, int argc, 36 | const ERL_NIF_TERM argv[]); 37 | 38 | ERL_NIF_TERM nif_image_update_metadata(ErlNifEnv *env, int argc, 39 | const ERL_NIF_TERM argv[]); 40 | 41 | ERL_NIF_TERM nif_image_set_metadata(ErlNifEnv *env, int argc, 42 | const ERL_NIF_TERM argv[]); 43 | 44 | ERL_NIF_TERM nif_image_remove_metadata(ErlNifEnv *env, int argc, 45 | const ERL_NIF_TERM argv[]); 46 | 47 | ERL_NIF_TERM nif_image_hasalpha(ErlNifEnv *env, int argc, 48 | const ERL_NIF_TERM argv[]); 49 | 50 | ERL_NIF_TERM nif_image_new_from_source(ErlNifEnv *env, int argc, 51 | const ERL_NIF_TERM argv[]); 52 | 53 | ERL_NIF_TERM nif_image_to_target(ErlNifEnv *env, int argc, 54 | const ERL_NIF_TERM argv[]); 55 | 56 | ERL_NIF_TERM nif_image_new_from_binary(ErlNifEnv *env, int argc, 57 | const ERL_NIF_TERM argv[]); 58 | 59 | ERL_NIF_TERM nif_image_write_to_binary(ErlNifEnv *env, int argc, 60 | const ERL_NIF_TERM argv[]); 61 | 62 | ERL_NIF_TERM nif_image_write_area_to_binary(ErlNifEnv *env, int argc, 63 | const ERL_NIF_TERM argv[]); 64 | #endif 65 | -------------------------------------------------------------------------------- /livebooks/picture-language.livemd: -------------------------------------------------------------------------------- 1 | # Picture Language 2 | 3 | ## Install dependencies 4 | 5 | ```elixir 6 | Mix.install([ 7 | {:kino, "~> 0.3.0"}, 8 | {:vix, "~> 0.5"} 9 | ]) 10 | ``` 11 | 12 | Defining helper function `display` using kino so that we can show image inline 13 | 14 | ```elixir 15 | defmodule VixExt do 16 | alias Vix.Vips.Image 17 | alias Vix.Vips.Operation 18 | 19 | @max_height 500 20 | 21 | def show(%Image{} = image) do 22 | height = Image.height(image) 23 | 24 | # scale down if image height is larger than 500px 25 | image = 26 | if height > @max_height do 27 | Operation.resize!(image, @max_height / height) 28 | else 29 | image 30 | end 31 | 32 | # write vips-image as png image to memory 33 | {:ok, image_bin} = Image.write_to_buffer(image, ".png") 34 | Kino.render(Kino.Image.new(image_bin, "image/png")) 35 | 36 | :ok 37 | end 38 | end 39 | ``` 40 | 41 | ## Picture Language 42 | 43 | Implementing picture language defined in [**Structural and Interpretation of Computer Programs**](https://web.mit.edu/6.001/6.037/sicp.pdf) section [2.2.4](https://web.mit.edu/6.001/6.037/sicp.pdf#subsection.2.2.4) in Elixir using vix 44 | 45 | 46 | ```elixir 47 | defmodule Pict do 48 | alias Vix.Vips.Operation, as: Op 49 | 50 | def beside(a, b) do 51 | Op.resize!(Op.join!(a, b, :VIPS_DIRECTION_HORIZONTAL), 0.5, vscale: 1) 52 | end 53 | 54 | def below(a, b) do 55 | Op.resize!(Op.join!(a, b, :VIPS_DIRECTION_VERTICAL), 1, vscale: 0.5) 56 | end 57 | 58 | def vert_flip(p) do 59 | Op.flip!(p, :VIPS_DIRECTION_VERTICAL) 60 | end 61 | 62 | def horz_flip(p) do 63 | Op.flip!(p, :VIPS_DIRECTION_HORIZONTAL) 64 | end 65 | end 66 | ``` 67 | 68 | Implementation of [Fig. 2.9](https://web.mit.edu/6.001/6.037/sicp.pdf#page=201) 69 | 70 | ```elixir 71 | defmodule PictUtils do 72 | import Pict 73 | 74 | def right_split(p, 0), do: p 75 | 76 | def right_split(p, n) do 77 | t = right_split(p, n - 1) 78 | beside(p, below(t, t)) 79 | end 80 | 81 | def up_split(p, 0), do: p 82 | 83 | def up_split(p, n) do 84 | t = up_split(p, n - 1) 85 | below(beside(t, t), p) 86 | end 87 | 88 | def corner_split(p, 0), do: p 89 | 90 | def corner_split(p, n) do 91 | us = up_split(p, n - 1) 92 | rs = right_split(p, n - 1) 93 | 94 | beside( 95 | below(beside(us, us), p), 96 | below(corner_split(p, n - 1), below(rs, rs)) 97 | ) 98 | end 99 | end 100 | ``` 101 | 102 | ```elixir 103 | alias Vix.Vips.Image 104 | import VixExt 105 | 106 | {:ok, img} = Image.new_from_file("~/Downloads/kitty.png") 107 | img = PictUtils.corner_split(img, 5) 108 | 109 | right = Pict.below(img, Pict.vert_flip(img)) 110 | left = Pict.horz_flip(right) 111 | img = Pict.beside(left, right) 112 | 113 | show(img) 114 | ``` 115 | -------------------------------------------------------------------------------- /lib/vix/vips/array.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.ArrayHelper do 2 | @moduledoc false 3 | 4 | alias Vix.Nif 5 | alias Vix.Vips 6 | 7 | def __before_compile__(env) do 8 | def_vips_array(env, %{ 9 | module_name: Int, 10 | nested_type: Vix.GObject.Int, 11 | nested_typespec: Macro.escape(quote(do: integer())), 12 | to_nif_term: &Nif.nif_int_array/1, 13 | to_erl_term: &Nif.nif_vips_int_array_to_erl_list/1 14 | }) 15 | 16 | def_vips_array(env, %{ 17 | module_name: Double, 18 | nested_type: Vix.GObject.Double, 19 | nested_typespec: Macro.escape(quote(do: float())), 20 | to_nif_term: &Nif.nif_double_array/1, 21 | to_erl_term: &Nif.nif_vips_double_array_to_erl_list/1 22 | }) 23 | 24 | def_vips_array(env, %{ 25 | module_name: Image, 26 | nested_type: Vips.Image, 27 | nested_typespec: Macro.escape(quote(do: Vips.Image.t())), 28 | to_nif_term: &Nif.nif_image_array/1, 29 | to_erl_term: &Nif.nif_vips_image_array_to_erl_list/1 30 | }) 31 | 32 | # array of enum 33 | def_vips_array(env, %{ 34 | module_name: Enum.VipsBlendMode, 35 | nested_type: Vips.Enum.VipsBlendMode, 36 | nested_typespec: Macro.escape(quote(do: Vips.Operation.vips_blend_mode())), 37 | to_nif_term: &Nif.nif_int_array/1, 38 | to_erl_term: &Nif.nif_vips_int_array_to_erl_list/1 39 | }) 40 | end 41 | 42 | def def_vips_array(env, opts) do 43 | module_name = Module.concat([Vix.Vips.Array, opts.module_name]) 44 | 45 | contents = 46 | quote do 47 | # Internal module 48 | @nested_type unquote(opts.nested_type) 49 | @to_nif_term unquote(opts.to_nif_term) 50 | @to_erl_term unquote(opts.to_erl_term) 51 | 52 | @moduledoc false 53 | @opaque t() :: reference() 54 | 55 | alias Vix.Type 56 | 57 | @behaviour Type 58 | 59 | @impl Type 60 | def typespec do 61 | nested_typespec = unquote(opts.nested_typespec) 62 | 63 | quote do 64 | list(unquote(nested_typespec)) 65 | end 66 | end 67 | 68 | @impl Type 69 | def default(default), do: default 70 | 71 | @impl Type 72 | def to_nif_term(value, data) do 73 | Enum.map(value, fn nested_value -> 74 | @nested_type.to_nif_term(nested_value, data) 75 | end) 76 | |> @to_nif_term.() 77 | end 78 | 79 | @impl Type 80 | def to_erl_term(value) do 81 | {:ok, list} = @to_erl_term.(value) 82 | 83 | Enum.map(list, fn nested_value -> 84 | @nested_type.to_erl_term(nested_value) 85 | end) 86 | end 87 | end 88 | 89 | Module.create(module_name, contents, line: env.line, file: env.file) 90 | end 91 | end 92 | 93 | defmodule Vix.Vips.Array do 94 | @moduledoc false 95 | 96 | @before_compile Vix.Vips.ArrayHelper 97 | end 98 | -------------------------------------------------------------------------------- /lib/vix/source_pipe.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.SourcePipe do 2 | use GenServer 3 | require Logger 4 | 5 | alias Vix.Nif 6 | alias __MODULE__ 7 | 8 | @moduledoc false 9 | 10 | defstruct [:fd, :pending, :source] 11 | 12 | defmodule Pending do 13 | @moduledoc false 14 | defstruct bin: [], client_pid: nil 15 | end 16 | 17 | @spec new() :: {pid, Vix.Vips.Source.t()} 18 | def new do 19 | {:ok, pipe} = GenServer.start_link(__MODULE__, nil) 20 | source = GenServer.call(pipe, :source, :infinity) 21 | {pipe, source} 22 | end 23 | 24 | def write(pipe, bin) do 25 | GenServer.call(pipe, {:write, bin}, :infinity) 26 | end 27 | 28 | def stop(pipe) do 29 | GenServer.stop(pipe) 30 | end 31 | 32 | # Server 33 | 34 | def init(_) do 35 | {:ok, nil, {:continue, nil}} 36 | end 37 | 38 | def handle_continue(nil, _) do 39 | case Nif.nif_source_new() do 40 | {:ok, {fd, source}} -> 41 | source_pipe = %SourcePipe{ 42 | fd: fd, 43 | pending: %Pending{}, 44 | source: %Vix.Vips.Source{ref: source} 45 | } 46 | 47 | {:noreply, source_pipe} 48 | 49 | {:error, reason} -> 50 | {:stop, reason, nil} 51 | end 52 | end 53 | 54 | def handle_call(:source, _from, %SourcePipe{source: source} = state) do 55 | {:reply, source, state} 56 | end 57 | 58 | def handle_call({:write, binary}, from, %SourcePipe{pending: %Pending{client_pid: nil}} = state) do 59 | do_write(%SourcePipe{state | pending: %Pending{bin: binary, client_pid: from}}) 60 | end 61 | 62 | def handle_call({:write, _binary}, _from, state) do 63 | {:reply, {:error, :pending_write}, state} 64 | end 65 | 66 | def handle_info({:select, _write_resource, _ref, :ready_output}, state) do 67 | do_write(state) 68 | end 69 | 70 | defmacrop eagain, do: {:error, :eagain} 71 | 72 | defp do_write(%SourcePipe{pending: %Pending{bin: <<>>}} = state) do 73 | reply_action(state, :ok) 74 | end 75 | 76 | defp do_write(%SourcePipe{pending: pending} = state) do 77 | bin_size = byte_size(pending.bin) 78 | 79 | case Nif.nif_write(state.fd, pending.bin) do 80 | {:ok, size} when size < bin_size -> 81 | binary = binary_part(pending.bin, size, bin_size - size) 82 | noreply_action(%{state | pending: %Pending{pending | bin: binary}}) 83 | 84 | {:ok, _size} -> 85 | reply_action(state, :ok) 86 | 87 | eagain() -> 88 | noreply_action(state) 89 | 90 | {:error, errno} -> 91 | reply_action(state, {:error, errno}) 92 | end 93 | end 94 | 95 | defp reply_action(%SourcePipe{pending: pending} = state, ret) do 96 | if pending.client_pid do 97 | :ok = GenServer.reply(pending.client_pid, ret) 98 | end 99 | 100 | {:noreply, %SourcePipe{state | pending: %Pending{}}} 101 | end 102 | 103 | defp noreply_action(state) do 104 | {:noreply, state} 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /c_src/g_object/g_boxed.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../utils.h" 4 | 5 | #include "g_boxed.h" 6 | #include "g_object.h" 7 | 8 | ErlNifResourceType *G_BOXED_RT; 9 | 10 | static ERL_NIF_TERM ATOM_UNREF_GBOXED; 11 | 12 | bool erl_term_to_g_boxed(ErlNifEnv *env, ERL_NIF_TERM term, gpointer *ptr) { 13 | GBoxedResource *boxed_r = NULL; 14 | 15 | if (enif_get_resource(env, term, G_BOXED_RT, (void **)&boxed_r)) { 16 | (*ptr) = boxed_r->boxed_ptr; 17 | return true; 18 | } 19 | 20 | return false; 21 | } 22 | 23 | bool erl_term_boxed_type(ErlNifEnv *env, ERL_NIF_TERM term, GType *type) { 24 | GBoxedResource *boxed_r = NULL; 25 | 26 | if (enif_get_resource(env, term, G_BOXED_RT, (void **)&boxed_r)) { 27 | (*type) = boxed_r->boxed_type; 28 | return true; 29 | } 30 | 31 | return false; 32 | } 33 | 34 | ERL_NIF_TERM nif_g_boxed_unref(ErlNifEnv *env, int argc, 35 | const ERL_NIF_TERM argv[]) { 36 | ASSERT_ARGC(argc, 1); 37 | 38 | GBoxedResource *gboxed_r = NULL; 39 | 40 | if (!enif_get_resource(env, argv[0], G_BOXED_RT, (void **)&gboxed_r)) { 41 | // This should never happen, since g_boxed_unref is an internal call 42 | return ATOM_ERROR; 43 | } 44 | 45 | g_boxed_free(gboxed_r->boxed_type, gboxed_r->boxed_ptr); 46 | 47 | gboxed_r->boxed_ptr = NULL; 48 | 49 | debug("GBoxed unref"); 50 | 51 | return ATOM_OK; 52 | } 53 | 54 | ERL_NIF_TERM boxed_to_erl_term(ErlNifEnv *env, gpointer ptr, GType type) { 55 | ERL_NIF_TERM term; 56 | GBoxedResource *boxed_r; 57 | 58 | boxed_r = enif_alloc_resource(G_BOXED_RT, sizeof(GBoxedResource)); 59 | 60 | // TODO: use elixir-struct instead of c-struct, so that type 61 | // information is visible in elixir 62 | boxed_r->boxed_type = type; 63 | boxed_r->boxed_ptr = ptr; 64 | 65 | term = enif_make_resource(env, boxed_r); 66 | enif_release_resource(boxed_r); 67 | 68 | return term; 69 | } 70 | 71 | static void g_boxed_dtor(ErlNifEnv *env, void *obj) { 72 | GBoxedResource *orig_boxed_r = (GBoxedResource *)obj; 73 | 74 | /* 75 | * Safely unref objects using the janitor process. 76 | * See g_object_dtor() for details 77 | */ 78 | if (orig_boxed_r->boxed_ptr != NULL) { 79 | GBoxedResource *temp_gboxed_r = NULL; 80 | ERL_NIF_TERM temp_term; 81 | 82 | temp_gboxed_r = enif_alloc_resource(G_BOXED_RT, sizeof(GBoxedResource)); 83 | temp_gboxed_r->boxed_ptr = orig_boxed_r->boxed_ptr; 84 | temp_gboxed_r->boxed_type = orig_boxed_r->boxed_type; 85 | 86 | temp_term = enif_make_resource(env, temp_gboxed_r); 87 | enif_release_resource(temp_gboxed_r); 88 | 89 | send_to_janitor(env, ATOM_UNREF_GBOXED, temp_term); 90 | 91 | debug("GBoxedResource is sent to janitor process"); 92 | } else { 93 | debug("GBoxedResource is already unset"); 94 | } 95 | } 96 | 97 | int nif_g_boxed_init(ErlNifEnv *env) { 98 | G_BOXED_RT = enif_open_resource_type( 99 | env, NULL, "g_boxed_resource", (ErlNifResourceDtor *)g_boxed_dtor, 100 | ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); 101 | 102 | if (!G_BOXED_RT) { 103 | error("Failed to open g_boxed_resource"); 104 | return 1; 105 | } 106 | 107 | ATOM_UNREF_GBOXED = make_atom(env, "unref_gboxed"); 108 | 109 | return 0; 110 | } 111 | -------------------------------------------------------------------------------- /test/vix/vips/operation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.OperationTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Vix.Vips.Image 5 | alias Vix.Vips.Operation 6 | 7 | import Vix.Support.Images 8 | 9 | test "invert" do 10 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 11 | assert {:ok, out} = Operation.invert(im) 12 | 13 | out_path = Briefly.create!(extname: ".jpg") 14 | :ok = Image.write_to_file(out, out_path) 15 | 16 | assert_files_equal(img_path("invert_puppies.jpg"), out_path) 17 | end 18 | 19 | test "affine" do 20 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 21 | assert {:ok, out} = Operation.affine(im, [1, 0, 0, 0.5]) 22 | 23 | out_path = Briefly.create!(extname: ".jpg") 24 | :ok = Image.write_to_file(out, out_path) 25 | 26 | assert_files_equal(img_path("affine_puppies.jpg"), out_path) 27 | end 28 | 29 | test "gravity" do 30 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 31 | 32 | assert {:ok, out} = 33 | Operation.gravity(im, :VIPS_COMPASS_DIRECTION_CENTRE, 650, 500, 34 | extend: :VIPS_EXTEND_COPY 35 | ) 36 | 37 | out_path = Briefly.create!(extname: ".jpg") 38 | :ok = Image.write_to_file(out, out_path) 39 | 40 | assert_files_equal(img_path("gravity_puppies.jpg"), out_path) 41 | end 42 | 43 | test "conv with simple edge detection kernel" do 44 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 45 | {:ok, mask} = Image.new_matrix_from_array(3, 3, [[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]]) 46 | 47 | assert {:ok, out} = Operation.conv(im, mask, precision: :VIPS_PRECISION_FLOAT) 48 | 49 | out_path = Briefly.create!(extname: ".jpg") 50 | :ok = Image.write_to_file(out, out_path) 51 | 52 | assert_files_equal(img_path("conv_puppies.jpg"), out_path) 53 | end 54 | 55 | test "additional return values" do 56 | {:ok, im} = Image.new_from_file(img_path("black_on_white.jpg")) 57 | 58 | assert {:ok, {min, %{x: _, y: _, "out-array": [min], "x-array": [_ | _], "y-array": [_ | _]}}} = 59 | Operation.min(im) 60 | 61 | assert min in [-0.0, +0.0] 62 | end 63 | 64 | test "required output order" do 65 | {:ok, im} = Image.new_from_file(img_path("black_on_white.jpg")) 66 | assert Operation.find_trim(im) == {:ok, {41, 44, 45, 45}} 67 | end 68 | 69 | test "when unsupported argument is passed" do 70 | buf = File.read!(img_path("alpha_band.png")) 71 | assert {:ok, {%Image{}, _}} = Operation.pngload_buffer(buf, foo: "bar") 72 | end 73 | 74 | test "operation error" do 75 | {:ok, im} = Image.new_from_file(img_path("black_on_white.jpg")) 76 | 77 | assert Operation.affine(im, [1, 1, 1, 1]) == 78 | {:error, 79 | "operation build: vips__transform_calc_inverse: singular or near-singular matrix"} 80 | end 81 | 82 | test "image type mismatch error" do 83 | assert_raise ArgumentError, "expected Vix.Vips.Image. given: :invalid", fn -> 84 | Operation.invert(:invalid) 85 | end 86 | end 87 | 88 | test "enum parameter" do 89 | {:ok, im} = Image.new_from_file(img_path("black_on_white.jpg")) 90 | {:ok, out} = Operation.flip(im, :VIPS_DIRECTION_HORIZONTAL) 91 | 92 | out_path = Briefly.create!(extname: ".jpg") 93 | :ok = Image.write_to_file(out, out_path) 94 | 95 | assert_files_equal(img_path("black_on_white_hflip.jpg"), out_path) 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/vix/vips.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips do 2 | @moduledoc """ 3 | Module for Vix.Vips. 4 | """ 5 | 6 | alias Vix.Nif 7 | 8 | @doc """ 9 | Set the maximum number of operations we keep in cache. 10 | """ 11 | @spec cache_set_max(integer()) :: :ok 12 | def cache_set_max(max) do 13 | Nif.nif_vips_cache_set_max(max) 14 | end 15 | 16 | @doc """ 17 | Get the maximum number of operations we keep in cache. 18 | """ 19 | @spec cache_get_max() :: integer() 20 | def cache_get_max do 21 | Nif.nif_vips_cache_get_max() 22 | end 23 | 24 | @doc """ 25 | Sets the number of worker threads that vips should use when running a VipsThreadPool. 26 | 27 | The special value 0 means "default". In this case, the number of threads is set by the environment variable VIPS_CONCURRENCY, or if that is not set, the number of threads available on the host machine. 28 | """ 29 | @spec concurrency_set(integer()) :: :ok 30 | def concurrency_set(concurrency) do 31 | Nif.nif_vips_concurrency_set(concurrency) 32 | end 33 | 34 | @doc """ 35 | Returns the number of worker threads that vips should use when running a VipsThreadPool. 36 | 37 | The final value is clipped to the range 1 - 1024. 38 | """ 39 | @spec concurrency_get() :: integer() 40 | def concurrency_get do 41 | Nif.nif_vips_concurrency_get() 42 | end 43 | 44 | @doc """ 45 | Set the maximum number of tracked files we allow before we start dropping cached operations. 46 | """ 47 | @spec cache_set_max_files(integer()) :: :ok 48 | def cache_set_max_files(max_files) do 49 | Nif.nif_vips_cache_set_max_files(max_files) 50 | end 51 | 52 | @doc """ 53 | Get the maximum number of tracked files we allow before we start dropping cached operations. 54 | 55 | libvips only tracks file descriptors it allocates, it can't track ones allocated by external libraries. 56 | """ 57 | @spec cache_get_max_files() :: integer() 58 | def cache_get_max_files do 59 | Nif.nif_vips_cache_get_max_files() 60 | end 61 | 62 | @doc """ 63 | Set the maximum amount of tracked memory we allow before we start dropping cached operations. 64 | 65 | libvips only tracks file descriptors it allocates, it can't track ones allocated by external libraries. 66 | """ 67 | @spec cache_set_max_mem(integer()) :: :ok 68 | def cache_set_max_mem(max_mem) do 69 | Nif.nif_vips_cache_set_max_mem(max_mem) 70 | end 71 | 72 | @doc """ 73 | Get the maximum amount of tracked memory we allow before we start dropping cached operations. 74 | """ 75 | @spec cache_get_max_mem() :: integer() 76 | def cache_get_max_mem do 77 | Nif.nif_vips_cache_get_max_mem() 78 | end 79 | 80 | @doc """ 81 | Turn on or off vips leak checking 82 | """ 83 | @spec set_vips_leak_checking(boolean()) :: :ok 84 | def set_vips_leak_checking(bool) when is_boolean(bool) do 85 | Nif.nif_vips_leak_set(if bool, do: 1, else: 0) 86 | end 87 | 88 | @doc """ 89 | Returns the number of bytes currently allocated by libvips. 90 | 91 | Libvips uses this figure to decide when to start dropping cache. 92 | """ 93 | @spec tracked_get_mem() :: integer() 94 | def tracked_get_mem do 95 | Nif.nif_vips_tracked_get_mem() 96 | end 97 | 98 | @doc """ 99 | Returns the largest number of bytes simultaneously allocated via libvips. 100 | 101 | Handy for estimating max memory requirements for a program. 102 | """ 103 | @spec tracked_get_mem_highwater() :: integer() 104 | def tracked_get_mem_highwater do 105 | Nif.nif_vips_tracked_get_mem_highwater() 106 | end 107 | 108 | @doc """ 109 | Get installed vips version 110 | """ 111 | @spec version() :: String.t() 112 | def version do 113 | {major, minor, micro} = Nif.nif_vips_version() 114 | "#{major}.#{minor}.#{micro}" 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/vix/target_pipe.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.TargetPipe do 2 | use GenServer 3 | require Logger 4 | 5 | alias Vix.Nif 6 | alias __MODULE__ 7 | 8 | @moduledoc false 9 | 10 | @type t() :: struct 11 | 12 | defstruct [:fd, :pending, :task_result, :task_pid] 13 | 14 | defmodule Pending do 15 | @moduledoc false 16 | defstruct size: nil, client_pid: nil, opts: [] 17 | end 18 | 19 | @default_buffer_size 65_535 20 | 21 | @spec new(Vix.Vips.Image.t(), String.t(), keyword) :: GenServer.on_start() 22 | def new(image, suffix, opts) do 23 | GenServer.start_link(__MODULE__, %{image: image, suffix: suffix, opts: opts}) 24 | end 25 | 26 | def read(process, max_size \\ @default_buffer_size) 27 | when is_integer(max_size) and max_size > 0 do 28 | GenServer.call(process, {:read, max_size}, :infinity) 29 | end 30 | 31 | def stop(pid) do 32 | GenServer.stop(pid) 33 | end 34 | 35 | # Server 36 | 37 | def init(%{image: image, suffix: suffix, opts: opts}) do 38 | Process.flag(:trap_exit, true) 39 | {:ok, nil, {:continue, %{image: image, suffix: suffix, opts: opts}}} 40 | end 41 | 42 | def handle_continue(%{image: image, suffix: suffix, opts: opts}, _) do 43 | case Nif.nif_target_new() do 44 | {:ok, {fd, target}} -> 45 | pid = start_task(image, %Vix.Vips.Target{ref: target}, suffix, opts) 46 | {:noreply, %TargetPipe{fd: fd, task_pid: pid, pending: %Pending{}}} 47 | 48 | {:error, reason} -> 49 | {:stop, reason, nil} 50 | end 51 | end 52 | 53 | def handle_call({:read, size}, from, %TargetPipe{pending: %Pending{client_pid: nil}} = state) do 54 | do_read(%TargetPipe{state | pending: %Pending{size: size, client_pid: from}}) 55 | end 56 | 57 | def handle_call({:read, _size}, _from, state) do 58 | {:reply, {:error, :pending_read}, state} 59 | end 60 | 61 | def handle_info({:select, _read_resource, _ref, :ready_input}, state) do 62 | do_read(state) 63 | end 64 | 65 | def handle_info({:EXIT, from, result}, %{task_pid: from} = state) do 66 | do_read(%TargetPipe{state | task_result: result, task_pid: nil}) 67 | end 68 | 69 | defmacrop eof, do: {:ok, <<>>} 70 | defmacrop eagain, do: {:error, :eagain} 71 | 72 | defp do_read(%TargetPipe{task_result: {:error, _reason} = error} = state) do 73 | reply_action(state, error) 74 | end 75 | 76 | defp do_read(%TargetPipe{pending: %{size: size}} = state) do 77 | case Nif.nif_read(state.fd, size) do 78 | eof() -> 79 | reply_action(state, :eof) 80 | 81 | {:ok, binary} -> 82 | reply_action(state, {:ok, binary}) 83 | 84 | eagain() -> 85 | noreply_action(state) 86 | 87 | {:error, errno} -> 88 | reply_action(state, {:error, errno}) 89 | end 90 | end 91 | 92 | defp reply_action(%TargetPipe{pending: pending} = state, ret) do 93 | if pending.client_pid do 94 | :ok = GenServer.reply(pending.client_pid, ret) 95 | end 96 | 97 | {:noreply, %TargetPipe{state | pending: %Pending{}}} 98 | end 99 | 100 | defp noreply_action(state) do 101 | {:noreply, state} 102 | end 103 | 104 | @spec start_task(Vix.Vips.Image.t(), Vix.Vips.Target.t(), String.t(), keyword) :: pid 105 | defp start_task(%Vix.Vips.Image{} = image, target, suffix, []) do 106 | spawn_link(fn -> 107 | result = Nif.nif_image_to_target(image.ref, target.ref, suffix) 108 | Process.exit(self(), result) 109 | end) 110 | end 111 | 112 | defp start_task(image, target, suffix, opts) do 113 | spawn_link(fn -> 114 | result = 115 | with {:ok, saver} <- Vix.Vips.Foreign.find_save_target(suffix) do 116 | Vix.Vips.Operation.Helper.operation_call(saver, [image, target], opts) 117 | end 118 | 119 | Process.exit(self(), result) 120 | end) 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /c_src/.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Right 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllArgumentsOnNextLine: true 12 | AllowAllConstructorInitializersOnNextLine: true 13 | AllowAllParametersOfDeclarationOnNextLine: true 14 | AllowShortBlocksOnASingleLine: false 15 | AllowShortCaseLabelsOnASingleLine: false 16 | AllowShortFunctionsOnASingleLine: All 17 | AllowShortLambdasOnASingleLine: All 18 | AllowShortIfStatementsOnASingleLine: Never 19 | AllowShortLoopsOnASingleLine: false 20 | AlwaysBreakAfterDefinitionReturnType: None 21 | AlwaysBreakAfterReturnType: None 22 | AlwaysBreakBeforeMultilineStrings: false 23 | AlwaysBreakTemplateDeclarations: MultiLine 24 | BinPackArguments: true 25 | BinPackParameters: true 26 | BraceWrapping: 27 | AfterCaseLabel: false 28 | AfterClass: false 29 | AfterControlStatement: false 30 | AfterEnum: false 31 | AfterFunction: false 32 | AfterNamespace: false 33 | AfterObjCDeclaration: false 34 | AfterStruct: false 35 | AfterUnion: false 36 | AfterExternBlock: false 37 | BeforeCatch: false 38 | BeforeElse: false 39 | IndentBraces: false 40 | SplitEmptyFunction: true 41 | SplitEmptyRecord: true 42 | SplitEmptyNamespace: true 43 | BreakBeforeBinaryOperators: None 44 | BreakBeforeBraces: Attach 45 | BreakBeforeInheritanceComma: false 46 | BreakInheritanceList: BeforeColon 47 | BreakBeforeTernaryOperators: true 48 | BreakConstructorInitializersBeforeComma: false 49 | BreakConstructorInitializers: BeforeColon 50 | BreakAfterJavaFieldAnnotations: false 51 | BreakStringLiterals: true 52 | ColumnLimit: 80 53 | CommentPragmas: '^ IWYU pragma:' 54 | CompactNamespaces: false 55 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 56 | ConstructorInitializerIndentWidth: 4 57 | ContinuationIndentWidth: 4 58 | Cpp11BracedListStyle: true 59 | DerivePointerAlignment: false 60 | DisableFormat: false 61 | ExperimentalAutoDetectBinPacking: false 62 | FixNamespaceComments: true 63 | ForEachMacros: 64 | - foreach 65 | - Q_FOREACH 66 | - BOOST_FOREACH 67 | IncludeBlocks: Preserve 68 | IncludeCategories: 69 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 70 | Priority: 2 71 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 72 | Priority: 3 73 | - Regex: '.*' 74 | Priority: 1 75 | IncludeIsMainRegex: '(Test)?$' 76 | IndentCaseLabels: false 77 | IndentPPDirectives: None 78 | IndentWidth: 2 79 | IndentWrappedFunctionNames: false 80 | JavaScriptQuotes: Leave 81 | JavaScriptWrapImports: true 82 | KeepEmptyLinesAtTheStartOfBlocks: true 83 | MacroBlockBegin: '' 84 | MacroBlockEnd: '' 85 | MaxEmptyLinesToKeep: 1 86 | NamespaceIndentation: None 87 | ObjCBinPackProtocolList: Auto 88 | ObjCBlockIndentWidth: 2 89 | ObjCSpaceAfterProperty: false 90 | ObjCSpaceBeforeProtocolList: true 91 | PenaltyBreakAssignment: 2 92 | PenaltyBreakBeforeFirstCallParameter: 19 93 | PenaltyBreakComment: 300 94 | PenaltyBreakFirstLessLess: 120 95 | PenaltyBreakString: 1000 96 | PenaltyBreakTemplateDeclaration: 10 97 | PenaltyExcessCharacter: 1000000 98 | PenaltyReturnTypeOnItsOwnLine: 60 99 | PointerAlignment: Right 100 | ReflowComments: true 101 | SortIncludes: true 102 | SortUsingDeclarations: true 103 | SpaceAfterCStyleCast: false 104 | SpaceAfterLogicalNot: false 105 | SpaceAfterTemplateKeyword: true 106 | SpaceBeforeAssignmentOperators: true 107 | SpaceBeforeCpp11BracedList: false 108 | SpaceBeforeCtorInitializerColon: true 109 | SpaceBeforeInheritanceColon: true 110 | SpaceBeforeParens: ControlStatements 111 | SpaceBeforeRangeBasedForLoopColon: true 112 | SpaceInEmptyParentheses: false 113 | SpacesBeforeTrailingComments: 1 114 | SpacesInAngles: false 115 | SpacesInContainerLiterals: true 116 | SpacesInCStyleCastParentheses: false 117 | SpacesInParentheses: false 118 | SpacesInSquareBrackets: false 119 | Standard: Cpp11 120 | StatementMacros: 121 | - Q_UNUSED 122 | - QT_REQUIRE_VERSION 123 | TabWidth: 8 124 | UseTab: Never 125 | ... 126 | -------------------------------------------------------------------------------- /.github/workflows/precompile.yaml: -------------------------------------------------------------------------------- 1 | name: precompile 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | linux: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 60 12 | env: 13 | MIX_ENV: "prod" 14 | strategy: 15 | matrix: 16 | include: 17 | - elixir: 1.14.x 18 | otp: 25.x 19 | - elixir: 1.14.x 20 | otp: 26.x 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: erlef/setup-beam@v1 24 | with: 25 | otp-version: ${{matrix.otp}} 26 | elixir-version: ${{matrix.elixir}} 27 | - name: Cache musl cross compilers 28 | id: cache-musl 29 | uses: actions/cache@v3 30 | with: 31 | path: | 32 | x86_64-linux-musl-cross 33 | aarch64-linux-musl-cross 34 | key: musl-${{ runner.os }}-build 35 | - if: ${{ steps.cache-musl.outputs.cache-hit != 'true' }} 36 | name: Setup musl compilers 37 | run: scripts/download_toolchains.sh 38 | - name: Install Dependencies 39 | run: | 40 | set -e 41 | sudo apt-get update 42 | sudo apt-get install -y gcc make curl tar \ 43 | gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf 44 | echo "$PWD/x86_64-linux-musl-cross/bin" >> $GITHUB_PATH 45 | echo "$PWD/aarch64-linux-musl-cross/bin" >> $GITHUB_PATH 46 | - run: | 47 | set -e 48 | mix deps.get 49 | MIX_ENV=test mix test 50 | - name: Pre-compile NIF library 51 | run: | 52 | set -e 53 | export ELIXIR_MAKE_CACHE_DIR=$(pwd)/cache 54 | mkdir -p "${ELIXIR_MAKE_CACHE_DIR}" 55 | mix elixir_make.precompile 56 | - uses: softprops/action-gh-release@v1 57 | if: startsWith(github.ref, 'refs/tags/') 58 | with: 59 | files: | 60 | cache/*.tar.gz 61 | 62 | macos: 63 | runs-on: macos-14 64 | timeout-minutes: 60 65 | env: 66 | MIX_ENV: "prod" 67 | strategy: 68 | matrix: 69 | include: 70 | - elixir: '1.14.5' 71 | otp: '25.1' 72 | - elixir: '1.14.5' 73 | otp: '26.0' 74 | steps: 75 | - uses: actions/checkout@v3 76 | - name: Install asdf 77 | uses: asdf-vm/actions/setup@v2 78 | 79 | - name: Cache asdf 80 | id: asdf-cache 81 | uses: actions/cache@v3 82 | with: 83 | path: ~/.asdf 84 | key: asdf-${{ runner.os }}-build-${{ matrix.otp }}-${{ matrix.elixir }} 85 | 86 | - if: ${{ steps.asdf-cache.outputs.cache-hit != 'true' }} 87 | name: Install Erlang & Elixir 88 | env: 89 | ELIXIR_VERSION: ${{ matrix.elixir }} 90 | OTP_VERSION: ${{ matrix.otp }} 91 | run: | 92 | set -e 93 | asdf plugin-add erlang 94 | asdf install erlang ${OTP_VERSION} 95 | 96 | ELIXIR_OTP_VERSION=$(echo $OTP_VERSION | cut -d. -f1) 97 | asdf plugin-add elixir 98 | asdf install elixir ${ELIXIR_VERSION}-otp-${ELIXIR_OTP_VERSION} 99 | 100 | - name: Setup Erlang & Elixir 101 | env: 102 | ELIXIR_VERSION: ${{ matrix.elixir }} 103 | OTP_VERSION: ${{ matrix.otp }} 104 | run: | 105 | set -e 106 | asdf global erlang ${OTP_VERSION} 107 | ELIXIR_OTP_VERSION=$(echo $OTP_VERSION | cut -d. -f1) 108 | asdf global elixir ${ELIXIR_VERSION}-otp-${ELIXIR_OTP_VERSION} 109 | 110 | - name: Install hex & rebar 111 | run: | 112 | set -e 113 | mix local.hex --force 114 | mix local.rebar --force 115 | - run: | 116 | set -e 117 | mix deps.get 118 | MIX_ENV=test mix test 119 | 120 | - name: Pre-compile NIF library 121 | run: | 122 | set -e 123 | export ELIXIR_MAKE_CACHE_DIR=$(pwd)/cache 124 | mkdir -p "${ELIXIR_MAKE_CACHE_DIR}" 125 | mix elixir_make.precompile 126 | - uses: softprops/action-gh-release@v1 127 | if: startsWith(github.ref, 'refs/tags/') 128 | with: 129 | files: | 130 | cache/*.tar.gz 131 | -------------------------------------------------------------------------------- /lib/vix/vips/mutable_operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.MutableOperation do 2 | @moduledoc """ 3 | Module for Vix.Vips.MutableOperation. 4 | """ 5 | 6 | import Vix.Vips.Operation.Helper 7 | 8 | alias Vix.Vips.Operation.Error 9 | 10 | # define typespec for enums 11 | Enum.map(vips_enum_list(), fn {name, enum} -> 12 | {enum_str_list, _} = Enum.unzip(enum) 13 | @type unquote(type_name(name)) :: unquote(atom_typespec_ast(enum_str_list)) 14 | end) 15 | 16 | # define typespec for flags 17 | Enum.map(vips_flag_list(), fn {name, flag} -> 18 | {flag_str_list, _} = Enum.unzip(flag) 19 | @type unquote(type_name(name)) :: list(unquote(atom_typespec_ast(flag_str_list))) 20 | end) 21 | 22 | Enum.map(vips_mutable_operation_list(), fn name -> 23 | %{ 24 | desc: desc, 25 | in_req_spec: in_req_spec, 26 | in_opt_spec: in_opt_spec, 27 | out_req_spec: out_req_spec, 28 | out_opt_spec: out_opt_spec 29 | } = spec = operation_args_spec(name) 30 | 31 | # ensure only first param is mutable image 32 | [%{type: "MutableVipsImage"} | remaining_args] = in_req_spec ++ in_opt_spec 33 | 34 | if Enum.any?(remaining_args, &(&1.type == "MutableVipsImage")) do 35 | raise "Only first param can be MutableVipsImage" 36 | end 37 | 38 | func_name = function_name(name) 39 | 40 | req_params = 41 | Enum.map(in_req_spec, fn param -> 42 | param.param_name 43 | |> String.to_atom() 44 | |> Macro.var(__MODULE__) 45 | end) 46 | 47 | @doc """ 48 | #{prepare_doc(desc, in_req_spec, in_opt_spec, out_req_spec, out_opt_spec)} 49 | """ 50 | @spec unquote(func_typespec(func_name, in_req_spec, in_opt_spec, out_req_spec, out_opt_spec)) 51 | if in_opt_spec == [] do 52 | # operations without optional arguments 53 | def unquote(func_name)(unquote_splicing(req_params)) do 54 | [mutable_image | rest_params] = unquote(req_params) 55 | 56 | operation_cb = fn image -> 57 | operation_call( 58 | unquote(name), 59 | [image | rest_params], 60 | [], 61 | unquote(Macro.escape(spec)) 62 | ) 63 | end 64 | 65 | GenServer.call(mutable_image.pid, {:operation, operation_cb}) 66 | end 67 | else 68 | # operations with optional arguments 69 | def unquote(func_name)(unquote_splicing(req_params), optional \\ []) do 70 | [mutable_image | rest_params] = unquote(req_params) 71 | 72 | operation_cb = fn image -> 73 | operation_call( 74 | unquote(name), 75 | [image | rest_params], 76 | optional, 77 | unquote(Macro.escape(spec)) 78 | ) 79 | end 80 | 81 | GenServer.call(mutable_image.pid, {:operation, operation_cb}) 82 | end 83 | end 84 | 85 | bang_func_name = function_name(String.to_atom(name <> "!")) 86 | 87 | @doc """ 88 | #{prepare_doc(desc, in_req_spec, in_opt_spec, out_req_spec, out_opt_spec)} 89 | """ 90 | @spec unquote( 91 | bang_func_typespec( 92 | bang_func_name, 93 | in_req_spec, 94 | in_opt_spec, 95 | out_req_spec, 96 | out_opt_spec 97 | ) 98 | ) 99 | if in_opt_spec == [] do 100 | @dialyzer {:no_match, [{bang_func_name, length(req_params)}]} 101 | # operations without optional arguments 102 | def unquote(bang_func_name)(unquote_splicing(req_params)) do 103 | case __MODULE__.unquote(func_name)(unquote_splicing(req_params)) do 104 | :ok -> :ok 105 | {:ok, result} -> result 106 | {:error, reason} when is_binary(reason) -> raise Error, message: reason 107 | {:error, reason} -> raise Error, message: inspect(reason) 108 | end 109 | end 110 | else 111 | @dialyzer {:no_match, [{bang_func_name, length(req_params) + 1}]} 112 | # operations with optional arguments 113 | def unquote(bang_func_name)(unquote_splicing(req_params), optional \\ []) do 114 | case __MODULE__.unquote(func_name)(unquote_splicing(req_params), optional) do 115 | :ok -> :ok 116 | {:ok, result} -> result 117 | {:error, reason} when is_binary(reason) -> raise Error, message: reason 118 | {:error, reason} -> raise Error, message: inspect(reason) 119 | end 120 | end 121 | end 122 | end) 123 | end 124 | -------------------------------------------------------------------------------- /c_src/g_object/g_object.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "../utils.h" 4 | 5 | #include "g_object.h" 6 | 7 | ErlNifResourceType *G_OBJECT_RT; 8 | 9 | static ERL_NIF_TERM ATOM_UNREF_GOBJECT; 10 | 11 | // Ownership is transferred to beam, `obj` must *not* be freed 12 | // by the caller 13 | ERL_NIF_TERM g_object_to_erl_term(ErlNifEnv *env, GObject *obj) { 14 | ERL_NIF_TERM term; 15 | GObjectResource *gobject_r; 16 | 17 | gobject_r = enif_alloc_resource(G_OBJECT_RT, sizeof(GObjectResource)); 18 | 19 | // TODO: Keep gtype name and use elixir-struct instead of c-struct, 20 | // so that type information is visible in elixir. 21 | gobject_r->obj = obj; 22 | 23 | term = enif_make_resource(env, gobject_r); 24 | enif_release_resource(gobject_r); 25 | 26 | return term; 27 | } 28 | 29 | ERL_NIF_TERM nif_g_object_type_name(ErlNifEnv *env, int argc, 30 | const ERL_NIF_TERM argv[]) { 31 | ASSERT_ARGC(argc, 1); 32 | 33 | GObject *obj; 34 | 35 | if (!erl_term_to_g_object(env, argv[0], &obj)) 36 | return make_error(env, "Failed to get GObject"); 37 | 38 | return make_binary(env, G_OBJECT_TYPE_NAME(obj)); 39 | } 40 | 41 | ERL_NIF_TERM nif_g_object_unref(ErlNifEnv *env, int argc, 42 | const ERL_NIF_TERM argv[]) { 43 | ASSERT_ARGC(argc, 1); 44 | 45 | GObjectResource *gobject_r = NULL; 46 | 47 | if (!enif_get_resource(env, argv[0], G_OBJECT_RT, (void **)&gobject_r)) { 48 | // This should never happen, since g_object_unref is an internal call 49 | return ATOM_ERROR; 50 | } 51 | 52 | g_object_unref(gobject_r->obj); 53 | gobject_r->obj = NULL; 54 | 55 | debug("GObject unref"); 56 | 57 | return ATOM_OK; 58 | } 59 | 60 | bool erl_term_to_g_object(ErlNifEnv *env, ERL_NIF_TERM term, GObject **obj) { 61 | GObjectResource *gobject_r = NULL; 62 | if (enif_get_resource(env, term, G_OBJECT_RT, (void **)&gobject_r)) { 63 | (*obj) = gobject_r->obj; 64 | return true; 65 | } 66 | return false; 67 | } 68 | 69 | static void g_object_dtor(ErlNifEnv *env, void *ptr) { 70 | GObjectResource *orig_gobject_r = (GObjectResource *)ptr; 71 | 72 | /** 73 | * The resource destructor is executed inside a normal scheduler instead of a 74 | * dirty scheduler, which can cause issues if the code is time-consuming. 75 | * See: https://erlangforums.com/t/4290 76 | * 77 | * To address this, we avoid performing time-consuming work in the destructor 78 | * and offload it to a janitor process. The Janitor process then calls the 79 | * time-consuming cleanup NIF code on a dirty scheduler. Since Beam 80 | * deallocates the resource at the end of the `dtor` call, we must create a 81 | * new resource term to pass the object to the janitor process. 82 | * 83 | * Resources can be of two types: 84 | * 85 | * 1. Normal Resource: Constructed during normal operations; the pointer to 86 | * the object is never NULL in this case. 87 | * 88 | * 2. Internal Resource: Constructed within the `dtor` of a normal resource 89 | * solely for cleanup purposes and not for image processing operations. The 90 | * pointer to the object will be NULL after cleanup. 91 | * 92 | * Currently, we use this length approach for all `g_object` and 93 | * `g_boxed` objects, including smaller types like `VipsArray` of 94 | * integers or doubles. For these smaller objects, it might be more 95 | * efficient to skip certain steps. However, we are deferring the 96 | * implementation of such special cases to keep the code simple for 97 | * now. 98 | * 99 | */ 100 | if (orig_gobject_r->obj != NULL) { 101 | GObjectResource *temp_gobject_r = NULL; 102 | ERL_NIF_TERM temp_term; 103 | 104 | /* Create temporary internal resource for the cleanup */ 105 | temp_gobject_r = enif_alloc_resource(G_OBJECT_RT, sizeof(GObjectResource)); 106 | temp_gobject_r->obj = orig_gobject_r->obj; 107 | 108 | temp_term = enif_make_resource(env, temp_gobject_r); 109 | enif_release_resource(temp_gobject_r); 110 | send_to_janitor(env, ATOM_UNREF_GOBJECT, temp_term); 111 | debug("GObjectResource is sent to janitor process"); 112 | } else { 113 | debug("GObjectResource is already unset"); 114 | } 115 | 116 | return; 117 | } 118 | 119 | int nif_g_object_init(ErlNifEnv *env) { 120 | G_OBJECT_RT = enif_open_resource_type( 121 | env, NULL, "g_object_resource", (ErlNifResourceDtor *)g_object_dtor, 122 | ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); 123 | 124 | if (!G_OBJECT_RT) { 125 | error("Failed to open gobject_resource"); 126 | return 1; 127 | } 128 | 129 | ATOM_UNREF_GOBJECT = make_atom(env, "unref_gobject"); 130 | 131 | return 0; 132 | } 133 | -------------------------------------------------------------------------------- /test/vix/vips/access_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.AccessTest do 2 | use ExUnit.Case, async: true 3 | import Vix.Support.Images 4 | alias Vix.Vips.Image 5 | 6 | test "Access behaviour for Vix.Vips.Image for an integer retrieves a band" do 7 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 8 | assert im[1] 9 | end 10 | 11 | test "Access behaviour for Vix.Vips.Image for a range retrieves a range of bands" do 12 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 13 | assert im[1..2] 14 | end 15 | 16 | test "Access behaviour for Vix.Vips.Image with invalid integer band" do 17 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 18 | 19 | assert_raise ArgumentError, "Invalid band requested. Found 5", fn -> 20 | im[5] 21 | end 22 | end 23 | 24 | test "Access behaviour for Vix.Vips.Image with invalid range band" do 25 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 26 | 27 | assert_raise Image.Error, "Invalid band range 1..5", fn -> 28 | im[1..5] 29 | end 30 | 31 | assert_raise ArgumentError, "Invalid range -2..2", fn -> 32 | im[-5..-1] 33 | end 34 | end 35 | 36 | test "Access behaviour for Vix.Vips.Image with slicing and integer values" do 37 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 38 | assert shape(im[[]]) == shape(im) 39 | assert shape(im[[1]]) == {1, 389, 3} 40 | assert shape(im[[0..2, 5, -1]]) == {3, 1, 1} 41 | end 42 | 43 | test "Access behaviour for Vix.Vips.Image with slicing and range values" do 44 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 45 | assert shape(im[[]]) == shape(im) 46 | assert shape(im[[0..2]]) == {3, 389, 3} 47 | assert shape(im[[0..2, 0..3]]) == {3, 4, 3} 48 | assert shape(im[[0..2, 0..2, 0..1]]) == {3, 3, 2} 49 | end 50 | 51 | test "Access behaviour for Vix.Vips.Image with slicing and negative ranges" do 52 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 53 | assert shape(im[[-3..-1]]) == {3, 389, 3} 54 | assert shape(im[[-3..-1, -3..-1, -2..-1]]) == {3, 3, 2} 55 | end 56 | 57 | test "Access behaviour for Vix.Vips.Image with invalid argument" do 58 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 59 | 60 | assert_raise ArgumentError, 61 | "Argument must be list of integers or ranges or keyword list", 62 | fn -> im[[:foo]] end 63 | 64 | assert_raise ArgumentError, 65 | "Argument must be list of integers or ranges or keyword list", 66 | fn -> im[[nil, 1]] end 67 | end 68 | 69 | test "Access behaviour with invalid dimensions" do 70 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 71 | 72 | # Negative indices can't include 0 since that's a wrap-around 73 | assert_raise ArgumentError, "Invalid range -3..0", fn -> 74 | im[[-3..0]] 75 | end 76 | 77 | # Index larger than the image 78 | assert_raise ArgumentError, "Invalid range 0..1000", fn -> 79 | im[[0..1_000]] 80 | end 81 | end 82 | 83 | test "Access behaviour for Vix.Vips.Image with slicing and keyword list" do 84 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 85 | assert shape(im[[]]) == {518, 389, 3} 86 | assert shape(im[[height: 0..10]]) == {518, 11, 3} 87 | assert shape(im[[band: 2, height: 0..10]]) == {518, 11, 1} 88 | assert shape(im[[band: -2..-1]]) == {518, 389, 2} 89 | end 90 | 91 | # We can't use the 1..3//1 syntax since it fails on older 92 | # Elixir. So we detect when the `Range.t` has a `:step` and then 93 | # use Map.put/3 to place the expected value 94 | 95 | if range_has_step() do 96 | test "Access behaviour for Vix.Vips.Image with slicing and mixed positive/negative ranges" do 97 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 98 | assert shape(im[[Map.put(0..-1//-1, :step, 1)]]) == {518, 389, 3} 99 | 100 | im = 101 | im[ 102 | [Map.put(0..-1//-1, :step, 1), Map.put(1..-1//-1, :step, 1), Map.put(-2..-1, :step, 1)] 103 | ] 104 | 105 | assert shape(im) == {518, 388, 2} 106 | end 107 | 108 | test "Access behaviour with invalid dimensions and invalid step" do 109 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 110 | 111 | # Step != 1 112 | assert_raise ArgumentError, "Range arguments must have a step of 1. Found 0..-3//2", fn -> 113 | im[[Map.put(0..-3//-1, :step, 2)]] 114 | end 115 | end 116 | 117 | test "Access behaviour with invalid dimensions when ranges have steps" do 118 | {:ok, im} = Image.new_from_file(img_path("puppies.jpg")) 119 | 120 | # Index not increasing 121 | assert_raise ArgumentError, "Range arguments must have a step of 1. Found 0..-3//-1", fn -> 122 | im[[0..-3//-1]] 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "briefly": {:hex, :briefly, "0.5.1", "ee10d48da7f79ed2aebdc3e536d5f9a0c3e36ff76c0ad0d4254653a152b13a8a", [:mix], [], "hexpm", "bd684aa92ad8b7b4e0d92c31200993c4bc1469fc68cd6d5f15144041bd15cb57"}, 3 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 4 | "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, 5 | "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, 6 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 7 | "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, 8 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 9 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 10 | "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, 11 | "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, 12 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 13 | "fss": {:hex, :fss, "0.1.1", "9db2344dbbb5d555ce442ac7c2f82dd975b605b50d169314a20f08ed21e08642", [:mix], [], "hexpm", "78ad5955c7919c3764065b21144913df7515d52e228c09427a004afe9c1a16b0"}, 14 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 15 | "kino": {:hex, :kino, "0.14.2", "46c5da03f2d62dc119ec5e1c1493f409f08998eac26015ecdfae322ffff46d76", [:mix], [{:fss, "~> 0.1.0", [hex: :fss, repo: "hexpm", optional: false]}, {:nx, "~> 0.1", [hex: :nx, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "f54924dd0800ee8b291fe437f942889e90309eb3541739578476f53c1d79c968"}, 16 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 17 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 18 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 19 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 20 | "table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"}, 21 | } 22 | -------------------------------------------------------------------------------- /c_src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef VIX_UTILS_H 2 | #define VIX_UTILS_H 3 | 4 | #include "erl_nif.h" 5 | #include 6 | #include 7 | 8 | /* #define DEBUG */ 9 | 10 | #define vix_log(...) \ 11 | do { \ 12 | enif_fprintf(stderr, "%s:%d\t(fn \"%s\") - ", __FILE__, __LINE__, \ 13 | __func__); \ 14 | enif_fprintf(stderr, __VA_ARGS__); \ 15 | enif_fprintf(stderr, "\n"); \ 16 | } while (0) 17 | 18 | #ifdef DEBUG 19 | #define debug(...) vix_log(__VA_ARGS__) 20 | #define start_timing() ErlNifTime __start = enif_monotonic_time(ERL_NIF_USEC) 21 | #define elapsed_microseconds() (enif_monotonic_time(ERL_NIF_USEC) - __start) 22 | #else 23 | #define debug(...) 24 | #define start_timing() 25 | #define elapsed_microseconds() 0 26 | #endif 27 | 28 | extern const guint VIX_LOG_LEVEL_ERROR; 29 | extern guint VIX_LOG_LEVEL; 30 | 31 | #define error(...) \ 32 | do { \ 33 | if (VIX_LOG_LEVEL == VIX_LOG_LEVEL_ERROR) { \ 34 | vix_log(__VA_ARGS__); \ 35 | } \ 36 | } while (0) 37 | 38 | #define ASSERT_ARGC(argc, count) \ 39 | if (argc != count) { \ 40 | error("number of arguments must be %d", count); \ 41 | return enif_make_badarg(env); \ 42 | } 43 | 44 | // Using macro to preserve file and line number metadata in the error log 45 | #define SET_ERROR_RESULT(env, reason, res) \ 46 | do { \ 47 | res.is_success = false; \ 48 | res.result = make_binary(env, reason); \ 49 | error(reason); \ 50 | } while (0) 51 | 52 | #define SET_RESULT_FROM_VIPS_ERROR(env, label, res) \ 53 | do { \ 54 | res.is_success = false; \ 55 | res.result = enif_make_tuple2(env, make_binary(env, label), \ 56 | make_binary(env, vips_error_buffer())); \ 57 | error("%s: %s", label, vips_error_buffer()); \ 58 | vips_error_clear(); \ 59 | } while (0) 60 | 61 | #define SET_VIX_RESULT(res, term) \ 62 | do { \ 63 | res.is_success = true; \ 64 | res.result = term; \ 65 | } while (0) 66 | 67 | typedef struct _VixResult { 68 | bool is_success; 69 | ERL_NIF_TERM result; 70 | } VixResult; 71 | 72 | /* size of the data is not really needed. but can be useful for debugging */ 73 | typedef struct _VixBinaryResource { 74 | void *data; 75 | size_t size; 76 | } VixBinaryResource; 77 | 78 | extern ErlNifResourceType *VIX_BINARY_RT; 79 | 80 | extern int MAX_G_TYPE_NAME_LENGTH; 81 | 82 | extern ERL_NIF_TERM ATOM_OK; 83 | 84 | extern ERL_NIF_TERM ATOM_ERROR; 85 | 86 | extern ERL_NIF_TERM ATOM_NIL; 87 | 88 | extern ERL_NIF_TERM ATOM_TRUE; 89 | 90 | extern ERL_NIF_TERM ATOM_FALSE; 91 | 92 | extern ERL_NIF_TERM ATOM_NULL_VALUE; 93 | 94 | extern ERL_NIF_TERM ATOM_UNDEFINED; 95 | 96 | extern ERL_NIF_TERM ATOM_EAGAIN; 97 | 98 | extern const int VIX_FD_CLOSED; 99 | 100 | ERL_NIF_TERM raise_exception(ErlNifEnv *env, const char *msg); 101 | 102 | ERL_NIF_TERM raise_badarg(ErlNifEnv *env, const char *reason); 103 | 104 | ERL_NIF_TERM make_ok(ErlNifEnv *env, ERL_NIF_TERM term); 105 | 106 | ERL_NIF_TERM make_error(ErlNifEnv *env, const char *reason); 107 | 108 | ERL_NIF_TERM make_error_term(ErlNifEnv *env, ERL_NIF_TERM term); 109 | 110 | ERL_NIF_TERM make_atom(ErlNifEnv *env, const char *name); 111 | 112 | ERL_NIF_TERM make_binary(ErlNifEnv *env, const char *str); 113 | 114 | bool get_binary(ErlNifEnv *env, ERL_NIF_TERM bin_term, char *str, size_t size); 115 | 116 | VixResult vix_result(ERL_NIF_TERM term); 117 | 118 | int utils_init(ErlNifEnv *env, const char *log_level); 119 | 120 | int close_fd(int *fd); 121 | 122 | void notify_consumed_timeslice(ErlNifEnv *env, ErlNifTime start, 123 | ErlNifTime stop); 124 | 125 | ERL_NIF_TERM to_binary_term(ErlNifEnv *env, void *data, size_t size); 126 | 127 | void send_to_janitor(ErlNifEnv *env, ERL_NIF_TERM label, 128 | ERL_NIF_TERM resource_term); 129 | 130 | #endif 131 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.35.0" 5 | @scm_url "https://github.com/akash-akya/vix" 6 | 7 | def project do 8 | [ 9 | app: :vix, 10 | version: @version, 11 | elixir: "~> 1.12", 12 | start_permanent: Mix.env() == :prod, 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | compilers: [:elixir_make] ++ Mix.compilers(), 15 | make_targets: ["all"], 16 | make_clean: ["clean"], 17 | deps: deps(), 18 | aliases: aliases(), 19 | 20 | # elixir_make config 21 | make_precompiler: make_precompiler(), 22 | make_precompiler_url: "#{@scm_url}/releases/download/v#{@version}/@{artefact_filename}", 23 | make_precompiler_priv_paths: [ 24 | "vix.*", 25 | "precompiled_libvips/lib/libvips.dylib", 26 | "precompiled_libvips/lib/libvips.*.dylib", 27 | "precompiled_libvips/lib/libvips.so", 28 | "precompiled_libvips/lib/libvips.so.*", 29 | "precompiled_libvips/lib/*.dll", 30 | "precompiled_libvips/lib/*.lib" 31 | ], 32 | make_precompiler_nif_versions: [ 33 | versions: ["2.16", "2.17"] 34 | ], 35 | make_force_build: make_force_build(), 36 | cc_precompiler: [ 37 | cleanup: "clean_precompiled_libvips", 38 | allow_missing_compiler: true, 39 | compilers: %{ 40 | {:unix, :linux} => %{ 41 | "x86_64-linux-gnu" => "x86_64-linux-gnu-", 42 | "aarch64-linux-gnu" => "aarch64-linux-gnu-", 43 | "armv7l-linux-gnueabihf" => "arm-linux-gnueabihf-", 44 | "arm-linux-gnueabihf" => "arm-linux-gnueabihf-", 45 | "x86_64-linux-musl" => "x86_64-linux-musl-", 46 | "aarch64-linux-musl" => "aarch64-linux-musl-" 47 | }, 48 | {:unix, :darwin} => %{ 49 | "x86_64-apple-darwin" => { 50 | "gcc", 51 | "g++", 52 | "<%= cc %> -arch x86_64", 53 | "<%= cxx %> -arch x86_64" 54 | }, 55 | "aarch64-apple-darwin" => { 56 | "gcc", 57 | "g++", 58 | "<%= cc %> -arch arm64", 59 | "<%= cxx %> -arch arm64" 60 | } 61 | }, 62 | {:win32, :nt} => %{} 63 | } 64 | ], 65 | 66 | # Coverage 67 | test_coverage: [tool: ExCoveralls], 68 | preferred_cli_env: [ 69 | coveralls: :test, 70 | "coveralls.detail": :test, 71 | "coveralls.post": :test, 72 | "coveralls.html": :test 73 | ], 74 | 75 | # Package 76 | package: package(), 77 | description: description(), 78 | 79 | # Docs 80 | source_url: @scm_url, 81 | homepage_url: @scm_url, 82 | docs: [ 83 | main: "readme", 84 | source_ref: "v#{@version}", 85 | extras: [ 86 | "README.md", 87 | "LICENSE", 88 | "livebooks/introduction.livemd", 89 | "livebooks/picture-language.livemd", 90 | "livebooks/rainbow.livemd", 91 | "livebooks/auto_correct_rotation.livemd" 92 | ], 93 | groups_for_extras: [ 94 | Livebooks: Path.wildcard("livebooks/*.livemd") 95 | ] 96 | ] 97 | ] 98 | end 99 | 100 | def application do 101 | [ 102 | extra_applications: [:logger, :public_key, :ssl, :inets] 103 | ] 104 | end 105 | 106 | defp description do 107 | "NIF based bindings for libvips" 108 | end 109 | 110 | defp package do 111 | [ 112 | maintainers: ["Akash Hiremath"], 113 | licenses: ["MIT"], 114 | files: 115 | ~w(lib build_scripts checksum.exs mix.exs README.md LICENSE Makefile c_src/Makefile c_src/*.{h,c} c_src/g_object/*.{h,c}), 116 | links: %{ 117 | GitHub: @scm_url, 118 | libvips: "https://libvips.github.io/libvips" 119 | } 120 | ] 121 | end 122 | 123 | defp deps do 124 | maybe_kino() ++ 125 | [ 126 | {:elixir_make, "~> 0.8 or ~> 0.7.3", runtime: false}, 127 | {:cc_precompiler, "~> 0.2 or ~> 0.1.4", runtime: false}, 128 | 129 | # development & test 130 | {:credo, "~> 1.6", only: [:dev], runtime: false}, 131 | {:dialyxir, "~> 1.4", only: [:dev], runtime: false}, 132 | {:ex_doc, ">= 0.0.0", only: :dev}, 133 | {:excoveralls, "~> 0.15", only: :test}, 134 | {:briefly, "~> 0.5.0", only: :test} 135 | ] 136 | end 137 | 138 | defp maybe_kino do 139 | if Version.compare(System.version(), "1.14.0") in [:gt, :eq] do 140 | [{:kino, "~> 0.7", optional: true}] 141 | else 142 | [] 143 | end 144 | end 145 | 146 | defp elixirc_paths(:test), do: ["lib", "test/support"] 147 | defp elixirc_paths(_), do: ["lib"] 148 | 149 | defp aliases do 150 | [ 151 | deep_clean: "cmd make clean_precompiled_libvips", 152 | precompile: [ 153 | "deep_clean", 154 | "elixir_make.precompile" 155 | ] 156 | ] 157 | end 158 | 159 | defp make_precompiler do 160 | if compilation_mode() == "PLATFORM_PROVIDED_LIBVIPS" do 161 | nil 162 | else 163 | {:nif, CCPrecompiler} 164 | end 165 | end 166 | 167 | defp make_force_build, do: compilation_mode() == "PRECOMPILED_LIBVIPS" 168 | 169 | defp compilation_mode do 170 | (System.get_env("VIX_COMPILATION_MODE") || "PRECOMPILED_NIF_AND_LIBVIPS") 171 | |> String.upcase() 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /test/vix/operator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Vix.OperatorTest do 2 | use ExUnit.Case, async: true 3 | use Vix.Operator 4 | 5 | import Vix.Support.Images 6 | 7 | alias Vix.Vips.Image 8 | alias Vix.Vips.Operation 9 | 10 | doctest Vix.Operator 11 | 12 | describe "+/2" do 13 | test "when both arguments are image" do 14 | black = Operation.black!(10, 10, bands: 3) 15 | grey = Operation.linear!(black, [1], [125]) 16 | 17 | out = black + grey 18 | 19 | assert_images_equal(out, grey) 20 | end 21 | 22 | test "when first argument is image and second argument is list" do 23 | black = Operation.black!(10, 10, bands: 3) 24 | 25 | expected = Operation.linear!(black, [1], [0, 125, 0]) 26 | out = black + [0, 125, 0] 27 | 28 | assert_images_equal(out, expected) 29 | end 30 | 31 | test "when first argument is a list and second argument is image" do 32 | black = Operation.black!(10, 10, bands: 3) 33 | 34 | expected = Operation.linear!(black, [1], [0, 125, 0]) 35 | out = [0, 125, 0] + black 36 | 37 | assert_images_equal(out, expected) 38 | end 39 | 40 | test "when argument is invalid" do 41 | black = Operation.black!(10, 10, bands: 3) 42 | 43 | assert_raise ArgumentError, "list elements must be a number, got: [nil]", fn -> 44 | black + [nil] 45 | end 46 | end 47 | 48 | test "when both arguments are numbers" do 49 | assert 1 + 1 == 2 50 | end 51 | end 52 | 53 | describe "*/2" do 54 | test "when both arguments are image" do 55 | black = Operation.black!(10, 10, bands: 3) 56 | img = Operation.linear!(black, [1], [2]) 57 | 58 | out = img * img 59 | 60 | expected = Operation.linear!(black, [1], [4]) 61 | assert_images_equal(out, expected) 62 | end 63 | 64 | test "when first argument is image and second argument is list" do 65 | black = Operation.black!(10, 10, bands: 3) 66 | img = Operation.linear!(black, [1], [2]) 67 | 68 | out = img * [1, 2, 1] 69 | 70 | # [2, 2, 2] * [1, 2, 1] = [2, 4, 2] 71 | expected = Operation.linear!(black, [1], [2, 4, 2]) 72 | assert_images_equal(out, expected) 73 | end 74 | 75 | test "when first argument is a list and second argument is image" do 76 | black = Operation.black!(10, 10, bands: 3) 77 | img = Operation.linear!(black, [1], [2]) 78 | 79 | out = [1, 2, 1] * img 80 | 81 | # [1, 2, 1] * [2, 2, 2] = [2, 4, 2] 82 | expected = Operation.linear!(black, [1], [2, 4, 2]) 83 | assert_images_equal(out, expected) 84 | end 85 | 86 | test "when both arguments are numbers" do 87 | assert 1 * 2 == 2 88 | end 89 | end 90 | 91 | describe "-/2" do 92 | test "when both arguments are image" do 93 | black = Operation.black!(10, 10, bands: 3) 94 | grey = Operation.linear!(black, [1], [125]) 95 | 96 | # credo:disable-for-next-line 97 | out = grey - grey 98 | 99 | assert_images_equal(out, black) 100 | end 101 | 102 | test "when first argument is image and second argument is list" do 103 | black = Operation.black!(10, 10, bands: 3) 104 | grey = Operation.linear!(black, [1], [125]) 105 | 106 | expected = Operation.linear!(grey, [1], [0, -125, 0]) 107 | out = grey - [0, 125, 0] 108 | 109 | assert_images_equal(out, expected) 110 | end 111 | 112 | test "when first argument is a list and second argument is image" do 113 | black = Operation.black!(10, 10, bands: 3) 114 | grey = Operation.linear!(black, [1], [125]) 115 | 116 | expected = Operation.linear!(grey, [-1], [255, 255, 255]) 117 | out = [255, 255, 255] - grey 118 | 119 | assert_images_equal(out, expected) 120 | end 121 | 122 | test "when both arguments are numbers" do 123 | assert 1 - 1 == 0 124 | end 125 | end 126 | 127 | describe "//2" do 128 | test "when both arguments are image" do 129 | black = Operation.black!(10, 10, bands: 3) 130 | grey = Operation.linear!(black, [1], [4]) 131 | 132 | # credo:disable-for-next-line 133 | out = grey / grey 134 | 135 | expected = Operation.linear!(black, [1], [1]) 136 | assert_images_equal(out, expected) 137 | end 138 | 139 | test "when first argument is image and second argument is list" do 140 | black = Operation.black!(10, 10, bands: 3) 141 | img = Operation.linear!(black, [1], [4]) 142 | 143 | out = img / [1, 2, 1] 144 | 145 | # [4, 4, 4] / [1, 2, 1] = [4, 2, 4] 146 | expected = Operation.linear!(black, [1], [4, 2, 4]) 147 | assert_images_equal(out, expected) 148 | end 149 | 150 | test "when first argument is a list and second argument is image" do 151 | black = Operation.black!(10, 10, bands: 3) 152 | img = Operation.linear!(black, [1], [2]) 153 | 154 | out = [4, 8, 4] / img 155 | 156 | # [4, 8, 4] / [2, 2, 2] = [2, 4, 2] 157 | expected = Operation.linear!(black, [1], [2, 4, 2]) 158 | assert_images_equal(out, expected) 159 | end 160 | 161 | test "when both arguments are numbers" do 162 | assert 4 / 2 == 2 163 | end 164 | end 165 | 166 | describe "**/2" do 167 | test "when both arguments are image" do 168 | black = Operation.black!(10, 10, bands: 3) 169 | img = Operation.linear!(black, [1], [4]) 170 | 171 | out = img ** img 172 | 173 | expected_pow = 4 ** 4 174 | expected = Operation.linear!(black, [1], [expected_pow]) 175 | assert_images_equal(out, expected) 176 | end 177 | 178 | test "when first argument is image and second argument is list" do 179 | black = Operation.black!(10, 10, bands: 3) 180 | img = Operation.linear!(black, [1], [3]) 181 | 182 | out = img ** [1, 2, 1] 183 | 184 | # [3, 3, 3] ** [1, 2, 1] = [3, 9, 3] 185 | expected = Operation.linear!(black, [1], [3, 9, 3]) 186 | assert_images_equal(out, expected) 187 | end 188 | 189 | test "when first argument is a list and second argument is image" do 190 | black = Operation.black!(10, 10, bands: 3) 191 | img = Operation.linear!(black, [1], [3]) 192 | 193 | out = [1, 2, 1] ** img 194 | 195 | # [1, 2, 1] ** [3, 3, 3] = [1, 8, 1] 196 | expected = Operation.linear!(black, [1], [1, 8, 1]) 197 | assert_images_equal(out, expected) 198 | end 199 | 200 | test "when both arguments are numbers" do 201 | assert 3 ** 2 == 9 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /c_src/Makefile: -------------------------------------------------------------------------------- 1 | # Force POSIX-compatible shell 2 | SHELL = /bin/sh 3 | 4 | # Build configuration 5 | MIX_APP_PATH ?= .. 6 | PREFIX = $(MIX_APP_PATH)/priv 7 | VIX = $(PREFIX)/vix.so 8 | 9 | # Compilation mode configuration 10 | VIX_COMPILATION_MODE ?= PRECOMPILED_NIF_AND_LIBVIPS 11 | VIX_COMPILATION_MODE := $(shell echo "$(VIX_COMPILATION_MODE)" | tr '[:lower:]' '[:upper:]') 12 | 13 | # Build tools 14 | CC ?= gcc 15 | RM ?= rm -f 16 | MKDIR_P ?= mkdir -p 17 | 18 | # System detection and platform-specific configuration 19 | UNAME_SYS := $(shell uname -s 2>/dev/null || echo Unknown) 20 | UNAME_ARCH := $(shell uname -m 2>/dev/null || echo Unknown) 21 | 22 | # Base compiler flags 23 | BASE_CFLAGS = -O2 -Wall -Werror -Wextra -Wno-unused-parameter -Wmissing-prototypes -std=c11 24 | 25 | # Platform-specific flags 26 | ifeq ($(UNAME_SYS), Darwin) 27 | CFLAGS += $(BASE_CFLAGS) 28 | LDFLAGS += -flat_namespace -undefined suppress 29 | else ifeq ($(UNAME_SYS), Linux) 30 | CFLAGS += $(BASE_CFLAGS) 31 | endif 32 | 33 | # flags that are common to all platform 34 | CFLAGS += -D_POSIX_C_SOURCE=200809L -fPIC 35 | 36 | # Erlang/OTP includes and libraries 37 | CFLAGS += -I "$(ERTS_INCLUDE_DIR)" -I "$(ERL_INTERFACE_INCLUDE_DIR)" 38 | LDLIBS += -L "$(ERL_INTERFACE_LIB_DIR)" 39 | LDFLAGS += -shared 40 | 41 | ifeq ($(VIX_COMPILATION_MODE), PLATFORM_PROVIDED_LIBVIPS) 42 | VIPS_CFLAGS := $(shell pkg-config vips --cflags 2>/dev/null) 43 | VIPS_LIBS := $(shell pkg-config vips --libs 2>/dev/null) 44 | CFLAGS += $(VIPS_CFLAGS) 45 | LDLIBS += $(VIPS_LIBS) 46 | PKG_CONFIG_CHECK = check-pkg-config 47 | else 48 | PKG_CONFIG_CHECK = 49 | PRECOMPILED_LIBVIPS_PATH = $(PREFIX)/precompiled_libvips 50 | 51 | ifeq ($(VIX_COMPILATION_MODE), PRECOMPILED_LIBVIPS) 52 | # force fetching libvips if previously configured to use PRECOMPILED_NIF_AND_LIBVIPS 53 | # since the precompiled vix does not bundle `include` 54 | PRECOMPILED_LIBVIPS_PREREQUISITE = $(PRECOMPILED_LIBVIPS_PATH)/include 55 | else 56 | PRECOMPILED_LIBVIPS_PREREQUISITE = $(PRECOMPILED_LIBVIPS_PATH) 57 | endif 58 | 59 | CFLAGS += -pthread 60 | CFLAGS += -I "$(PRECOMPILED_LIBVIPS_PATH)/include" 61 | CFLAGS += -I "$(PRECOMPILED_LIBVIPS_PATH)/lib/glib-2.0/include" 62 | CFLAGS += -I "$(PRECOMPILED_LIBVIPS_PATH)/include/glib-2.0" 63 | LDLIBS += -L "$(PRECOMPILED_LIBVIPS_PATH)/lib" 64 | 65 | ifeq ($(UNAME_SYS), Darwin) 66 | LDFLAGS += -Wl,-rpath,@loader_path/precompiled_libvips/lib 67 | LDLIBS += -lvips.42 68 | else ifeq ($(UNAME_SYS), Linux) 69 | LDFLAGS += -Wl,-s -Wl,--disable-new-dtags -Wl,-rpath='$$ORIGIN/precompiled_libvips/lib' 70 | LDLIBS += -l:libvips.so.42 71 | endif 72 | endif 73 | 74 | # Verbosity control 75 | V ?= 0 76 | ifeq ($(V),0) 77 | Q = @ 78 | SAY = @echo 79 | else 80 | Q = 81 | SAY = @\# 82 | endif 83 | 84 | c_verbose_0 = @echo " CC " $(/dev/null 2>&1; then \ 130 | echo "Error: pkg-config not found but required for PLATFORM_PROVIDED_LIBVIPS"; \ 131 | exit 1; \ 132 | fi 133 | @if ! pkg-config --exists vips; then \ 134 | echo "Error: vips not found via pkg-config"; \ 135 | echo "Please install libvips development headers"; \ 136 | exit 1; \ 137 | fi 138 | 139 | # Create output directory 140 | $(PREFIX): 141 | $(Q)$(MKDIR_P) "$@" 142 | 143 | $(OBJ): Makefile $(PRECOMPILED_LIBVIPS_PREREQUISITE) 144 | 145 | # Dependency generation 146 | %.d: %.c 147 | $(Q)$(CC) $(CFLAGS) -MM -MT $(@:.d=.o) $< > $@ 148 | 149 | # Include dependency files if they exist 150 | -include $(DEP) 151 | 152 | # Object file compilation with dependency tracking 153 | %.o: %.c 154 | $(c_verbose)$(CC) -c $(CFLAGS) -MMD -MP -o $@ $< 155 | 156 | # Final linking 157 | $(VIX): $(PREFIX) $(OBJ) 158 | $(link_verbose)$(CC) $(OBJ) -o $@ $(LDFLAGS) $(LDLIBS) 159 | 160 | # Precompiled libvips setup 161 | $(PRECOMPILED_LIBVIPS_PREREQUISITE): 162 | $(SAY) "Setting up precompiled libvips..." 163 | $(Q) elixir ../build_scripts/precompiler.exs "$(PREFIX)" 164 | 165 | # Clean targets 166 | clean: 167 | $(SAY) "Cleaning build artifacts..." 168 | $(Q)$(RM) $(VIX) $(OBJ) $(DEP) 169 | 170 | clean_precompiled_libvips: clean 171 | $(SAY) "Cleaning precompiled libvips..." 172 | $(Q)$(RM) "$(PREFIX)"/libvips-*.tar.gz 173 | $(Q)$(RM) -r "$(PREFIX)/precompiled_libvips" 174 | 175 | # Help target 176 | help: 177 | @echo "Available targets:" 178 | @echo " all - Build the NIF library" 179 | @echo " clean - Clean build artifacts" 180 | @echo " clean_precompiled_libvips - Clean precompiled libvips" 181 | @echo " check-env - Validate build environment" 182 | @echo " help - Show this help" 183 | @echo "" 184 | @echo "Build configuration:" 185 | @echo " VIX_COMPILATION_MODE: $(VIX_COMPILATION_MODE)" 186 | @echo " System: $(UNAME_SYS) $(UNAME_ARCH)" 187 | @echo " CC: $(CC)" 188 | @echo "" 189 | @echo "Set V=1 for verbose output" 190 | 191 | # Debug target 192 | debug: 193 | @echo "=== Build Configuration ===" 194 | @echo "VIX_COMPILATION_MODE: $(VIX_COMPILATION_MODE)" 195 | @echo "UNAME_SYS: $(UNAME_SYS)" 196 | @echo "UNAME_ARCH: $(UNAME_ARCH)" 197 | @echo "CC: $(CC)" 198 | @echo "CFLAGS: $(CFLAGS)" 199 | @echo "LDFLAGS: $(LDFLAGS)" 200 | @echo "LDLIBS: $(LDLIBS)" 201 | @echo "SRC: $(SRC)" 202 | @echo "OBJ: $(OBJ)" 203 | @echo "PREFIX: $(PREFIX)" 204 | @echo "VIX: $(VIX)" 205 | 206 | .PHONY: all clean clean_precompiled_libvips calling_from_make install check-env check-pkg-config help debug 207 | -------------------------------------------------------------------------------- /lib/vix/vips/mutable_image.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.MutableImage do 2 | defstruct [:pid] 3 | 4 | alias __MODULE__ 5 | alias Vix.Type 6 | alias Vix.Vips.Image 7 | 8 | @moduledoc """ 9 | Vips Mutable Image 10 | 11 | See `Vix.Vips.Image.mutate/2` 12 | """ 13 | 14 | alias Vix.Nif 15 | 16 | @behaviour Type 17 | 18 | @typedoc """ 19 | Represents a mutable instance of VipsImage 20 | """ 21 | 22 | @type t() :: %MutableImage{pid: pid} 23 | 24 | @impl Type 25 | def typespec do 26 | quote do 27 | unquote(__MODULE__).t() 28 | end 29 | end 30 | 31 | @impl Type 32 | def default(nil) do 33 | raise "default/1 for Vix.Vips.MutableImage is not supported" 34 | end 35 | 36 | @impl Type 37 | def to_nif_term(%Image{} = image, data) do 38 | Image.to_nif_term(image, data) 39 | end 40 | 41 | def to_nif_term(%MutableImage{}, _data) do 42 | raise "to_nif_term/2 for Vix.Vips.MutableImage is not supported" 43 | end 44 | 45 | @impl Type 46 | def to_erl_term(_term) do 47 | raise "to_erl_term/1 for Vix.Vips.MutableImage is not supported" 48 | end 49 | 50 | # Create mutable image 51 | @doc false 52 | @spec new(Vix.Vips.Image.t()) :: {:ok, __MODULE__.t()} | {:error, term()} 53 | def new(%Image{} = image) do 54 | GenServer.start_link(__MODULE__, image) 55 | |> wrap_type() 56 | end 57 | 58 | @doc """ 59 | Return the number of bands of a mutable image. 60 | """ 61 | def bands(%MutableImage{pid: pid}) do 62 | GenServer.call(pid, :bands) 63 | end 64 | 65 | @doc """ 66 | Return the width of a mutable image. 67 | """ 68 | def width(%MutableImage{pid: pid}) do 69 | GenServer.call(pid, :width) 70 | end 71 | 72 | @doc """ 73 | Return the height of a mutable image. 74 | """ 75 | def height(%MutableImage{pid: pid}) do 76 | GenServer.call(pid, :height) 77 | end 78 | 79 | @doc """ 80 | Return a boolean indicating if a mutable image 81 | has an alpha band. 82 | """ 83 | def has_alpha?(%MutableImage{pid: pid}) do 84 | GenServer.call(pid, :has_alpha?) 85 | end 86 | 87 | @doc """ 88 | Return the shape of the image as 89 | `{width, height, bands}`. 90 | """ 91 | def shape(%MutableImage{pid: pid}) do 92 | GenServer.call(pid, :shape) 93 | end 94 | 95 | @doc """ 96 | Set the value of existing metadata item on an image. Value is converted to match existing value GType 97 | """ 98 | @spec update(__MODULE__.t(), String.t(), term()) :: :ok | {:error, term()} 99 | def update(%MutableImage{pid: pid}, name, value) do 100 | GenServer.call(pid, {:update, name, value}) 101 | end 102 | 103 | @supported_gtype ~w(gint guint gdouble gboolean gchararray VipsArrayInt VipsArrayDouble VipsArrayImage VipsRefString VipsBlob VipsImage VipsInterpolate)a 104 | 105 | @doc """ 106 | Create a metadata item on an image of the specified type. 107 | Vix converts value to specified GType 108 | 109 | Supported GTypes 110 | #{Enum.map(@supported_gtype, fn type -> " * `#{inspect(type)}`\n" end)} 111 | """ 112 | @spec set(__MODULE__.t(), String.t(), atom(), term()) :: :ok | {:error, term()} 113 | def set(%MutableImage{pid: pid}, name, type, value) do 114 | if type in @supported_gtype do 115 | type = to_string(type) 116 | GenServer.call(pid, {:set, name, type, cast_value(type, value)}) 117 | else 118 | {:error, "invalid gtype. Supported types are #{inspect(@supported_gtype)}"} 119 | end 120 | end 121 | 122 | @doc """ 123 | Remove a metadata item from an image. 124 | """ 125 | @spec remove(__MODULE__.t(), String.t()) :: :ok | {:error, term()} 126 | def remove(%MutableImage{pid: pid}, name) do 127 | GenServer.call(pid, {:remove, name}) 128 | end 129 | 130 | @doc """ 131 | Returns metadata from the image 132 | """ 133 | @spec get(__MODULE__.t(), String.t()) :: {:ok, term()} | {:error, term()} 134 | def get(%MutableImage{pid: pid}, name) do 135 | GenServer.call(pid, {:get, name}) 136 | end 137 | 138 | @doc false 139 | def to_image(%MutableImage{pid: pid}) do 140 | GenServer.call(pid, :to_image) 141 | end 142 | 143 | @doc false 144 | def stop(%MutableImage{pid: pid}) do 145 | GenServer.stop(pid, :normal) 146 | end 147 | 148 | use GenServer 149 | 150 | @impl true 151 | def init(image) do 152 | case Image.copy_memory(image) do 153 | {:ok, copy} -> {:ok, %{image: copy}} 154 | {:error, error} -> {:stop, error} 155 | end 156 | end 157 | 158 | @impl true 159 | def handle_call({:update, name, value}, _from, %{image: image} = state) do 160 | {:reply, Nif.nif_image_update_metadata(image.ref, name, value), state} 161 | end 162 | 163 | @impl true 164 | def handle_call({:set, name, type, value}, _from, %{image: image} = state) do 165 | {:reply, Nif.nif_image_set_metadata(image.ref, name, type, value), state} 166 | end 167 | 168 | @impl true 169 | def handle_call({:remove, name}, _from, %{image: image} = state) do 170 | {:reply, Nif.nif_image_remove_metadata(image.ref, name), state} 171 | end 172 | 173 | @impl true 174 | def handle_call({:get, name}, _from, %{image: image} = state) do 175 | {:reply, Image.header_value(image, name), state} 176 | end 177 | 178 | @impl true 179 | def handle_call(:to_image, _from, %{image: image} = state) do 180 | {:reply, Image.copy_memory(image), state} 181 | end 182 | 183 | @impl true 184 | def handle_call({:operation, callback}, _from, %{image: image} = state) do 185 | {:reply, callback.(image), state} 186 | end 187 | 188 | @impl true 189 | def handle_call(:width, _from, %{image: image} = state) do 190 | {:reply, {:ok, Image.width(image)}, state} 191 | end 192 | 193 | @impl true 194 | def handle_call(:height, _from, %{image: image} = state) do 195 | {:reply, {:ok, Image.height(image)}, state} 196 | end 197 | 198 | @impl true 199 | def handle_call(:bands, _from, %{image: image} = state) do 200 | {:reply, {:ok, Image.bands(image)}, state} 201 | end 202 | 203 | @impl true 204 | def handle_call(:has_alpha?, _from, %{image: image} = state) do 205 | {:reply, {:ok, Image.has_alpha?(image)}, state} 206 | end 207 | 208 | @impl true 209 | def handle_call(:shape, _from, %{image: image} = state) do 210 | width = Image.width(image) 211 | height = Image.height(image) 212 | bands = Image.bands(image) 213 | 214 | {:reply, {:ok, {width, height, bands}}, state} 215 | end 216 | 217 | defp wrap_type({:ok, pid}), do: {:ok, %MutableImage{pid: pid}} 218 | defp wrap_type(value), do: value 219 | 220 | defp cast_value(type, value) do 221 | Vix.Type.to_nif_term(type, value, nil) 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vix 2 | 3 | [![CI](https://github.com/akash-akya/vix/actions/workflows/ci.yaml/badge.svg)](https://github.com/akash-akya/vix/actions/workflows/ci.yaml) 4 | [![Hex.pm](https://img.shields.io/hexpm/v/vix.svg)](https://hex.pm/packages/vix) 5 | [![docs](https://img.shields.io/badge/docs-hexpm-blue.svg)](https://hexdocs.pm/vix/) 6 | 7 | 8 | Blazing fast image processing for Elixir powered by [libvips](https://libvips.github.io/libvips/), the same engine that powers [sharp.js](https://github.com/lovell/sharp). 9 | 10 | ## Perfect For 11 | 12 | - Building image processing APIs and services 13 | - Generating thumbnails at scale 14 | - Image manipulation in web applications 15 | - Computer vision preprocessing 16 | - Processing large scientific/satellite images 17 | 18 | ## Features 19 | 20 | - **High Performance**: Uses libvips' demand-driven, horizontally threaded architecture 21 | - **Memory Efficient**: Processes images in chunks, perfect for large files 22 | - **Streaming Support**: Read/write images without loading them fully into memory 23 | - **Rich Ecosystem**: Zero-copy integration with [Nx](https://hex.pm/packages/nx) and [eVision](https://hex.pm/packages/evision) 24 | - **Zero Setup**: Pre-built binaries for MacOS and Linux platforms included. 25 | - **Auto-updating API**: New libvips features automatically available 26 | - **Comprehensive Documentation**: [Type specifications and documentation](https://hexdocs.pm/vix/Vix.Vips.Operation.html) for 300+ operations 27 | 28 | ## Quick Start 29 | 30 | ```elixir 31 | Mix.install([ 32 | {:vix, "~> 0.23"} 33 | ]) 34 | 35 | alias Vix.Vips.{Image, Operation} 36 | 37 | # Create a thumbnail and optimize for web 38 | {:ok, thumb} = Operation.thumbnail("profile.jpg", 300) 39 | :ok = Image.write_to_file(thumb, "thumbnail.jpg", Q: 90, strip: true, interlace: true) 40 | ``` 41 | 42 | [👉 Try in Livebook](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fakash-akya%2Fvix%2Fblob%2Fmaster%2Flivebooks%2Fintroduction.livemd) 43 | 44 | 45 | 46 | ## Common Operations 47 | 48 | ### Basic Processing 49 | ```elixir 50 | # Reading an image 51 | {:ok, img} = Image.new_from_file("profile.jpg") 52 | 53 | # Resize preserving aspect ratio 54 | {:ok, resized} = Operation.resize(img, 0.5) # 50% of original size 55 | 56 | # Crop a section 57 | {:ok, cropped} = Operation.crop(img, 100, 100, 500, 500) 58 | 59 | # Rotate with white background 60 | {:ok, rotated} = Operation.rotate(img, 90, background: [255, 255, 255]) 61 | 62 | # Smart thumbnail (preserves important features) 63 | {:ok, thumb} = Operation.thumbnail("large.jpg", 300, size: :VIPS_SIZE_DOWN, crop: :VIPS_INTERESTING_ATTENTION) 64 | ``` 65 | 66 | ### Web Optimization 67 | ```elixir 68 | # Convert to WebP with quality optimization 69 | :ok = Image.write_to_file(img, "output.webp", Q: 80, effort: 4) 70 | 71 | # Create progressive JPEG with metadata stripped 72 | :ok = Image.write_to_file(img, "output.jpg", interlace: true, strip: true, Q: 85) 73 | 74 | # Generate multiple formats 75 | :ok = Image.write_to_file(img, "photo.avif", Q: 60) 76 | :ok = Image.write_to_file(img, "photo.webp", Q: 80) 77 | :ok = Image.write_to_file(img, "photo.jpg", Q: 85) 78 | ``` 79 | 80 | ### Filters & Effects 81 | ```elixir 82 | # Blur 83 | {:ok, blurred} = Operation.gaussblur(img, 3.0) 84 | 85 | # Sharpen 86 | {:ok, sharp} = Operation.sharpen(img, sigma: 1.0) 87 | 88 | # Grayscale 89 | {:ok, bw} = Operation.colourspace(img, :VIPS_INTERPRETATION_B_W) 90 | ``` 91 | 92 | ### Advanced Usage 93 | ```elixir 94 | # Smart thumbnail preserving important features 95 | {:ok, thumb} = Operation.thumbnail( 96 | "large.jpg", 97 | 300, 98 | size: :VIPS_SIZE_DOWN, # only downsize, it will just copy if asked to upsize 99 | crop: :VIPS_INTERESTING_ATTENTION 100 | ) 101 | 102 | # Process image stream on the fly 103 | {:ok, image} = 104 | File.stream!("large_photo.jpg", [], 65_536) 105 | |> Image.new_from_enum() 106 | # use `image` for further operations... 107 | 108 | # Stream image to S3 109 | :ok = 110 | Image.write_to_stream(image, ".png") 111 | |> Stream.each(&upload_chunk_to_s3/1) 112 | |> Stream.run() 113 | ``` 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | ## Performance 122 | 123 | Libvips very fast and uses very little memory. See the detailed [benchmark](https://github.com/libvips/libvips/wiki/Speed-and-memory-use). Resizing an image is typically 4x-5x faster than using the quickest ImageMagick settings. It can also work with very large images without completely loading them to the memory. 124 | 125 | ## Installation 126 | 127 | Add Vix to your dependencies: 128 | 129 | ```elixir 130 | def deps do 131 | [ 132 | {:vix, "~> x.x.x"} 133 | ] 134 | end 135 | ``` 136 | 137 | That's it! Vix includes pre-built binaries for MacOS & Linux. 138 | 139 | ## Advanced Setup 140 | 141 | Want to use your system's libvips? Set before compilation: 142 | 143 | ```bash 144 | export VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS 145 | ``` 146 | 147 | See [libvips installation guide](https://www.libvips.org/install.html) for more details. 148 | 149 | 150 | ## Documentation & Resources 151 | 152 | - [Complete API Documentation](https://hexdocs.pm/vix/) 153 | - [Interactive Introduction (Livebook)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fakash-akya%2Fvix%2Fblob%2Fmaster%2Flivebooks%2Fintroduction.livemd) 154 | - [Creating Rainbow Effects (Livebook)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fakash-akya%2Fvix%2Fblob%2Fmaster%2Flivebooks%2Frainbow.livemd) 155 | - [Auto Document Rotation (Livebook)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fakash-akya%2Fvix%2Fblob%2Fmaster%2Flivebooks%2Fauto_correct_rotation.livemd) 156 | - [Picture Language from SICP (Livebook)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fakash-akya%2Fvix%2Fblob%2Fmaster%2Flivebooks%2Fpicture-language.livemd) 157 | 158 | ## FAQ 159 | 160 | ### Should I use Vix or Image? 161 | 162 | [Image](https://github.com/kipcole9/image) is a library which builds on top of Vix. 163 | 164 | - Use [Image](https://github.com/kipcole9/image) when you need: 165 | - A more Elixir-friendly API for common operations 166 | - Higher-level operations like Blurhash 167 | - Simple, chainable functions for common operations 168 | 169 | - Use Vix directly when you need: 170 | - Advanced VIPS features and fine-grained control 171 | - Complex image processing pipelines 172 | - Direct libvips performance and capabilities 173 | - Lesser dependencies 174 | 175 | ### What image formats are supported? 176 | Out of the box: JPEG, PNG, WEBP, TIFF, SVG, HEIF, GIF, and more. Need others? Just install libvips with the required libraries! 177 | 178 | ## License 179 | 180 | MIT License - see [LICENSE](LICENSE) for details. 181 | -------------------------------------------------------------------------------- /c_src/utils.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | ErlNifResourceType *VIX_BINARY_RT; 8 | 9 | int MAX_G_TYPE_NAME_LENGTH = 1024; 10 | 11 | const int VIX_FD_CLOSED = -1; 12 | 13 | ERL_NIF_TERM ATOM_OK; 14 | ERL_NIF_TERM ATOM_ERROR; 15 | ERL_NIF_TERM ATOM_NIL; 16 | ERL_NIF_TERM ATOM_TRUE; 17 | ERL_NIF_TERM ATOM_FALSE; 18 | ERL_NIF_TERM ATOM_NULL_VALUE; 19 | ERL_NIF_TERM ATOM_UNDEFINED; 20 | ERL_NIF_TERM ATOM_EAGAIN; 21 | 22 | /** 23 | * Name of the process responsible for cleanup, we send resources 24 | * requiring cleanup to this process. 25 | */ 26 | static ERL_NIF_TERM VIX_JANITOR_PROCESS_NAME; 27 | 28 | const guint VIX_LOG_LEVEL_NONE = 0; 29 | const guint VIX_LOG_LEVEL_WARNING = 1; 30 | const guint VIX_LOG_LEVEL_ERROR = 2; 31 | 32 | guint VIX_LOG_LEVEL = VIX_LOG_LEVEL_NONE; 33 | 34 | static void libvips_log_callback(char const *log_domain, 35 | GLogLevelFlags log_level, char const *message, 36 | void *enable) { 37 | enif_fprintf(stderr, "[libvips]: %s\n", message); 38 | } 39 | 40 | static void libvips_log_null_callback(char const *log_domain, 41 | GLogLevelFlags log_level, 42 | char const *message, void *enable) { 43 | // void 44 | } 45 | 46 | ERL_NIF_TERM make_ok(ErlNifEnv *env, ERL_NIF_TERM term) { 47 | return enif_make_tuple2(env, ATOM_OK, term); 48 | } 49 | 50 | ERL_NIF_TERM make_error(ErlNifEnv *env, const char *reason) { 51 | return enif_make_tuple2(env, ATOM_ERROR, make_binary(env, reason)); 52 | } 53 | 54 | ERL_NIF_TERM make_error_term(ErlNifEnv *env, ERL_NIF_TERM term) { 55 | return enif_make_tuple2(env, ATOM_ERROR, term); 56 | } 57 | 58 | ERL_NIF_TERM raise_exception(ErlNifEnv *env, const char *msg) { 59 | return enif_raise_exception(env, make_binary(env, msg)); 60 | } 61 | 62 | ERL_NIF_TERM raise_badarg(ErlNifEnv *env, const char *reason) { 63 | error("bad argument: %s", reason); 64 | return enif_make_badarg(env); 65 | } 66 | 67 | ERL_NIF_TERM make_atom(ErlNifEnv *env, const char *name) { 68 | ERL_NIF_TERM ret; 69 | if (enif_make_existing_atom(env, name, &ret, ERL_NIF_LATIN1)) { 70 | return ret; 71 | } 72 | return enif_make_atom(env, name); 73 | } 74 | 75 | ERL_NIF_TERM make_binary(ErlNifEnv *env, const char *str) { 76 | ERL_NIF_TERM bin; 77 | ssize_t length; 78 | unsigned char *temp; 79 | 80 | length = strlen(str); 81 | temp = enif_make_new_binary(env, length, &bin); 82 | memcpy(temp, str, length); 83 | 84 | return bin; 85 | } 86 | 87 | bool get_binary(ErlNifEnv *env, ERL_NIF_TERM bin_term, char *str, 88 | size_t dest_size) { 89 | ErlNifBinary bin; 90 | 91 | if (!enif_inspect_binary(env, bin_term, &bin)) { 92 | error("failed to get binary string from erl term"); 93 | return false; 94 | } 95 | 96 | if (bin.size >= dest_size) { 97 | error("destination size is smaller than required"); 98 | return false; 99 | } 100 | 101 | memcpy(str, bin.data, bin.size); 102 | str[bin.size] = '\0'; 103 | 104 | return true; 105 | } 106 | 107 | VixResult vix_result(ERL_NIF_TERM term) { 108 | return (VixResult){.is_success = true, .result = term}; 109 | } 110 | 111 | void send_to_janitor(ErlNifEnv *env, ERL_NIF_TERM label, 112 | ERL_NIF_TERM resource_term) { 113 | ErlNifPid pid; 114 | 115 | /* Currently there is no way to raise error when any of the 116 | condition fail. Realistically this should never fail */ 117 | if (!enif_whereis_pid(env, VIX_JANITOR_PROCESS_NAME, &pid)) { 118 | error("Failed to get pid for vix janitor process"); 119 | return; 120 | } 121 | 122 | if (!enif_send(env, &pid, NULL, 123 | enif_make_tuple2(env, label, resource_term))) { 124 | error("Failed to send unref msg to vix janitor"); 125 | return; 126 | } 127 | 128 | return; 129 | } 130 | 131 | static void vix_binary_dtor(ErlNifEnv *env, void *ptr) { 132 | VixBinaryResource *vix_bin_r = (VixBinaryResource *)ptr; 133 | g_free(vix_bin_r->data); 134 | debug("vix_binary_resource dtor"); 135 | } 136 | 137 | int utils_init(ErlNifEnv *env, const char *log_level) { 138 | ATOM_OK = make_atom(env, "ok"); 139 | ATOM_ERROR = make_atom(env, "error"); 140 | ATOM_NIL = make_atom(env, "nil"); 141 | ATOM_TRUE = make_atom(env, "true"); 142 | ATOM_FALSE = make_atom(env, "false"); 143 | ATOM_NULL_VALUE = make_atom(env, "null_value"); 144 | ATOM_UNDEFINED = make_atom(env, "undefined"); 145 | ATOM_EAGAIN = make_atom(env, "eagain"); 146 | 147 | VIX_JANITOR_PROCESS_NAME = make_atom(env, "Elixir.Vix.Nif.Janitor"); 148 | 149 | VIX_BINARY_RT = enif_open_resource_type( 150 | env, NULL, "vix_binary_resource", (ErlNifResourceDtor *)vix_binary_dtor, 151 | ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); 152 | 153 | if (strcmp(log_level, "warning") == 0) { 154 | VIX_LOG_LEVEL = VIX_LOG_LEVEL_WARNING; 155 | } else if (strcmp(log_level, "error") == 0) { 156 | VIX_LOG_LEVEL = VIX_LOG_LEVEL_ERROR; 157 | } else { 158 | #ifdef DEBUG 159 | // default to ERROR if we are running in debug mode 160 | VIX_LOG_LEVEL = VIX_LOG_LEVEL_ERROR; 161 | #else 162 | VIX_LOG_LEVEL = VIX_LOG_LEVEL_NONE; 163 | #endif 164 | } 165 | 166 | if (VIX_LOG_LEVEL == VIX_LOG_LEVEL_WARNING || 167 | VIX_LOG_LEVEL == VIX_LOG_LEVEL_ERROR) { 168 | g_log_set_handler("VIPS", G_LOG_LEVEL_WARNING, libvips_log_callback, NULL); 169 | } else { 170 | g_log_set_handler("VIPS", G_LOG_LEVEL_WARNING, libvips_log_null_callback, 171 | NULL); 172 | } 173 | 174 | if (!VIX_BINARY_RT) { 175 | error("Failed to open vix_binary_resource"); 176 | return 1; 177 | } 178 | 179 | return 0; 180 | } 181 | 182 | int close_fd(int *fd) { 183 | int ret = 0; 184 | 185 | if (*fd != VIX_FD_CLOSED) { 186 | ret = close(*fd); 187 | 188 | if (ret != 0) { 189 | error("failed to close fd: %d, error: %s", *fd, strerror(errno)); 190 | } else { 191 | *fd = VIX_FD_CLOSED; 192 | } 193 | } 194 | 195 | return ret; 196 | } 197 | 198 | void notify_consumed_timeslice(ErlNifEnv *env, ErlNifTime start, 199 | ErlNifTime stop) { 200 | ErlNifTime pct; 201 | 202 | pct = (ErlNifTime)((stop - start) / 10); 203 | if (pct > 100) 204 | pct = 100; 205 | else if (pct == 0) 206 | pct = 1; 207 | enif_consume_timeslice(env, pct); 208 | } 209 | 210 | ERL_NIF_TERM to_binary_term(ErlNifEnv *env, void *data, size_t size) { 211 | VixBinaryResource *vix_bin_r = 212 | enif_alloc_resource(VIX_BINARY_RT, sizeof(VixBinaryResource)); 213 | ERL_NIF_TERM bin_term; 214 | 215 | vix_bin_r->data = data; 216 | vix_bin_r->size = size; 217 | 218 | bin_term = enif_make_resource_binary(env, vix_bin_r, vix_bin_r->data, 219 | vix_bin_r->size); 220 | 221 | enif_release_resource(vix_bin_r); 222 | 223 | return bin_term; 224 | } 225 | -------------------------------------------------------------------------------- /c_src/g_object/g_param_spec.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "../utils.h" 6 | 7 | #include "g_param_spec.h" 8 | 9 | ErlNifResourceType *G_PARAM_SPEC_RT; 10 | 11 | /* elixir/erlang does not support infinity, use extreme values 12 | instead */ 13 | static double clamp_double(double value) { 14 | if (value == INFINITY) 15 | return DBL_MAX; 16 | else if (value == -INFINITY) 17 | return DBL_MIN; 18 | else 19 | return value; 20 | } 21 | 22 | static ERL_NIF_TERM enum_details(ErlNifEnv *env, GParamSpec *pspec) { 23 | GParamSpecEnum *pspec_enum = G_PARAM_SPEC_ENUM(pspec); 24 | GEnumClass *e_class; 25 | GEnumValue *e_value; 26 | 27 | e_class = pspec_enum->enum_class; 28 | e_value = g_enum_get_value(e_class, pspec_enum->default_value); 29 | return make_atom(env, e_value->value_name); 30 | } 31 | 32 | static ERL_NIF_TERM flag_details(ErlNifEnv *env, GParamSpec *pspec) { 33 | GParamSpecFlags *pspec_flags = G_PARAM_SPEC_FLAGS(pspec); 34 | GFlagsClass *f_class; 35 | ERL_NIF_TERM default_flags, flag; 36 | guint default_int; 37 | unsigned int i; 38 | 39 | f_class = pspec_flags->flags_class; 40 | default_int = pspec_flags->default_value; 41 | 42 | default_flags = enif_make_list(env, 0); 43 | 44 | for (i = 0; i < f_class->n_values - 1; i++) { 45 | if (f_class->values[i].value & default_int) { 46 | flag = make_atom(env, f_class->values[i].value_name); 47 | default_flags = enif_make_list_cell(env, flag, default_flags); 48 | } 49 | } 50 | 51 | return default_flags; 52 | } 53 | 54 | static ERL_NIF_TERM boolean_details(ErlNifEnv *env, GParamSpec *pspec) { 55 | GParamSpecBoolean *pspec_bool = G_PARAM_SPEC_BOOLEAN(pspec); 56 | 57 | if (pspec_bool->default_value) { 58 | return make_atom(env, "true"); 59 | } else { 60 | return make_atom(env, "false"); 61 | } 62 | } 63 | 64 | static ERL_NIF_TERM uint64_details(ErlNifEnv *env, GParamSpec *pspec) { 65 | GParamSpecUInt64 *pspec_uint64 = G_PARAM_SPEC_UINT64(pspec); 66 | 67 | return enif_make_tuple3(env, enif_make_uint64(env, pspec_uint64->minimum), 68 | enif_make_uint64(env, pspec_uint64->maximum), 69 | enif_make_uint64(env, pspec_uint64->default_value)); 70 | } 71 | 72 | static ERL_NIF_TERM double_details(ErlNifEnv *env, GParamSpec *pspec) { 73 | GParamSpecDouble *pspec_double = G_PARAM_SPEC_DOUBLE(pspec); 74 | return enif_make_tuple3( 75 | env, enif_make_double(env, clamp_double(pspec_double->minimum)), 76 | enif_make_double(env, clamp_double(pspec_double->maximum)), 77 | enif_make_double(env, clamp_double(pspec_double->default_value))); 78 | } 79 | 80 | static ERL_NIF_TERM int_details(ErlNifEnv *env, GParamSpec *pspec) { 81 | GParamSpecInt *pspec_int = G_PARAM_SPEC_INT(pspec); 82 | return enif_make_tuple3(env, enif_make_int(env, pspec_int->minimum), 83 | enif_make_int(env, pspec_int->maximum), 84 | enif_make_int(env, pspec_int->default_value)); 85 | } 86 | 87 | static ERL_NIF_TERM uint_details(ErlNifEnv *env, GParamSpec *pspec) { 88 | GParamSpecUInt *pspec_uint = G_PARAM_SPEC_UINT(pspec); 89 | return enif_make_tuple3(env, enif_make_uint(env, pspec_uint->minimum), 90 | enif_make_uint(env, pspec_uint->maximum), 91 | enif_make_uint(env, pspec_uint->default_value)); 92 | } 93 | 94 | static ERL_NIF_TERM int64_details(ErlNifEnv *env, GParamSpec *pspec) { 95 | GParamSpecInt64 *pspec_int64 = G_PARAM_SPEC_INT64(pspec); 96 | return enif_make_tuple3(env, enif_make_int64(env, pspec_int64->minimum), 97 | enif_make_int64(env, pspec_int64->maximum), 98 | enif_make_int64(env, pspec_int64->default_value)); 99 | } 100 | 101 | static ERL_NIF_TERM string_details(ErlNifEnv *env, GParamSpec *pspec) { 102 | GParamSpecString *pspec_string = G_PARAM_SPEC_STRING(pspec); 103 | if (pspec_string->default_value == NULL) { 104 | return ATOM_NIL; 105 | } else { 106 | return make_binary(env, pspec_string->default_value); 107 | } 108 | } 109 | 110 | ERL_NIF_TERM g_param_spec_to_erl_term(ErlNifEnv *env, GParamSpec *pspec) { 111 | GParamSpecResource *pspec_r = 112 | enif_alloc_resource(G_PARAM_SPEC_RT, sizeof(GParamSpecResource)); 113 | 114 | pspec_r->pspec = pspec; 115 | ERL_NIF_TERM term = enif_make_resource(env, pspec_r); 116 | enif_release_resource(pspec_r); 117 | 118 | return term; 119 | } 120 | 121 | bool erl_term_to_g_param_spec(ErlNifEnv *env, ERL_NIF_TERM term, 122 | GParamSpec **pspec) { 123 | GParamSpecResource *pspec_r = NULL; 124 | if (enif_get_resource(env, term, G_PARAM_SPEC_RT, (void **)&pspec_r)) { 125 | (*pspec) = pspec_r->pspec; 126 | return true; 127 | } else { 128 | return false; 129 | } 130 | } 131 | 132 | ERL_NIF_TERM g_param_spec_details(ErlNifEnv *env, GParamSpec *pspec) { 133 | ERL_NIF_TERM term, value_type, spec_type, desc; 134 | 135 | spec_type = make_binary(env, g_type_name(G_PARAM_SPEC_TYPE(pspec))); 136 | value_type = make_binary(env, g_type_name(G_PARAM_SPEC_VALUE_TYPE(pspec))); 137 | desc = make_binary(env, g_param_spec_get_blurb(pspec)); 138 | 139 | if (G_IS_PARAM_SPEC_ENUM(pspec)) { 140 | term = enum_details(env, pspec); 141 | } else if (G_IS_PARAM_SPEC_BOOLEAN(pspec)) { 142 | term = boolean_details(env, pspec); 143 | } else if (G_IS_PARAM_SPEC_UINT64(pspec)) { 144 | term = uint64_details(env, pspec); 145 | } else if (G_IS_PARAM_SPEC_DOUBLE(pspec)) { 146 | term = double_details(env, pspec); 147 | } else if (G_IS_PARAM_SPEC_INT(pspec)) { 148 | term = int_details(env, pspec); 149 | } else if (G_IS_PARAM_SPEC_UINT(pspec)) { 150 | term = uint_details(env, pspec); 151 | } else if (G_IS_PARAM_SPEC_INT64(pspec)) { 152 | term = int64_details(env, pspec); 153 | } else if (G_IS_PARAM_SPEC_STRING(pspec)) { 154 | term = string_details(env, pspec); 155 | } else if (G_IS_PARAM_SPEC_FLAGS(pspec)) { 156 | term = flag_details(env, pspec); 157 | } else if (G_IS_PARAM_SPEC_BOXED(pspec)) { 158 | term = ATOM_NIL; // TODO: handle default value 159 | } else if (G_IS_PARAM_SPEC_OBJECT(pspec)) { 160 | term = ATOM_NIL; // TODO: handle default value 161 | } else { 162 | error("Unknown GParamSpec: %s", 163 | g_type_name(G_PARAM_SPEC_VALUE_TYPE(pspec))); 164 | return enif_make_badarg(env); 165 | } 166 | 167 | return enif_make_tuple4(env, desc, spec_type, value_type, term); 168 | } 169 | 170 | static void g_param_spec_dtor(ErlNifEnv *env, void *obj) { 171 | debug("GParamSpecResource dtor"); 172 | } 173 | 174 | int nif_g_param_spec_init(ErlNifEnv *env) { 175 | G_PARAM_SPEC_RT = 176 | enif_open_resource_type(env, NULL, "g_param_spec_resource", 177 | (ErlNifResourceDtor *)g_param_spec_dtor, 178 | ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER, NULL); 179 | 180 | if (!G_PARAM_SPEC_RT) { 181 | error("Failed to open g_param_spec_resource"); 182 | return 1; 183 | } 184 | 185 | return 0; 186 | } 187 | -------------------------------------------------------------------------------- /lib/vix/vips/operation.ex: -------------------------------------------------------------------------------- 1 | defmodule Vix.Vips.Operation do 2 | @moduledoc """ 3 | Provides access to VIPS operations for image processing. 4 | 5 | This module exposes VIPS operations as Elixir functions, allowing you to perform 6 | various image processing tasks like resizing, color manipulation, filtering, 7 | and format conversion. 8 | 9 | ## Quick Start 10 | 11 | Here's a simple example to resize an image: 12 | 13 | # Load and resize an image to 500px width, maintaining aspect ratio 14 | {:ok, image} = Operation.thumbnail("input.jpg", 500) 15 | 16 | ## Working with Operations 17 | 18 | Operations in Vix can be grouped into several categories: 19 | 20 | * **Loading/Saving** - `Vix.Vips.Image`, `thumbnail/2`, and format specific functions. 21 | * **Resizing** - `resize/2`, `thumbnail/2`, `smartcrop/3` 22 | * **Color Management** - `colourspace/2`, `icc_transform/2` 23 | * **Filters & Effects** - `gaussblur/2`, `sharpen/2` 24 | * **Composition** - `composite/3`, `join/3`, `insert/4` 25 | 26 | Most operations follow a consistent pattern: 27 | 28 | 1. Load your image 29 | 2. Apply one or more operations 30 | 3. Save the result 31 | 32 | ## Common Examples 33 | 34 | # Basic image resizing while preserving aspect ratio 35 | {:ok, image} = Vix.Vips.Image.new_from_file("input.jpg") 36 | # scale down by 50% 37 | {:ok, resized} = Operation.resize(image, scale: 0.5) 38 | :ok = Vix.Vips.Image.write_to_file(resized, "output.jpg") 39 | 40 | # Convert to grayscale and apply Gaussian blur 41 | {:ok, image} = Vix.Vips.Image.new_from_file("input.jpg") 42 | {:ok, gray} = Operation.colourspace(image, :VIPS_INTERPRETATION_B_W) 43 | {:ok, blurred} = Operation.gaussblur(gray, 3.0) 44 | 45 | ## Advanced Usage 46 | 47 | ### Smart Cropping for Thumbnails 48 | 49 | # Generate a smart-cropped thumbnail focusing on interesting areas 50 | {:ok, thumb} = Operation.thumbnail("input.jpg", 300, 51 | crop: :attention, # Uses image analysis to find interesting areas 52 | height: 300, # Force square thumbnail 53 | ) 54 | 55 | ### Complex Image Composition 56 | 57 | # Create a watermarked image with transparency 58 | {:ok, base} = Vix.Vips.Image.new_from_file("photo.jpg") 59 | {:ok, watermark} = Vix.Vips.Image.new_from_file("watermark.png") 60 | {:ok, composed} = Operation.composite2(base, watermark, 61 | :VIPS_BLEND_MODE_OVER, # Blend mode 62 | x: 20, # Offset from left 63 | y: 20, # Offset from top 64 | opacity: 0.8 # Watermark transparency 65 | ) 66 | 67 | ### Color Management 68 | 69 | # Convert between color spaces with ICC profiles 70 | {:ok, image} = Vix.Vips.Image.new_from_file("input.jpg") 71 | {:ok, converted} = Operation.icc_transform(image, 72 | "sRGB.icc", # Target color profile 73 | "input-profile": "Adobe-RGB.icc" 74 | ) 75 | 76 | 77 | > ## Performance Tips {: .tip} 78 | > 79 | > * Use `thumbnail/2` instead of `resize/2` when possible - it's optimized for common cases 80 | > * Chain operations to avoid intermediate file I/O 81 | > * For batch processing, reuse loaded ICC profiles and watermarks 82 | > * Consider using sequential mode for large images 83 | 84 | ## Additional Resources 85 | 86 | * [VIPS Documentation](https://www.libvips.org/API/current/) 87 | 88 | 89 | 90 | 91 | 92 | 93 | """ 94 | 95 | import Vix.Vips.Operation.Helper 96 | 97 | alias Vix.Vips.Operation.Error 98 | 99 | # define typespec for enums 100 | Enum.map(vips_enum_list(), fn {name, enum} -> 101 | {enum_str_list, _} = Enum.unzip(enum) 102 | @type unquote(type_name(name)) :: unquote(atom_typespec_ast(enum_str_list)) 103 | end) 104 | 105 | # define typespec for flags 106 | Enum.map(vips_flag_list(), fn {name, flag} -> 107 | {flag_str_list, _} = Enum.unzip(flag) 108 | @type unquote(type_name(name)) :: list(unquote(atom_typespec_ast(flag_str_list))) 109 | end) 110 | 111 | Enum.map(vips_immutable_operation_list(), fn name -> 112 | %{ 113 | desc: desc, 114 | in_req_spec: in_req_spec, 115 | in_opt_spec: in_opt_spec, 116 | out_req_spec: out_req_spec, 117 | out_opt_spec: out_opt_spec 118 | } = spec = operation_args_spec(name) 119 | 120 | func_name = function_name(name) 121 | in_req_spec = normalize_input_variable_names(in_req_spec) 122 | 123 | req_params = 124 | Enum.map(in_req_spec, fn param -> 125 | param.param_name 126 | |> String.to_atom() 127 | |> Macro.var(__MODULE__) 128 | end) 129 | 130 | @doc """ 131 | #{prepare_doc(desc, in_req_spec, in_opt_spec, out_req_spec, out_opt_spec)} 132 | """ 133 | @spec unquote(func_typespec(func_name, in_req_spec, in_opt_spec, out_req_spec, out_opt_spec)) 134 | if in_opt_spec == [] do 135 | # operations without optional arguments 136 | def unquote(func_name)(unquote_splicing(req_params)) do 137 | operation_call(unquote(name), unquote(req_params), [], unquote(Macro.escape(spec))) 138 | end 139 | else 140 | # operations with optional arguments 141 | def unquote(func_name)(unquote_splicing(req_params), optional \\ []) do 142 | operation_call(unquote(name), unquote(req_params), optional, unquote(Macro.escape(spec))) 143 | end 144 | end 145 | 146 | bang_func_name = function_name(String.to_atom(name <> "!")) 147 | 148 | @doc """ 149 | #{prepare_doc(desc, in_req_spec, in_opt_spec, out_req_spec, out_opt_spec)} 150 | """ 151 | @spec unquote( 152 | bang_func_typespec( 153 | bang_func_name, 154 | in_req_spec, 155 | in_opt_spec, 156 | out_req_spec, 157 | out_opt_spec 158 | ) 159 | ) 160 | if in_opt_spec == [] do 161 | @dialyzer {:no_match, [{bang_func_name, length(req_params)}]} 162 | # operations without optional arguments 163 | def unquote(bang_func_name)(unquote_splicing(req_params)) do 164 | case __MODULE__.unquote(func_name)(unquote_splicing(req_params)) do 165 | :ok -> :ok 166 | {:ok, result} -> result 167 | {:error, reason} when is_binary(reason) -> raise Error, message: reason 168 | {:error, reason} -> raise Error, message: inspect(reason) 169 | end 170 | end 171 | else 172 | @dialyzer {:no_match, [{bang_func_name, length(req_params) + 1}]} 173 | # operations with optional arguments 174 | def unquote(bang_func_name)(unquote_splicing(req_params), optional \\ []) do 175 | case __MODULE__.unquote(func_name)(unquote_splicing(req_params), optional) do 176 | :ok -> :ok 177 | {:ok, result} -> result 178 | {:error, reason} when is_binary(reason) -> raise Error, message: reason 179 | {:error, reason} -> raise Error, message: inspect(reason) 180 | end 181 | end 182 | end 183 | end) 184 | end 185 | -------------------------------------------------------------------------------- /c_src/vix.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "utils.h" 6 | 7 | #include "g_object/g_boxed.h" 8 | #include "g_object/g_object.h" 9 | #include "g_object/g_param_spec.h" 10 | #include "g_object/g_type.h" 11 | #include "pipe.h" 12 | #include "vips_boxed.h" 13 | #include "vips_foreign.h" 14 | #include "vips_image.h" 15 | #include "vips_interpolate.h" 16 | #include "vips_operation.h" 17 | 18 | static int on_load(ErlNifEnv *env, void **priv, ERL_NIF_TERM load_info) { 19 | if (VIPS_INIT("vix")) { 20 | error("Failed to initialize Vips"); 21 | return 1; 22 | } 23 | 24 | ERL_NIF_TERM logger_level; 25 | ERL_NIF_TERM logger_level_key = enif_make_atom(env, "nif_logger_level"); 26 | 27 | if (!enif_get_map_value(env, load_info, logger_level_key, &logger_level)) { 28 | error("Failed to fetch logger level config from map"); 29 | return 1; 30 | } 31 | 32 | char log_level[20] = {0}; 33 | if (enif_get_atom(env, logger_level, log_level, 19, ERL_NIF_LATIN1) < 1) { 34 | error("Failed to fetch logger level atom value"); 35 | return 1; 36 | } 37 | 38 | #ifdef DEBUG 39 | vips_leak_set(true); 40 | // when checking for leaks disable cache 41 | vips_cache_set_max(0); 42 | #endif 43 | 44 | if (utils_init(env, log_level)) 45 | return 1; 46 | 47 | if (nif_g_object_init(env)) 48 | return 1; 49 | 50 | if (nif_g_param_spec_init(env)) 51 | return 1; 52 | 53 | if (nif_g_boxed_init(env)) 54 | return 1; 55 | 56 | if (nif_g_type_init(env)) 57 | return 1; 58 | 59 | if (nif_vips_operation_init(env)) 60 | return 1; 61 | 62 | if (nif_pipe_init(env)) 63 | return 1; 64 | 65 | return 0; 66 | } 67 | 68 | static ErlNifFunc nif_funcs[] = { 69 | /* GObject */ 70 | {"nif_g_object_type_name", 1, nif_g_object_type_name, 0}, 71 | {"nif_g_object_unref", 1, nif_g_object_unref, ERL_NIF_DIRTY_JOB_CPU_BOUND}, 72 | 73 | /* GType */ 74 | {"nif_g_type_from_instance", 1, nif_g_type_from_instance, 0}, 75 | {"nif_g_type_name", 1, nif_g_type_name, 0}, 76 | 77 | /* VipsInterpolate */ 78 | {"nif_interpolate_new", 1, nif_interpolate_new, 0}, 79 | 80 | /* VipsImage */ 81 | {"nif_image_new_from_file", 1, nif_image_new_from_file, 82 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 83 | {"nif_image_new_from_image", 2, nif_image_new_from_image, 84 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 85 | {"nif_image_copy_memory", 1, nif_image_copy_memory, 86 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 87 | {"nif_image_write_to_file", 2, nif_image_write_to_file, 88 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 89 | {"nif_image_write_to_buffer", 2, nif_image_write_to_buffer, 90 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 91 | {"nif_image_new", 0, nif_image_new, ERL_NIF_DIRTY_JOB_IO_BOUND}, 92 | {"nif_image_new_temp_file", 1, nif_image_new_temp_file, 93 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 94 | {"nif_image_new_matrix_from_array", 5, nif_image_new_matrix_from_array, 0}, 95 | {"nif_image_get_fields", 1, nif_image_get_fields, 0}, 96 | {"nif_image_get_header", 2, nif_image_get_header, 0}, 97 | {"nif_image_get_as_string", 2, nif_image_get_as_string, 0}, 98 | {"nif_image_hasalpha", 1, nif_image_hasalpha, 0}, 99 | {"nif_image_new_from_source", 2, nif_image_new_from_source, 100 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 101 | {"nif_image_to_target", 3, nif_image_to_target, ERL_NIF_DIRTY_JOB_IO_BOUND}, 102 | {"nif_image_new_from_binary", 5, nif_image_new_from_binary, 103 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 104 | {"nif_image_write_to_binary", 1, nif_image_write_to_binary, 105 | ERL_NIF_DIRTY_JOB_CPU_BOUND}, 106 | {"nif_image_write_area_to_binary", 2, nif_image_write_area_to_binary, 107 | ERL_NIF_DIRTY_JOB_CPU_BOUND}, 108 | 109 | /* VipsImage UNSAFE */ 110 | {"nif_image_update_metadata", 3, nif_image_update_metadata, 0}, 111 | {"nif_image_set_metadata", 4, nif_image_set_metadata, 0}, 112 | {"nif_image_remove_metadata", 2, nif_image_remove_metadata, 0}, 113 | 114 | /* VipsOperation */ 115 | /* should these be ERL_NIF_DIRTY_JOB_IO_BOUND? */ 116 | {"nif_vips_operation_call", 2, nif_vips_operation_call, 117 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 118 | {"nif_vips_operation_get_arguments", 1, nif_vips_operation_get_arguments, 119 | ERL_NIF_DIRTY_JOB_CPU_BOUND}, 120 | {"nif_vips_operation_list", 0, nif_vips_operation_list, 121 | ERL_NIF_DIRTY_JOB_CPU_BOUND}, 122 | {"nif_vips_enum_list", 0, nif_vips_enum_list, ERL_NIF_DIRTY_JOB_CPU_BOUND}, 123 | {"nif_vips_flag_list", 0, nif_vips_flag_list, ERL_NIF_DIRTY_JOB_CPU_BOUND}, 124 | 125 | /* Vips */ 126 | {"nif_vips_cache_set_max", 1, nif_vips_cache_set_max, 0}, 127 | {"nif_vips_cache_get_max", 0, nif_vips_cache_get_max, 0}, 128 | {"nif_vips_concurrency_set", 1, nif_vips_concurrency_set, 0}, 129 | {"nif_vips_concurrency_get", 0, nif_vips_concurrency_get, 0}, 130 | {"nif_vips_cache_set_max_files", 1, nif_vips_cache_set_max_files, 0}, 131 | {"nif_vips_cache_get_max_files", 0, nif_vips_cache_get_max_files, 0}, 132 | {"nif_vips_cache_set_max_mem", 1, nif_vips_cache_set_max_mem, 0}, 133 | {"nif_vips_cache_get_max_mem", 0, nif_vips_cache_get_max_mem, 0}, 134 | {"nif_vips_leak_set", 1, nif_vips_leak_set, 0}, 135 | {"nif_vips_tracked_get_mem", 0, nif_vips_tracked_get_mem, 0}, 136 | {"nif_vips_tracked_get_mem_highwater", 0, nif_vips_tracked_get_mem, 0}, 137 | {"nif_vips_version", 0, nif_vips_version, 0}, 138 | {"nif_vips_shutdown", 0, nif_vips_shutdown, 0}, 139 | {"nif_vips_nickname_find", 1, nif_vips_nickname_find, 0}, 140 | 141 | /* VipsBoxed */ 142 | {"nif_int_array", 1, nif_int_array, 0}, 143 | {"nif_image_array", 1, nif_image_array, 0}, 144 | {"nif_double_array", 1, nif_double_array, 0}, 145 | {"nif_vips_blob", 1, nif_vips_blob, 0}, 146 | {"nif_vips_ref_string", 1, nif_vips_ref_string, 0}, 147 | {"nif_vips_int_array_to_erl_list", 1, nif_vips_int_array_to_erl_list, 0}, 148 | {"nif_vips_double_array_to_erl_list", 1, nif_vips_double_array_to_erl_list, 149 | 0}, 150 | {"nif_vips_image_array_to_erl_list", 1, nif_vips_image_array_to_erl_list, 151 | 0}, 152 | {"nif_vips_blob_to_erl_binary", 1, nif_vips_blob_to_erl_binary, 0}, 153 | {"nif_vips_ref_string_to_erl_binary", 1, nif_vips_ref_string_to_erl_binary, 154 | 0}, 155 | {"nif_g_boxed_unref", 1, nif_g_boxed_unref, ERL_NIF_DIRTY_JOB_CPU_BOUND}, 156 | 157 | /* VipsForeign */ 158 | {"nif_foreign_find_load", 1, nif_foreign_find_load, 0}, 159 | {"nif_foreign_find_save", 1, nif_foreign_find_save, 0}, 160 | {"nif_foreign_find_load_buffer", 1, nif_foreign_find_load_buffer, 161 | ERL_NIF_DIRTY_JOB_IO_BOUND}, 162 | // it might read bytes form the file 163 | {"nif_foreign_find_save_buffer", 1, nif_foreign_find_save_buffer, 0}, 164 | {"nif_foreign_find_load_source", 1, nif_foreign_find_load_source, 165 | ERL_NIF_DIRTY_JOB_IO_BOUND}, // it might read bytes from source 166 | {"nif_foreign_find_save_target", 1, nif_foreign_find_save_target, 0}, 167 | {"nif_foreign_get_suffixes", 0, nif_foreign_get_suffixes, 0}, 168 | {"nif_foreign_get_loader_suffixes", 0, nif_foreign_get_loader_suffixes, 0}, 169 | 170 | /* Syscalls */ 171 | {"nif_pipe_open", 1, nif_pipe_open, 0}, 172 | {"nif_write", 2, nif_write, ERL_NIF_DIRTY_JOB_CPU_BOUND}, 173 | {"nif_read", 2, nif_read, ERL_NIF_DIRTY_JOB_CPU_BOUND}, 174 | {"nif_source_new", 0, nif_source_new, ERL_NIF_DIRTY_JOB_CPU_BOUND}, 175 | {"nif_target_new", 0, nif_target_new, ERL_NIF_DIRTY_JOB_CPU_BOUND}}; 176 | 177 | ERL_NIF_INIT(Elixir.Vix.Nif, nif_funcs, &on_load, NULL, NULL, NULL) 178 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master, dev] 5 | pull_request: 6 | branches: [master, dev] 7 | jobs: 8 | linux: 9 | runs-on: ubuntu-24.04 10 | name: Test Compiled - Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}} 11 | timeout-minutes: 45 12 | env: 13 | VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS 14 | strategy: 15 | matrix: 16 | include: 17 | - elixir: 1.16.x 18 | otp: 26.x 19 | - elixir: 1.17.x 20 | otp: 27.x 21 | - elixir: 1.18.x 22 | otp: 27.x 23 | steps: 24 | - uses: erlef/setup-beam@v1 25 | with: 26 | otp-version: ${{matrix.otp}} 27 | elixir-version: ${{matrix.elixir}} 28 | 29 | - name: Checkout code 30 | uses: actions/checkout@v4 31 | 32 | - name: Install libvips build dependencies 33 | run: | 34 | sudo apt-get update 35 | sudo apt-get install \ 36 | meson pkg-config \ 37 | libarchive-dev libcfitsio-dev libcgif-dev \ 38 | libexif-dev libexpat1-dev libffi-dev \ 39 | libfftw3-dev libheif-dev libheif-plugin-aomenc \ 40 | libheif-plugin-x265 libhwy-dev libimagequant-dev \ 41 | libjpeg-dev libjxl-dev liblcms2-dev \ 42 | libmatio-dev libnifti-dev libopenexr-dev \ 43 | libopenjp2-7-dev libopenslide-dev libpango1.0-dev \ 44 | libpng-dev libpoppler-glib-dev librsvg2-dev \ 45 | libtiff5-dev libwebp-dev 46 | 47 | - name: Get latest version of libvips 48 | run: | 49 | VIPS_LATEST_RELEASE=$(curl -L -s https://api.github.com/repos/libvips/libvips/releases/latest | grep -o -E "https://(.*)/vips-(.*).tar.xz" | head -1) 50 | echo "VIPS_LATEST_RELEASE=${VIPS_LATEST_RELEASE}" >> $GITHUB_ENV 51 | 52 | - name: Cache libvips artifacts 53 | uses: actions/cache@v4 54 | id: vips-cache 55 | with: 56 | path: vips 57 | key: ${{ runner.os }}-vips-${{ env.VIPS_LATEST_RELEASE }} 58 | 59 | - name: Compile libvips from source 60 | if: steps.vips-cache.outputs.cache-hit != 'true' 61 | run: | 62 | set -e 63 | mkdir vips 64 | echo "Downloading libvips from: ${VIPS_LATEST_RELEASE}" 65 | curl -s -L "${VIPS_LATEST_RELEASE}" | tar xJ -C ./vips --strip-components=1 66 | cd vips 67 | echo "Setting up meson build..." 68 | meson setup build -Ddeprecated=false -Dmagick=disabled \ 69 | || { echo "Meson setup failed:"; cat build/meson-logs/meson-log.txt; exit 1; } 70 | echo "Compiling libvips..." 71 | meson compile -C build \ 72 | || { echo "Compilation failed"; exit 1; } 73 | 74 | - name: Install libvips 75 | run: | 76 | cd vips 77 | sudo meson install -C build 78 | sudo ldconfig -v 79 | 80 | - name: Cache Dependencies 81 | id: mix-cache 82 | uses: actions/cache@v4 83 | with: 84 | path: | 85 | deps 86 | _build 87 | key: ${{ runner.os }}-test-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} 88 | 89 | - name: Install Dependencies 90 | if: steps.mix-cache.outputs.cache-hit != 'true' 91 | run: mix deps.get 92 | - run: mix test --trace 93 | 94 | linux-precompiled-libvips: 95 | runs-on: ubuntu-24.04 96 | name: Test Pre-compiled libvips - Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}} 97 | timeout-minutes: 30 98 | env: 99 | VIX_COMPILATION_MODE: PRECOMPILED_LIBVIPS 100 | strategy: 101 | matrix: 102 | include: 103 | - elixir: 1.18.x 104 | otp: 27.x 105 | steps: 106 | - uses: erlef/setup-beam@v1 107 | with: 108 | otp-version: ${{matrix.otp}} 109 | elixir-version: ${{matrix.elixir}} 110 | 111 | - name: Checkout code 112 | uses: actions/checkout@v4 113 | 114 | - name: Cache Dependencies 115 | id: mix-cache 116 | uses: actions/cache@v4 117 | with: 118 | # _build contains compiled files. So we should not cache them 119 | path: | 120 | deps 121 | key: ${{ runner.os }}-precompiled-libvips-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} 122 | 123 | - name: Install Dependencies 124 | if: steps.mix-cache.outputs.cache-hit != 'true' 125 | run: mix deps.get 126 | 127 | - run: mix test --trace 128 | 129 | linux-precompiled: 130 | runs-on: ubuntu-24.04 131 | name: Test Pre-compiled - Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}} 132 | timeout-minutes: 30 133 | strategy: 134 | matrix: 135 | include: 136 | - elixir: 1.18.x 137 | otp: 27.x 138 | steps: 139 | - uses: erlef/setup-beam@v1 140 | with: 141 | otp-version: ${{matrix.otp}} 142 | elixir-version: ${{matrix.elixir}} 143 | 144 | - name: Checkout code 145 | uses: actions/checkout@v4 146 | 147 | - name: Cache Dependencies 148 | id: mix-cache 149 | uses: actions/cache@v4 150 | with: 151 | # _build contains compiled files. So we should not cache them 152 | path: | 153 | deps 154 | key: ${{ runner.os }}-precompiled-nif-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} 155 | 156 | - name: Install Dependencies 157 | if: steps.mix-cache.outputs.cache-hit != 'true' 158 | run: mix deps.get 159 | 160 | - name: Remove Artifacts & Generate checksum.exs 161 | run: | 162 | ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache" 163 | rm -rf priv/* _build/*/lib/vix "${ELIXIR_MAKE_CACHE_DIR}" 164 | mix elixir_make.checksum --only-local 165 | 166 | - run: mix test --trace 167 | 168 | linux-precompiled-arm: 169 | runs-on: ubuntu-24.04 170 | name: Test Pre-compiled ARM 171 | timeout-minutes: 60 172 | steps: 173 | - name: Checkout code 174 | uses: actions/checkout@v4 175 | 176 | - name: Set up QEMU 177 | uses: docker/setup-qemu-action@v3 178 | with: 179 | platforms: linux/arm/v7 180 | 181 | - name: Set up Docker Buildx 182 | uses: docker/setup-buildx-action@v3 183 | 184 | - name: Test on ARM 185 | run: | 186 | set -e 187 | docker run --rm \ 188 | --platform linux/arm/v7 \ 189 | -v "$PWD:/workspace" \ 190 | -w /workspace \ 191 | ubuntu:25.04 \ 192 | bash -c " 193 | set -e 194 | # Verify we're running on arm 195 | uname -m 196 | cat /proc/cpuinfo | head -10 197 | apt-get update 198 | apt-get install -y ca-certificates elixir erlang-dev erlang-xmerl 199 | mix local.hex --force 200 | mix local.rebar --force 201 | elixir --version 202 | mix deps.get 203 | ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache" 204 | rm -rf priv/* _build/*/lib/vix "${ELIXIR_MAKE_CACHE_DIR}" 205 | mix elixir_make.checksum --only-local 206 | mix test --trace 207 | " 208 | 209 | macos-precompiled: 210 | runs-on: macos-14 211 | name: Test macOS 212 | timeout-minutes: 45 213 | steps: 214 | - name: Checkout code 215 | uses: actions/checkout@v4 216 | - uses: DeterminateSystems/nix-installer-action@main 217 | - uses: DeterminateSystems/magic-nix-cache-action@main 218 | - uses: DeterminateSystems/flake-checker-action@main 219 | - run: nix develop --command mix deps.get 220 | - run: nix develop --command mix test --trace 221 | 222 | lint: 223 | runs-on: ubuntu-24.04 224 | name: Lint & Type Check 225 | timeout-minutes: 30 226 | strategy: 227 | matrix: 228 | include: 229 | - elixir: 1.18.x 230 | otp: 27.x 231 | steps: 232 | - uses: erlef/setup-beam@v1 233 | with: 234 | otp-version: ${{matrix.otp}} 235 | elixir-version: ${{matrix.elixir}} 236 | 237 | - name: Checkout code 238 | uses: actions/checkout@v4 239 | 240 | - name: Cache Dependencies 241 | id: mix-cache 242 | uses: actions/cache@v4 243 | with: 244 | path: | 245 | deps 246 | _build 247 | priv/plts 248 | key: ${{ runner.os }}-lint-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} 249 | 250 | - name: Install Dependencies 251 | if: steps.mix-cache.outputs.cache-hit != 'true' 252 | run: | 253 | mkdir -p priv/plts 254 | mix deps.get 255 | mix dialyzer --plt 256 | 257 | - run: mix clean && mix deep_clean 258 | - run: mix compile --force --warnings-as-errors 259 | - run: mix deps.unlock --check-unused 260 | - run: mix format --check-formatted 261 | - run: mix credo --strict 262 | - run: mix dialyzer --format github 263 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Vix Development Guide 2 | 3 | This document provides comprehensive guidance for Vix development, testing, and release processes. 4 | 5 | ## Table of Contents 6 | 7 | - [Development Environment Setup](#development-environment-setup) 8 | - [Build System](#build-system) 9 | - [Toolchain Management](#toolchain-management) 10 | - [Testing and Quality Assurance](#testing-and-quality-assurance) 11 | - [Release Process](#release-process) 12 | - [Troubleshooting](#troubleshooting) 13 | 14 | ## Development Environment Setup 15 | 16 | ### Prerequisites 17 | 18 | - Elixir 1.11+ with OTP 21+ 19 | - C compiler (gcc/clang) 20 | - Make 21 | - pkg-config (for system libvips detection) 22 | 23 | ### Compilation Modes 24 | 25 | Vix supports three compilation modes controlled by the `VIX_COMPILATION_MODE` environment variable: 26 | 27 | 1. **`PRECOMPILED_NIF_AND_LIBVIPS`** (default) - Use precompiled NIFs and libvips 28 | 2. **`PRECOMPILED_LIBVIPS`** - Compile NIFs locally, use precompiled libvips 29 | 3. **`PLATFORM_PROVIDED_LIBVIPS`** - Use system-provided libvips 30 | 31 | ```bash 32 | # Use system libvips (requires libvips-dev package) 33 | export VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS 34 | 35 | # Force precompiled libvips build 36 | export VIX_COMPILATION_MODE=PRECOMPILED_LIBVIPS 37 | ``` 38 | 39 | ### Initial Setup 40 | 41 | ```bash 42 | # Clone and build 43 | git clone https://github.com/akash-akya/vix.git 44 | cd vix 45 | make all 46 | 47 | # Run tests 48 | mix test 49 | 50 | # Verify installation 51 | iex -S mix 52 | ``` 53 | 54 | ## Build System 55 | 56 | ### Core Build Commands 57 | 58 | ```bash 59 | # Build everything 60 | make all 61 | make compile 62 | 63 | # Clean builds 64 | make clean # Clean build artifacts 65 | make deep_clean # Full clean including precompiled libvips 66 | make clean_precompiled_libvips # Remove only precompiled libvips 67 | 68 | # Debug and verbose builds 69 | make debug # Show build configuration (from c_src/) 70 | make V=1 all # Verbose compilation output 71 | ``` 72 | 73 | ### Build Configuration 74 | 75 | Build behavior is controlled by several environment variables: 76 | 77 | - `VIX_COMPILATION_MODE` - Compilation strategy 78 | - `LIBVIPS_VERSION` - Override default libvips version 79 | - `CC_PRECOMPILER_CURRENT_TARGET` - Override target platform 80 | - `ELIXIR_MAKE_CACHE_DIR` - Cache directory for precompiled binaries 81 | 82 | ## Toolchain Management 83 | 84 | ### Musl Toolchain System 85 | 86 | Vix uses musl toolchains for cross-compilation. Due to instability of the upstream musl.cc website, we maintain mirrors via GitHub releases. 87 | 88 | #### Downloading Toolchains 89 | 90 | ```bash 91 | # Download cached toolchains with fallback 92 | ./scripts/download_toolchains.sh 93 | ``` 94 | 95 | This script: 96 | 1. First attempts to download from our GitHub release mirror 97 | 2. Falls back to upstream musl.cc if mirror fails 98 | 3. Downloads and extracts `x86_64-linux-musl-cross.tgz` and `aarch64-linux-musl-cross.tgz` 99 | 100 | #### Mirroring New Toolchains 101 | 102 | ```bash 103 | # Mirror toolchains from upstream to local directory 104 | ./scripts/mirror_toolchains.sh 105 | ``` 106 | 107 | This creates a `toolchains/` directory with downloaded toolchain archives. To update the mirror: 108 | 109 | 1. Run the mirror script 110 | 2. Create a GitHub release tagged `toolchains-v{VERSION}` (e.g., `toolchains-v11.2.1`) 111 | 3. Upload the toolchain files as release assets 112 | 4. Update version in scripts if needed 113 | 114 | ### Precompiled libvips Management 115 | 116 | Precompiled libvips binaries are managed through our [sharp-libvips fork](https://github.com/akash-akya/sharp-libvips). 117 | 118 | Current configuration: 119 | - **libvips version**: `8.15.3` (defined in `build_scripts/precompiler.exs:11`) 120 | - **Release tag**: `8.15.3-rc3` (defined in `build_scripts/precompiler.exs:24`) 121 | 122 | Supported platforms: 123 | - Linux x64 (gnu/musl) 124 | - Linux ARM64 (gnu/musl) 125 | - Linux ARMv7/ARMv6 126 | - macOS x64/ARM64 127 | 128 | ## Testing and Quality Assurance 129 | 130 | ### Running Tests 131 | 132 | ```bash 133 | # Standard test suite 134 | mix test 135 | 136 | # Test with cached precompiled binaries 137 | ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache" mix test 138 | 139 | # Test specific files 140 | mix test test/vix/vips/image_test.exs 141 | 142 | # Coverage reports 143 | mix coveralls 144 | ``` 145 | 146 | ### Code Quality Tools 147 | 148 | ```bash 149 | # Static analysis 150 | make lint # or mix credo 151 | make dialyxir # or mix dialyxir 152 | 153 | # Code formatting 154 | make format # or mix format 155 | ``` 156 | 157 | ### Pre-commit Checks 158 | 159 | Before committing changes, ensure: 160 | 161 | ```bash 162 | # Clean build passes 163 | make clean && make all 164 | 165 | # All tests pass 166 | mix test 167 | 168 | # Code quality checks pass 169 | make lint && make dialyxir 170 | 171 | # Code is formatted 172 | make format 173 | ``` 174 | 175 | ## Release Process 176 | 177 | ### Standard Release (NIF/Package Updates) 178 | 179 | For releases without libvips changes: 180 | 181 | 1. **Prepare Release** 182 | ```bash 183 | # Bump version in mix.exs 184 | git add mix.exs 185 | git commit -m "Bump version to X.Y.Z" 186 | git push origin master 187 | ``` 188 | 189 | 2. **Create GitHub Release** 190 | - Go to https://github.com/akash-akya/vix/releases 191 | - Create new release with tag `vX.Y.Z` 192 | - GitHub Actions automatically builds and uploads NIF artifacts 193 | - Wait for all artifacts to be available (check all BEAM NIF versions: 2.16, 2.17, etc.) 194 | 195 | 3. **Generate Checksums** 196 | ```bash 197 | # Clean local state 198 | rm -rf cache/ priv/* checksum.exs _build/*/lib/vix 199 | 200 | # Generate checksum file 201 | ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache" MIX_ENV=prod mix elixir_make.checksum --all 202 | 203 | # Verify checksum contents 204 | cat checksum.exs 205 | ``` 206 | 207 | 4. **Test and Publish** 208 | ```bash 209 | # Test precompiled packages 210 | ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache" mix test 211 | 212 | # Publish to Hex 213 | mix hex.publish 214 | ``` 215 | 216 | ### Libvips Update Release 217 | 218 | For releases with new precompiled libvips versions: 219 | 220 | 1. **Update sharp-libvips Fork** 221 | ```bash 222 | cd ../sharp-libvips # Your fork directory 223 | 224 | # Pull latest stable upstream changes 225 | git remote add upstream https://github.com/lovell/sharp-libvips.git 226 | git fetch upstream 227 | git checkout upstream/main 228 | 229 | # Apply our patches for shared library compatibility 230 | git cherry-pick 231 | 232 | # Create tag matching upstream version 233 | git tag v8.15.X 234 | git push origin v8.15.X 235 | ``` 236 | 237 | 2. **Wait for Artifacts** 238 | - GitHub Actions in sharp-libvips fork creates release and artifacts 239 | - Verify all required platform artifacts are created 240 | 241 | 3. **Update Vix Configuration** 242 | ```bash 243 | # Update build_scripts/precompiler.exs 244 | # - @vips_version (line 11) 245 | # - @release_tag (line 24) 246 | ``` 247 | 248 | 4. **Test Locally** 249 | ```bash 250 | # Clean and test with new libvips 251 | rm -rf _build/*/lib/vix cache/ priv/* checksum.exs 252 | export VIX_COMPILATION_MODE=PRECOMPILED_LIBVIPS 253 | mix compile 254 | mix test 255 | ``` 256 | 257 | 5. **Release** 258 | - Follow standard release process above 259 | - Push libvips configuration changes 260 | - Create Vix release and publish to Hex 261 | 262 | ## Troubleshooting 263 | 264 | ### Common Build Issues 265 | 266 | **"libvips not found"** 267 | ```bash 268 | # Install system libvips 269 | sudo apt-get install libvips-dev # Ubuntu/Debian 270 | brew install vips # macOS 271 | 272 | # Or force precompiled mode 273 | export VIX_COMPILATION_MODE=PRECOMPILED_LIBVIPS 274 | ``` 275 | 276 | **"NIF compilation failed"** 277 | ```bash 278 | # Clean and rebuild 279 | make deep_clean 280 | make all 281 | 282 | # Check build configuration 283 | cd c_src && make debug 284 | ``` 285 | 286 | **"Checksum verification failed"** 287 | ```bash 288 | # Clear cache and regenerate 289 | rm -rf cache/ checksum.exs 290 | ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache" MIX_ENV=prod mix elixir_make.checksum --all 291 | ``` 292 | 293 | ### Toolchain Issues 294 | 295 | **"Toolchain download failed"** 296 | ```bash 297 | # Check if mirror is working 298 | curl -I https://github.com/akash-akya/vix/releases/download/toolchains-v11.2.1/x86_64-linux-musl-cross.tgz 299 | 300 | # Manually download and extract 301 | wget https://more.musl.cc/11.2.1/x86_64-linux-musl/x86_64-linux-musl-cross.tgz 302 | tar -xzf x86_64-linux-musl-cross.tgz 303 | ``` 304 | 305 | ### Development Tips 306 | 307 | - Use `ELIXIR_MAKE_CACHE_DIR="$(pwd)/cache"` to cache precompiled binaries locally 308 | - Set `VIX_COMPILATION_MODE=PLATFORM_PROVIDED_LIBVIPS` for faster iteration during development 309 | - Run `make debug` from `c_src/` directory to see detailed build configuration 310 | - Use `mix test --trace` for verbose test output 311 | - Check GitHub Actions logs for CI build failures 312 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | %{ 7 | # 8 | # You can have as many configs as you like in the `configs:` field. 9 | configs: [ 10 | %{ 11 | # 12 | # Run any config using `mix credo -C `. If no config name is given 13 | # "default" is used. 14 | # 15 | name: "default", 16 | # 17 | # These are the files included in the analysis: 18 | files: %{ 19 | # 20 | # You can give explicit globs or simply directories. 21 | # In the latter case `**/*.{ex,exs}` will be used. 22 | # 23 | included: [ 24 | "lib/", 25 | "src/", 26 | "test/", 27 | "web/", 28 | "apps/*/lib/", 29 | "apps/*/src/", 30 | "apps/*/test/", 31 | "apps/*/web/" 32 | ], 33 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 34 | }, 35 | # 36 | # Load and configure plugins here: 37 | # 38 | plugins: [], 39 | # 40 | # If you create your own checks, you must specify the source files for 41 | # them here, so they can be loaded by Credo before running the analysis. 42 | # 43 | requires: [], 44 | # 45 | # If you want to enforce a style guide and need a more traditional linting 46 | # experience, you can change `strict` to `true` below: 47 | # 48 | strict: false, 49 | # 50 | # To modify the timeout for parsing files, change this value: 51 | # 52 | parse_timeout: 5000, 53 | # 54 | # If you want to use uncolored output by default, you can change `color` 55 | # to `false` below: 56 | # 57 | color: true, 58 | # 59 | # You can customize the parameters of any check by adding a second element 60 | # to the tuple. 61 | # 62 | # To disable a check put `false` as second element: 63 | # 64 | # {Credo.Check.Design.DuplicatedCode, false} 65 | # 66 | checks: %{ 67 | enabled: [ 68 | # 69 | ## Consistency Checks 70 | # 71 | {Credo.Check.Consistency.ExceptionNames, []}, 72 | {Credo.Check.Consistency.LineEndings, []}, 73 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 74 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 75 | {Credo.Check.Consistency.SpaceInParentheses, []}, 76 | {Credo.Check.Consistency.TabsOrSpaces, []}, 77 | 78 | # 79 | ## Design Checks 80 | # 81 | # You can customize the priority of any check 82 | # Priority values are: `low, normal, high, higher` 83 | # 84 | {Credo.Check.Design.AliasUsage, 85 | [priority: :low, if_nested_deeper_than: 4, if_called_more_often_than: 0]}, 86 | # You can also customize the exit_status of each check. 87 | # If you don't want TODO comments to cause `mix credo` to fail, just 88 | # set this value to 0 (zero). 89 | # 90 | {Credo.Check.Design.TagTODO, [exit_status: 0]}, 91 | {Credo.Check.Design.TagFIXME, []}, 92 | 93 | # 94 | ## Readability Checks 95 | # 96 | {Credo.Check.Readability.AliasOrder, []}, 97 | {Credo.Check.Readability.FunctionNames, []}, 98 | {Credo.Check.Readability.LargeNumbers, []}, 99 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 100 | {Credo.Check.Readability.ModuleAttributeNames, []}, 101 | {Credo.Check.Readability.ModuleDoc, []}, 102 | {Credo.Check.Readability.ModuleNames, []}, 103 | {Credo.Check.Readability.ParenthesesInCondition, []}, 104 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 105 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 106 | {Credo.Check.Readability.PredicateFunctionNames, []}, 107 | {Credo.Check.Readability.PreferImplicitTry, []}, 108 | {Credo.Check.Readability.RedundantBlankLines, []}, 109 | {Credo.Check.Readability.Semicolons, []}, 110 | {Credo.Check.Readability.SpaceAfterCommas, []}, 111 | {Credo.Check.Readability.StringSigils, []}, 112 | {Credo.Check.Readability.TrailingBlankLine, []}, 113 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 114 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 115 | {Credo.Check.Readability.VariableNames, []}, 116 | {Credo.Check.Readability.WithSingleClause, []}, 117 | 118 | # 119 | ## Refactoring Opportunities 120 | # 121 | {Credo.Check.Refactor.Apply, []}, 122 | {Credo.Check.Refactor.CondStatements, []}, 123 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 124 | {Credo.Check.Refactor.FunctionArity, []}, 125 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 126 | {Credo.Check.Refactor.MatchInCondition, []}, 127 | {Credo.Check.Refactor.MapJoin, []}, 128 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 129 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 130 | {Credo.Check.Refactor.Nesting, []}, 131 | {Credo.Check.Refactor.UnlessWithElse, []}, 132 | {Credo.Check.Refactor.WithClauses, []}, 133 | {Credo.Check.Refactor.FilterFilter, []}, 134 | {Credo.Check.Refactor.RejectReject, []}, 135 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 136 | 137 | # 138 | ## Warnings 139 | # 140 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 141 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 142 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 143 | {Credo.Check.Warning.IExPry, []}, 144 | {Credo.Check.Warning.IoInspect, []}, 145 | {Credo.Check.Warning.OperationOnSameValues, []}, 146 | {Credo.Check.Warning.OperationWithConstantResult, []}, 147 | {Credo.Check.Warning.RaiseInsideRescue, []}, 148 | {Credo.Check.Warning.SpecWithStruct, []}, 149 | {Credo.Check.Warning.WrongTestFileExtension, []}, 150 | {Credo.Check.Warning.UnusedEnumOperation, []}, 151 | {Credo.Check.Warning.UnusedFileOperation, []}, 152 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 153 | {Credo.Check.Warning.UnusedListOperation, []}, 154 | {Credo.Check.Warning.UnusedPathOperation, []}, 155 | {Credo.Check.Warning.UnusedRegexOperation, []}, 156 | {Credo.Check.Warning.UnusedStringOperation, []}, 157 | {Credo.Check.Warning.UnusedTupleOperation, []}, 158 | {Credo.Check.Warning.UnsafeExec, []} 159 | ], 160 | disabled: [ 161 | # 162 | # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) 163 | 164 | # 165 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 166 | # and be sure to use `mix credo --strict` to see low priority checks) 167 | # 168 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 169 | {Credo.Check.Consistency.UnusedVariableNames, []}, 170 | {Credo.Check.Design.DuplicatedCode, []}, 171 | {Credo.Check.Design.SkipTestWithoutComment, []}, 172 | {Credo.Check.Readability.AliasAs, []}, 173 | {Credo.Check.Readability.BlockPipe, []}, 174 | {Credo.Check.Readability.ImplTrue, []}, 175 | {Credo.Check.Readability.MultiAlias, []}, 176 | {Credo.Check.Readability.NestedFunctionCalls, []}, 177 | {Credo.Check.Readability.SeparateAliasRequire, []}, 178 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 179 | {Credo.Check.Readability.SinglePipe, []}, 180 | {Credo.Check.Readability.Specs, []}, 181 | {Credo.Check.Readability.StrictModuleLayout, []}, 182 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 183 | {Credo.Check.Refactor.ABCSize, []}, 184 | {Credo.Check.Refactor.AppendSingleItem, []}, 185 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 186 | {Credo.Check.Refactor.FilterReject, []}, 187 | {Credo.Check.Refactor.IoPuts, []}, 188 | {Credo.Check.Refactor.MapMap, []}, 189 | {Credo.Check.Refactor.ModuleDependencies, []}, 190 | {Credo.Check.Refactor.NegatedIsNil, []}, 191 | {Credo.Check.Refactor.PipeChainStart, []}, 192 | {Credo.Check.Refactor.RejectFilter, []}, 193 | {Credo.Check.Refactor.VariableRebinding, []}, 194 | {Credo.Check.Warning.LazyLogging, []}, 195 | {Credo.Check.Warning.LeakyEnvironment, []}, 196 | {Credo.Check.Warning.MapGetUnsafePass, []}, 197 | {Credo.Check.Warning.MixEnv, []}, 198 | {Credo.Check.Warning.UnsafeToAtom, []} 199 | 200 | # {Credo.Check.Refactor.MapInto, []}, 201 | 202 | # 203 | # Custom checks can be created using `mix credo.gen.check`. 204 | # 205 | ] 206 | } 207 | } 208 | ] 209 | } 210 | --------------------------------------------------------------------------------