Current section
Files
Jump to
Current section
Files
lib/gmex.ex
defmodule Gmex do
@moduledoc """
A simple wrapper for GraphicsMagick in Elixir.
"""
@default_open_options [
gm_path: "gm"
]
@default_resize_options [
width: :auto,
height: :auto,
resize: :fill
]
@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 resize_options :: [ width: Integer.t, height: Integer.t, type: :fill | :fit ]
@type open_options :: [ gm_path: String.t() ]
@doc false
def test_gm( options \\ [] ) do
final_options = Keyword.merge( @default_open_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 """
Resizes image
## Options
* `:width` - (Optional) width of the resized image, if not specified will be calculated based on proportions.
* `:height` - (Optional) height of the resized image, if not specified will be calculated based on proportions.
* `:type` - (Optional) resize type, can be either :fill or :fit, defaults to :fill.
* `:fill` - Generates images of the specified size with cropping.
* `:fit` - Generates an image that will fit in the specified size, no cropping.
## Example
iex> Gmex.open( "test/images/blossom.jpg" )
iex> |> Gmex.resize( width: 300, height: 200, type: :fill )
iex> |> Gmex.save( "newimage.jpg" )
{ :ok, nil }
"""
@spec resize( image, resize_options ) :: image | gmex_error
def resize( image, options \\ [] ) do
with { :ok, _ } <- image
do
options = Keyword.merge( @default_resize_options, options )
{ _ ,image_data } = image |> get_info
src_width = image_data |> Keyword.get( :width )
src_height = image_data |> Keyword.get( :height )
tar_width = options |> Keyword.get( :width, :auto )
tar_height = options |> Keyword.get( :height, :auto )
src_ratio = src_width / src_height
resize_type = options |> Keyword.get( :type, :fill )
tar_width = cond do
tar_width == :auto and tar_height == :auto -> src_width
tar_width == :auto and tar_height != :auto -> src_width * tar_height / src_height
true -> tar_width
end
tar_height = cond do
tar_height == :auto and tar_width == :auto -> src_width
tar_height == :auto and tar_width != :auto -> src_height * tar_width / src_width
true -> tar_height
end
tar_ratio = tar_width / tar_height
case resize_type do
:fill ->
{ resize_width, resize_height } = if src_ratio >= tar_ratio do
{ src_width / ( src_height / tar_height ), tar_height }
else
{ tar_width, src_height / ( src_width / tar_width ) }
end
image
|> option( { :resize, resize_width, resize_height } )
|> option( { :gravity, "center" } )
|> option( { :crop, tar_width, tar_height, 0, 0 } )
:fit ->
image
|> option( { :resize, tar_width, tar_height } )
_ -> { :error, "unknown resize type" }
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 } ->
width = Kernel.round( width )
height = Kernel.round( height )
[ "-crop", "#{width}x#{height}" ]
{ :crop, width, height, x_offset, y_offset } ->
width = Kernel.round( width )
height = Kernel.round( height )
x_offset = Kernel.round( x_offset )
y_offset = Kernel.round( 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 } ->
width = Kernel.round( width )
height = Kernel.round( height )
[ "-extent", "#{width}x#{height}" ]
{ :extent, width, height, x_offset, y_offset } ->
width = Kernel.round( width )
height = Kernel.round( height )
x_offset = Kernel.round( x_offset )
y_offset = Kernel.round( 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 } ->
width = Kernel.round( width )
height = Kernel.round( height )
[ "-resize", "#{width}x#{height}" ]
{ :resize, percents } ->
[ "-resize", "#{percents}%" ]
{ :rotate, degrees } ->
[ "-rotate", "#{degrees}" ]
{ :size, width, height } ->
width = Kernel.round( width )
height = Kernel.round( height )
[ "-size", "#{width}x#{height}" ]
{ :size, width, height, offset } ->
width = Kernel.round( width )
height = Kernel.round( height )
offset = Kernel.round( offset )
[ "-size", "#{width}x#{height}+#{offset}" ]
{ :thumbnail, width, height } ->
width = Kernel.round( width )
height = Kernel.round( height )
[ "-thumbnail", "#{width}x#{height}" ]
{ :thumbnail, percents } ->
[ "-thumbnail", "#{percents}%" ]
{ :transparent, color } ->
[ "-transparent", "#{color}" ]
{ :type, type } ->
[ "-type", "#{type}" ]
{ :custom, other_options } ->
other_options
|> Enum.map( fn( option ) -> "#{option}" end )
_ -> :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