Current section

Files

Jump to
gmex lib gmex.ex
Raw

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 ]
@type option_param :: String.t | Integer.t | Float.t
@type option :: [ adjoin: boolean ] |
[ blur: { option_param, option_param } ] |
[ blur: option_param ] |
[ crop: { option_param, option_param, option_param, option_param } ] |
[ crop: { option_param, option_param } ] |
[ edge: option_param ] |
[ extent: { option_param, option_param, option_param, option_param } ] |
[ extent: { option_param, option_param } ] |
[ flatten: boolean ] |
[ fill: String.t ] |
[ strip: boolean ] |
[ format: String.t ] |
[ gravity: String.t ] |
[ magnify: boolean ] |
[ matte: boolean ] |
[ negate: true ] |
[ opaque: String.t ] |
[ quality: Integer.t ] |
[ resize: { option_param, option_param } ] |
[ resize: Integer.t ] |
[ rotate: Integer.t ] |
[ size: { option_param, option_param } ] |
[ size: { option_param, option_param, option_param } ] |
[ thumbnail: { :thumbnail, option_param, option_param } ] |
[ thumbnail: { :thumbnail, option_param } ] |
[ transparent: String.t ] |
[ type: String.t ] |
[ custom: list( option_param ) ]
@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
|> options(
resize: { resize_width, resize_height },
gravity: "center",
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: true )
iex> |> Gmex.option( resize: { 50, 50 } )
iex> |> Gmex.option( strip: true )
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 |
| ---- | ---- |
| adjoin: true | +adjoin |
| adjoin: false | -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: true | -flatten |
| fill: color | -fill color |
| strip: true | -strip |
| flip: true | -flip |
| format: format } | -format format |
| gravity: gravity | -gravity gravity |
| magnify: true | -magnify |
| matte: true | +matte |
| matte: false | -matte |
| negate: true | -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: { :thumbnail, width, height } | -thumbnail widthxheight |
| thumbnail: { :thumbnail, percents } | -thumbnail percents% |
| transparent: color | -transparent color |
| type: type | -type type |
| custom: [ arg1, arg2, arg3... ] | arg1 arg2 arg3 ... |
"""
@spec option( image, option ) :: image
def option( { :ok, image = %Gmex.Image{} }, [ adjoin: true ] ) do
{ :ok, Gmex.Image.append_option( image, [ "+adjoin" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ adjoin: false ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-adjoin" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ background: color ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-background", "#{color}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ blur: radius ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-blur", "#{radius}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ blur: { radius, sigma } ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-blur", "#{radius}x#{sigma}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ crop: { width, height } ] ) do
width = Kernel.round( width )
height = Kernel.round( height )
{ :ok, Gmex.Image.append_option( image, [ "-crop", "#{width}x#{height}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ crop: { width, height, x_offset, y_offset } ] ) do
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
{ :ok, Gmex.Image.append_option( image, [ "-crop", "#{width}x#{height}#{x_offset}#{y_offset}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ edge: radius ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-edge", "#{radius}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ extent: { width, height } ] ) do
width = Kernel.round( width )
height = Kernel.round( height )
{ :ok, Gmex.Image.append_option( image, [ "-extent", "#{width}x#{height}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ extent: { width, height, x_offset, y_offset } ] ) do
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
{ :ok, Gmex.Image.append_option( image, [ "-extent", "#{width}x#{height}#{x_offset}#{y_offset}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ flatten: true ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-flatten" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ fill: color ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-flatten", "#{color}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ strip: true ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-strip" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ flip: true ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-flip" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ format: format ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-format", "#{format}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ gravity: gravity ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-gravity", "#{gravity}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ magnify: true ] ) do
{ :ok, Gmex.Image.append_option( image, [ "magnify" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ matte: true ] ) do
{ :ok, Gmex.Image.append_option( image, [ "+matte" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ matte: false ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-matte" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ negate: true ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-negate" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ opaque: color ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-opaque", "#{color}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ quality: quality ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-quality", "#{quality}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ resize: { width, height } ] ) do
width = Kernel.round( width )
height = Kernel.round( height )
{ :ok, Gmex.Image.append_option( image, [ "-resize", "#{width}x#{height}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ resize: percents ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-resize", "#{percents}%" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ rotate: degrees ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-rotate", "#{degrees}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ size: { width, height } ] ) do
width = Kernel.round( width )
height = Kernel.round( height )
{ :ok, Gmex.Image.append_option( image, [ "-size", "#{width}x#{height}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ size: { width, height, offset } ] ) do
width = Kernel.round( width )
height = Kernel.round( height )
offset = Kernel.round( offset )
{ :ok, Gmex.Image.append_option( image, [ "-size", "#{width}x#{height}+#{offset}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ thumbnail: { width, height } ] ) do
width = Kernel.round( width )
height = Kernel.round( height )
{ :ok, Gmex.Image.append_option( image, [ "-thumbnail", "#{width}x#{height}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ thumbnail: percents ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-thumbnail", "#{percents}%" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ transparent: color ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-transparent", "#{color}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ type: type ] ) do
{ :ok, Gmex.Image.append_option( image, [ "-type", "#{type}" ] ) }
end
def option( { :ok, image = %Gmex.Image{} }, [ custom: other_options ] ) when is_list( other_options ) do
new_options = other_options
|> Enum.map( fn( option ) -> "#{option}" end )
{ :ok, Gmex.Image.append_option( image, new_options ) }
end
def option( { :ok, _image = %Gmex.Image{} }, _option ) do
{ :error, :unknown_option }
end
@doc """
Apply a list GraphicsMagick option to the given image.
## Example
iex> Gmex.open( "test/images/blossom.jpg" )
iex> |> Gmex.options( negate: true, resize: { 50, 50 }, strip: true, format: "jpg" )
{ :ok, %Gmex.Image{ image: "test/images/blossom.jpg", options: [ "gm", "-negate", "-resize", "50x50", "-strip", "-format", "jpg" ] } }
"""
@spec options( image, option ) :: image | gmex_error
def options( { :ok, image = %Gmex.Image{} }, [ option | other_options ] ) do
with { :ok, image } <- option( { :ok, image }, [ option ] ) do
if length( other_options ) === 0 do
{ :ok, image }
else
options( { :ok, image }, other_options )
end
end
end
end