defmodule Elxvips.ResizeOptions do defstruct [ width: :auto, height: :auto, resize_type: :auto, # :type conflicts with rust keyword "type" ] end defmodule Elxvips.SaveOptions do defstruct [ quality: 90, format: :auto, strip: true, path: "", compression: 6, background: [], ] end defmodule Elxvips.ImageFile do defstruct [ path: :nil, resize: %Elxvips.ResizeOptions{ :width => :auto, :height => :auto, :resize_type => :auto, }, save: %Elxvips.SaveOptions{} ] end defmodule Elxvips.ImageBytes do defstruct [ bytes: nil, resize: %Elxvips.ResizeOptions{ :width => :auto, :height => :auto, :resize_type => :auto, }, save: %Elxvips.SaveOptions{}, ] end defmodule Elxvips do @moduledoc """ Documentation for `Elxvips`. """ use Rustler, otp_app: :elxvips, crate: "lvips" alias Elxvips.ImageFile, as: ImageFile alias Elxvips.ImageBytes, as: ImageBytes alias Elxvips.SaveOptions, as: SaveOptions # NIFs defp vips_set_concurrency(_a), do: :erlang.nif_error(:nif_not_loaded) defp vips_get_image_sizes(_a), do: :erlang.nif_error(:nif_not_loaded) # returns {:ok, { width, height } } defp vips_get_image_bytes_sizes(_a), do: :erlang.nif_error(:nif_not_loaded) # same but works with bytes defp vips_process_file_to_file(_a), do: :erlang.nif_error(:nif_not_loaded) # applies processing from %ImageFile{} defp vips_process_file_to_bytes(_a), do: :erlang.nif_error(:nif_not_loaded) # applies processing from %ImageBytes{} created from image path defp vips_process_bytes_to_bytes(_a), do: :erlang.nif_error(:nif_not_loaded) # applies processing from %ImageBytes{} created from image bytes defp vips_process_bytes_to_file(_a), do: :erlang.nif_error(:nif_not_loaded) # applies processing from %ImageBytes{} created from image bytes defp vips_get_image_file_format(_a), do: :erlang.nif_error(:nif_not_loaded) # applies processing from %ImageBytes{} created from image bytes defp vips_get_image_bytes_format(_a), do: :erlang.nif_error(:nif_not_loaded) # applies processing from %ImageBytes{} created from image bytes # creating new image from an existing image path defp process_to_file( image_file = %ImageFile{}, path ) when is_binary( path ) do image_file = %ImageFile{ image_file | :save => Kernel.struct( image_file.save, [ path: path ] ) } with :ok <- vips_process_file_to_file( image_file ) do { :ok, %ImageFile{ :path => path, } } else err -> err end end defp process_to_file( image_bytes = %ImageBytes{ :bytes => bytes }, path ) when is_binary( path ) and is_list( bytes ) do image_bytes = %ImageBytes{ image_bytes | :save => Kernel.struct( image_bytes.save, [ path: path ] ) } with :ok <- vips_process_bytes_to_file( image_bytes ) do { :ok, %ImageFile{ :path => path, } } else err -> err end end # In case the we have a image path as image_bytes defp process_to_bytes( image_file = %ImageFile{ :path => path } ) when is_binary( path ) do with { :ok, bytes } <- vips_process_file_to_bytes( image_file ) do { :ok, %ImageBytes{ :bytes => bytes, } } else err -> err end end # In case we have raw bytes defp process_to_bytes( image_bytes = %ImageBytes{ :bytes => bytes } ) when is_list( bytes ) do with { :ok, bytes } <- vips_process_bytes_to_bytes( image_bytes ) do { :ok, %ImageBytes{ :bytes => bytes, } } else err -> err end end defp format_merge( :background, a_val, b_val ) do case b_val do [] -> a_val _ -> b_val end end defp format_merge( _, _, b_val ) do b_val end defp format( image_file = %ImageFile{}, format , opts ) do with opts when is_list( opts ) <- check_opts( opts ) do { :ok, %ImageFile{ image_file | :save => Map.merge( image_file.save, Map.new( opts ++ [ format: format, path: "" ] ), &format_merge/3 ), } } end end defp format( image_bytes = %ImageBytes{}, format , opts ) do with opts when is_list( opts ) <- check_opts( opts ) do { :ok, %ImageBytes{ image_bytes | :save => Map.merge( image_bytes.save, Map.new( opts ++ [ format: format, path: "" ] ), &format_merge/3 ), } } end end defp format( { :ok, image }, format, opts ), do: format( image, format, opts ) def check_opts( opts \\ [] ) do with opts when is_list( opts ) <- check_background( opts ) do opts end end defp check_background( opts ) do with background_list when is_list( background_list ) <- background_opts( Keyword.get( opts, :background, [] ) ) do Keyword.put( opts, :background, background_list ) end end # vips expects a vector of f64, this should convert integers to float defp background_opts( [ c1 ] ), do: [ c1 / 1 ] defp background_opts( [ c1, c2, c3 ] ), do: [ c1 / 1, c2 / 1, c3 / 1 ] defp background_opts( [] ), do: [] defp background_opts( _ ), do: { :error, "background can be a vector of 1 or 3 numbers" } @doc """ Sets the background of the image in case there is a transparent background. Accepts a empty list, or a list of length 2 or 3. ## Examples iex> import Elxvips iex> iex> from_file( "test/input.png" ) iex |> jpg() iex> |> background( [ 255, 0, 0 ] ) # red background iex |> to_file( "test/output.jpg" ) {:ok, %ImageBytes{}} """ # def background( image = %ImageFile{}, c1 ), do: background( image, c1 ) # def background( image = %ImageFile{}, c1, c2, c3 ), do: background( image, c1 ) # def background( image = %ImageFile{}, c1 ), do: background( image, c1 ) # def background( image = %ImageFile{}, c1, c2, c3 ), do: background( image, c1 ) def background( image, opts \\ [] ) def background( image_file = %ImageFile{}, colors ) do with background_list when is_list( background_list ) <- background_opts( colors ) do { :ok, %ImageFile{ image_file | :save => %SaveOptions { image_file.save | :background => background_list, }, } } end end def background( image_bytes = %ImageBytes{}, colors ) do with background_list when is_list( background_list ) <- background_opts( colors ) do { :ok, %ImageBytes{ image_bytes | :save => %SaveOptions { image_bytes.save | :background => background_list, }, } } end end def background( { :ok, image_file = %ImageFile{} }, colors ), do: background( image_file, colors ) def background( { :ok, image_bytes = %ImageBytes{} }, colors ), do: background( image_bytes, colors ) @doc """ Applies resize options to an %ImageFile{} or %ImageBytes{}, accepts :width, :height and :type (not implemented yet). If no width or height is specified dimensions are calculated from the input image. Empty resize( no :width and no :height) will produce an image with the dimensions as the original one. ## Examples iex> import Elxvips iex> iex> from_file( "test/input.png" ) iex> |> resize( width: 300 ) iex |> to_bytes() {:ok, %ImageBytes{}} """ def resize( image_file, opts \\ [] ) def resize( image_file = %ImageFile{}, opts ) do { :ok, %ImageFile{ image_file | :resize => Kernel.struct( image_file.resize, opts ) } } end def resize( { :ok, image_file = %ImageFile{} }, opts ), do: resize( image_file, opts ) def resize( image_bytes = %ImageBytes{}, opts ) do { :ok, %ImageBytes{ image_bytes | :resize => Kernel.struct( image_bytes.resize, opts ) } } end def resize( { :ok, image_bytes = %ImageBytes{} }, opts ), do: resize( image_bytes, opts ) @save_opts_default [ quality: 100, strip: true, compression: 6, background: [] ] @jpg_default_opts Keyword.merge( @save_opts_default, [ quality: 90 ] ) @doc """ Will save the ImageFile in jpeg format to a specified path. Accepts quality and strip options. By default quality is set to 90 and strip to true. ## Examples iex> import Elxvips iex> iex> from_file( "/path/input.png" ) iex> |> jpg( strip: true, quality: 72 ) iex |> to_file( "/path/output.jpg" ) { :ok, %ImageFile{} } """ def jpg( image, opts \\ [] ) def jpg( image_file = %ImageFile{}, opts ), do: format( image_file, :jpg, Keyword.merge( @jpg_default_opts, opts ) ) def jpg( image_file = %ImageBytes{}, opts ), do: format( image_file, :jpg, Keyword.merge( @jpg_default_opts, opts ) ) def jpg( { :ok, image }, opts ), do: jpg( image, opts ) @png_default_opts Keyword.merge( @save_opts_default, [ quality: 100 ] ) @doc """ Will save the ImageFile in png format to a specified path. Accepts quality, compression(0-9) and strip options. By default quality is set to 100, compression to 6, strip to true. Decreasing compression will speed up image saving. ## Examples iex> import Elxvips iex> iex> from_file( "/path/input.jpg" ) iex> |> png( strip: true, quality: 72 ) iex |> to_file( "/path/output.png" ) { :ok, %ImageFile{} } """ def png( image, opts \\ [] ) def png( image_file = %ImageFile{}, opts ), do: format( image_file, :png, Keyword.merge( @png_default_opts, opts ) ) def png( image_file = %ImageBytes{}, opts ), do: format( image_file, :png, Keyword.merge( @png_default_opts, opts ) ) def png( { :ok, image }, opts ), do: png( image, opts ) @webp_default_opts Keyword.merge( @save_opts_default, [ quality: 100 ] ) @doc """ Will save the ImageFile in webp format to a specified path. Accepts quality and strip options. By default quality is set to 100, and strip to true. ## Examples iex> import Elxvips iex> iex> from_file( "/path/input.jpg" ) iex> |> webp( strip: true, quality: 72 ) iex |> to_file( "/path/output.webp" ) { :ok, %ImageFile{} } """ def webp( image, opts \\ [] ) def webp( image_file = %ImageFile{}, opts ), do: format( image_file, :webp, Keyword.merge( @webp_default_opts, opts ) ) def webp( image_file = %ImageBytes{}, opts ), do: format( image_file, :webp, Keyword.merge( @webp_default_opts, opts ) ) def webp( { :ok, image }, opts ), do: webp( image, opts ) @doc """ Will create an %ImageFile{} struct from path. This struct will be used for further processing. ## Examples iex> import Elxvips iex> iex> from_file( "/path/input.png" ) %ImageFile{} """ def from_file( path ) when is_binary( path ) do { :ok, %ImageFile{ :path => path, } } end @doc """ Will create an %ImageByte{} struct from bitstring or byte list. This struct will be used for further processing. ## Examples iex> import Elxvips iex> iex> file = File.open!( "/path/input.png" ) iex> bytes = IO.binread( file, :all ) iex> from_bytes( bytes ) %ImageBytes{} """ def from_bytes( bytes ) when is_list( bytes ) do { :ok, %ImageBytes{ :bytes => bytes, } } end def from_bytes( bytes ) when is_bitstring( bytes ) do { :ok, %ImageBytes{ :bytes => :erlang.binary_to_list( bytes ), } } end @doc """ Will create a new %ImageBytes{} struct containing all the changes. ## Examples iex> import Elxvips iex> iex> from_file( "test/input.png" ) iex> |> png() iex> |> to_bytes() {:ok, %ImageBytes{}} """ def to_bytes( image = %ImageFile{} ), do: process_to_bytes( image ) def to_bytes( image = %ImageBytes{} ), do: process_to_bytes( image ) def to_bytes( { :ok, image } ), do: to_bytes( image ) @doc """ Will save the image to a path on disk and return a new %ImageFile{} from the new path. ## Examples iex> import Elxvips iex> iex> from_file( "test/input.png" ) iex> |> png() iex> |> to_file( "test/outping.png" ) {:ok, %ImageBytes{}} """ def to_file( image = %ImageFile{}, path ) when is_binary( path ), do: process_to_file( image, path ) def to_file( image = %ImageBytes{}, path ) when is_binary( path ), do: process_to_file( image, path ) def to_file( { :ok, image }, path ), do: to_file( image, path ) def set_concurrency( concurrency ) when is_integer( concurrency ) do vips_set_concurrency( concurrency ) end @doc """ Returns dimensions of the specified image, works with a image path or bytes. ## Examples iex> import Elxvips iex> iex> from_file( "test/input.png" ) iex> |> get_image_sizes() {:ok, [640, 486]} """ def get_image_sizes( path ) when is_binary( path ), do: vips_get_image_sizes( path ) def get_image_sizes( bytes ) when is_list( bytes ), do: vips_get_image_bytes_sizes( bytes ) def get_image_sizes( %ImageFile{ :path => path } ), do: get_image_sizes( path ) def get_image_sizes( {:ok, %ImageFile{ :path => path } } ), do: get_image_sizes( path ) def get_image_sizes( %ImageBytes{ :bytes => bytes } ) when is_list( bytes ), do: get_image_sizes( bytes ) def get_image_sizes( { :ok, image_bytes = %ImageBytes{} } ), do: get_image_sizes( image_bytes ) @doc """ Returns format of the specified image, works with a image path or bytes. ## Examples iex> import Elxvips iex> iex> from_file( "test/input.png" ) iex> |> get_image_format() {:ok, :png} """ def get_image_format( path ) when is_binary( path ), do: vips_get_image_file_format( path ) def get_image_format( bytes ) when is_list( bytes ), do: vips_get_image_bytes_format( bytes ) def get_image_format( %ImageFile{ :path => path } ), do: get_image_format( path ) def get_image_format( {:ok, %ImageFile{ :path => path } } ), do: get_image_format( path ) def get_image_format( %ImageBytes{ :bytes => bytes } ) when is_list( bytes ), do: get_image_format( bytes ) def get_image_format( { :ok, image_bytes = %ImageBytes{} } ), do: get_image_format( image_bytes ) end