defmodule Gmex do @moduledoc """ A simple wrapper for GraphicsMagick in Elixir. """ @default_options [ gm_path: "gm" ] @type image :: { :ok, Gmex.Image } @type gmex_error :: { :error, any() } @type image_info :: [ width: Integer.t, height: Integer.t, size: String.t, format: String.t, quality: Integer.t ] @type open_options :: [ gm_path: String.t() ] @doc false def test_gm( options \\ [] ) do final_options = Keyword.merge( @default_options, options ) executable = Keyword.get( final_options, :gm_path ) if System.find_executable( executable ) == nil do { :error, "graphicsmagick executable not found at:#{executable}" } else { :ok, executable } end end @doc """ Opens image source. ## Options * `:gm_path` - path to GraphicsMagick executable, defaults to `gm`, if the executable is missing an error will be returned. ## Example iex> Gmex.open( "test/images/blossom.jpg" ) { :ok, %Gmex.Image{ image: "test/images/blossom.jpg", options: [ "gm" ] } } iex> Gmex.open( "test/images/blossom.jpg", gm_path: "/404/gm" ) { :error, "graphicsmagick executable not found at:/404/gm" } iex> Gmex.open( "non-existing.png" ) { :error, :enoent } """ @spec open( String.t(), [ open_options ] ) :: image | gmex_error def open( src_path, options \\ [] ) do with { :ok, executable } <- test_gm( options ) do if File.exists?( src_path ) do { :ok, %Gmex.Image{ image: src_path, options: [ executable ] } } else { :error, :enoent } end end end @doc """ Saves the modified image ## Example iex> Gmex.open( "test/images/blossom.jpg" ) iex> |> Gmex.save( "newimage.jpg" ) { :ok, nil } """ @spec save( image, String.t() ) :: image | gmex_error def save( image, dest_path ) do with { :ok, image_struct } <- image do [ executable | final_options ] = image_struct.options final_options = [ "convert" | [ image_struct.image | final_options ] ] ++ [ dest_path ] { result, status_code } = System.cmd executable, final_options , stderr_to_stdout: true result = result |> String.replace( "\r", "" ) |> String.replace( "\n", "" ) if status_code == 0 do { :ok, nil } else { :error, result } end end end @doc """ Returns a keywords list with information about the image like width, height, size, format and quality. """ @spec get_info( image ) :: { :ok, image_info } | gmex_error def get_info( image ) do with { :ok, image_struct } <- image do [ executable | _ ] = image_struct.options { image_data, status_code } = System.cmd executable, [ "identify", "-format", "width=%w,height=%h,size=%b,format=%m,quality=%Q", image_struct.image ], stderr_to_stdout: true image_data = image_data |> String.replace( "\r", "" ) |> String.replace( "\n", "" ) if status_code == 0 do [ :ok, String.split( image_data, "," ) |> Enum.reduce( [], fn ( row, acc ) -> [ field , value ] = String.split( row, "=" ) case field do "width" -> width = value |> String.to_integer() acc ++ [ width: width ] "height" -> width = value |> String.to_integer() acc ++ [ height: width ] "format" -> format = value |> String.downcase() |> String.to_atom() acc ++ [ format: format ] "quality" -> quality = value |> String.to_integer() acc ++ [ compression_quality: quality ] "size" -> acc ++ [ size: value ] _ -> acc end end ) ] else { :error, image_data } end end end @doc """ Apply a GraphicsMagick option to the given image. ## Example iex> Gmex.open( "test/images/blossom.jpg" ) iex> |> Gmex.option( :negate ) iex> |> Gmex.option( { :resize, 50, 50 } ) iex> |> Gmex.option( :strip ) iex> |> Gmex.option( { :format, "jpg" } ) { :ok, %Gmex.Image{ image: "test/images/blossom.jpg", options: [ "gm", "-negate", "-resize", "50x50", "-strip", "-format", "jpg" ] } } List of available options: | Option | GraphicsMagick | | ---- | ---- | | :plus_adjoin | +adjoin | | :adjoin | -adjoin | | { :blur, radius, sigma } | -blur radiusxsigma | | { :blur, radius } | -blur radius | | { :crop, width, height, x_offset, y_offset } | -crop widthxheight+x_offset+y_offset | | { :crop, width, height } | -crop widthxheight | | { :edge, edge } | -edge edge | | { :extent, width, height, x_offset, y_offset } | -extent widthxheight+x_offset+y_offset | | { :extent, width, height } | -extent widthxheight | | flatten | -flatten | | { :fill, color } | -fill color | | :strip | -strip | | :flip | -flip | | { :format, format } | -format format | | { :gravity, gravity } | -gravity gravity | | magnify | -magnify | | plus_matte | +matte | | matte | -matte | | negate | -negate | | { :opaque, color } | -opaque color | | { :quality, quality } | -quality quality | | { :resize, width, height } | -resize widthxheight | | { :resize, percents } | -resize percents% | | { :rotate, degrees } | -rotate degrees | | { :size, width, height } | -size widthxheight | | { :size, width, height, offset } | -size widthxheight+offset | | { :thumbnail, width, height } | -thumbnail widthxheight | | { :thumbnail, percents } | -thumbnail percents% | | { :transparent, color } | -transparent color | | { :type, type } | -type type | | { :custom, [ arg1, arg2, arg3... ] } | arg1 arg2 arg3 ... | """ @spec option( image, Option ) :: image | gmex_error def option( image, option ) do with { :ok, image } <- image do new_option = case option do :plus_adjoin -> [ "+adjoin" ] :adjoin -> [ "-adjoin" ] { :background, color } -> [ "-background", color ] { :blur, radius, sigma } -> [ "-blur", "#{radius}x#{sigma}" ] { :blur, radius } -> [ "-blur", "#{radius}" ] { :crop, width, height } -> [ "-crop", "#{width}x#{height}" ] { :crop, width, height, x_offset, y_offset } -> x_offset = if x_offset > 0, do: "+#{x_offset}", else: x_offset y_offset = if y_offset > 0, do: "+#{y_offset}", else: y_offset [ "-crop", "#{width}x#{height}#{x_offset}#{y_offset}" ] { :edge, radius } -> [ "-edge", radius ] { :extent, width, height } -> [ "-extent", "#{width}x#{height}" ] { :extent, width, height, x_offset, y_offset } -> x_offset = if x_offset > 0, do: "+#{x_offset}", else: x_offset y_offset = if y_offset > 0, do: "+#{y_offset}", else: y_offset [ "-extent", "#{width}x#{height}#{x_offset}#{y_offset}" ] :flatten -> [ "-flatten" ] { :fill, color } -> [ "-flatten", color ] :strip -> [ "-strip" ] :flip -> [ "-flip" ] { :format, format } -> [ "-format", format ] { :gravity, gravity } -> [ "-gravity", gravity ] :magnify -> [ "magnify" ] :plus_matte -> [ "+matte" ] :matte -> [ "-matte" ] :negate -> [ "-negate" ] { :opaque, color } -> [ "-opaque", color ] { :quality, quality } -> [ "-quality", quality ] { :resize, width, height } -> [ "-resize", "#{width}x#{height}" ] { :resize, percents } -> [ "-resize", "#{percents}%" ] { :rotate, degrees } -> [ "-rotate", degrees ] { :size, width, height } -> [ "-size", "#{width}x#{height}" ] { :size, width, height, offset } -> [ "-size", "#{width}x#{height}+#{offset}" ] { :thumbnail, width, height } -> [ "-thumbnail", "#{width}x#{height}" ] { :thumbnail, percents } -> [ "-thumbnail", "#{percents}%" ] { :transparent, color } -> [ "-transparent", color ] { :type, type } -> [ "-type", type ] { :custom, other_options } -> other_options _ -> :unknown_option end if new_option == :unknown_option do { :error, :unknown_option } else { :ok, Gmex.Image.append_option( image, new_option ) } end end end end