125 changes: 67 additions & 58 deletions lib/dragonfly/image_magick/generators/text.rb
Original file line number Diff line number Diff line change
@@ -1,100 +1,111 @@
require 'dragonfly/hash_with_css_style_keys'
require "dragonfly/hash_with_css_style_keys"
require "dragonfly/image_magick/commands"
require "dragonfly/param_validators"

module Dragonfly
module ImageMagick
module Generators
class Text
include ParamValidators

FONT_STYLES = {
'normal' => 'normal',
'italic' => 'italic',
'oblique' => 'oblique'
"normal" => "normal",
"italic" => "italic",
"oblique" => "oblique",
}

FONT_STRETCHES = {
'normal' => 'normal',
'semi-condensed' => 'semi-condensed',
'condensed' => 'condensed',
'extra-condensed' => 'extra-condensed',
'ultra-condensed' => 'ultra-condensed',
'semi-expanded' => 'semi-expanded',
'expanded' => 'expanded',
'extra-expanded' => 'extra-expanded',
'ultra-expanded' => 'ultra-expanded'
"normal" => "normal",
"semi-condensed" => "semi-condensed",
"condensed" => "condensed",
"extra-condensed" => "extra-condensed",
"ultra-condensed" => "ultra-condensed",
"semi-expanded" => "semi-expanded",
"expanded" => "expanded",
"extra-expanded" => "extra-expanded",
"ultra-expanded" => "ultra-expanded",
}

FONT_WEIGHTS = {
'normal' => 'normal',
'bold' => 'bold',
'bolder' => 'bolder',
'lighter' => 'lighter',
'100' => 100,
'200' => 200,
'300' => 300,
'400' => 400,
'500' => 500,
'600' => 600,
'700' => 700,
'800' => 800,
'900' => 900
"normal" => "normal",
"bold" => "bold",
"bolder" => "bolder",
"lighter" => "lighter",
"100" => 100,
"200" => 200,
"300" => 300,
"400" => 400,
"500" => 500,
"600" => 600,
"700" => 700,
"800" => 800,
"900" => 900,
}

def update_url(url_attributes, string, opts={})
IS_COLOUR = ->(param) {
/\A(#\w+|rgba?\([\d\.,]+\)|\w+)\z/ === param
}

def update_url(url_attributes, string, opts = {})
url_attributes.name = "text.#{extract_format(opts)}"
end

def call(content, string, opts={})
def call(content, string, opts = {})
validate_all_keys!(opts, %w(font font_family), &is_words)
validate_all_keys!(opts, %w(color background_color stroke_color), &IS_COLOUR)
validate!(opts["format"], &is_word)

opts = HashWithCssStyleKeys[opts]
args = []
format = extract_format(opts)
background = opts['background_color'] || 'none'
font_size = (opts['font_size'] || 12).to_i
background = opts["background_color"] || "none"
font_size = (opts["font_size"] || 12).to_i
font_family = opts["font_family"] || opts["font"]
escaped_string = "\"#{string.gsub(/"/, '\"')}\""

# Settings
args.push("-gravity NorthWest")
args.push("-antialias")
args.push("-pointsize #{font_size}")
args.push("-font \"#{opts['font']}\"") if opts['font']
args.push("-family '#{opts['font_family']}'") if opts['font_family']
args.push("-fill #{opts['color']}") if opts['color']
args.push("-stroke #{opts['stroke_color']}") if opts['stroke_color']
args.push("-style #{FONT_STYLES[opts['font_style']]}") if opts['font_style']
args.push("-stretch #{FONT_STRETCHES[opts['font_stretch']]}") if opts['font_stretch']
args.push("-weight #{FONT_WEIGHTS[opts['font_weight']]}") if opts['font_weight']
args.push("-family '#{font_family}'") if font_family
args.push("-fill #{opts["color"]}") if opts["color"]
args.push("-stroke #{opts["stroke_color"]}") if opts["stroke_color"]
args.push("-style #{FONT_STYLES[opts["font_style"]]}") if opts["font_style"]
args.push("-stretch #{FONT_STRETCHES[opts["font_stretch"]]}") if opts["font_stretch"]
args.push("-weight #{FONT_WEIGHTS[opts["font_weight"]]}") if opts["font_weight"]
args.push("-background #{background}")
args.push("label:#{escaped_string}")

# Padding
pt, pr, pb, pl = parse_padding_string(opts['padding']) if opts['padding']
padding_top = (opts['padding_top'] || pt || 0)
padding_right = (opts['padding_right'] || pr || 0)
padding_bottom = (opts['padding_bottom'] || pb || 0)
padding_left = (opts['padding_left'] || pl || 0)
pt, pr, pb, pl = parse_padding_string(opts["padding"]) if opts["padding"]
padding_top = (opts["padding_top"] || pt).to_i
padding_right = (opts["padding_right"] || pr).to_i
padding_bottom = (opts["padding_bottom"] || pb).to_i
padding_left = (opts["padding_left"] || pl).to_i

content.generate!(:convert, args.join(' '), format)
Commands.generate(content, args.join(" "), format)

if (padding_top || padding_right || padding_bottom || padding_left)
dimensions = content.analyse(:image_properties)
text_width = dimensions['width']
text_height = dimensions['height']
width = padding_left + text_width + padding_right
height = padding_top + text_height + padding_bottom
text_width = dimensions["width"]
text_height = dimensions["height"]
width = padding_left + text_width + padding_right
height = padding_top + text_height + padding_bottom

args = args.slice(0, args.length - 2)
args.push("-size #{width}x#{height}")
args.push("xc:#{background}")
args.push("-annotate 0x0+#{padding_left}+#{padding_top} #{escaped_string}")
content.generate!(:convert, args.join(' '), format)
Commands.generate(content, args.join(" "), format)
end

content.add_meta('format' => format, 'name' => "text.#{format}")
content.add_meta("format" => format, "name" => "text.#{format}")
end

private

def extract_format(opts)
opts['format'] || 'png'
opts["format"] || "png"
end

# Use css-style padding declaration, i.e.
Expand All @@ -103,25 +114,23 @@ def extract_format(opts)
# 10 5 10 (top, left/right, bottom)
# 10 5 10 5 (top, right, bottom, left)
def parse_padding_string(str)
padding_parts = str.gsub('px','').split(/\s+/).map{|px| px.to_i}
padding_parts = str.gsub("px", "").split(/\s+/).map { |px| px.to_i }
case padding_parts.size
when 1
p = padding_parts.first
[p,p,p,p]
[p, p, p, p]
when 2
p,q = padding_parts
[p,q,p,q]
p, q = padding_parts
[p, q, p, q]
when 3
p,q,r = padding_parts
[p,q,r,q]
p, q, r = padding_parts
[p, q, r, q]
when 4
padding_parts
else raise ArgumentError, "Couldn't parse padding string '#{str}' - should be a css-style string"
end
end
end

end
end
end

45 changes: 20 additions & 25 deletions lib/dragonfly/image_magick/plugin.rb
Original file line number Diff line number Diff line change
@@ -1,49 +1,48 @@
require 'dragonfly/image_magick/analysers/image_properties'
require 'dragonfly/image_magick/generators/convert'
require 'dragonfly/image_magick/generators/plain'
require 'dragonfly/image_magick/generators/plasma'
require 'dragonfly/image_magick/generators/text'
require 'dragonfly/image_magick/processors/convert'
require 'dragonfly/image_magick/processors/encode'
require 'dragonfly/image_magick/processors/thumb'
require "dragonfly/image_magick/analysers/image_properties"
require "dragonfly/image_magick/generators/plain"
require "dragonfly/image_magick/generators/plasma"
require "dragonfly/image_magick/generators/text"
require "dragonfly/image_magick/processors/encode"
require "dragonfly/image_magick/processors/thumb"
require "dragonfly/image_magick/commands"
require "dragonfly/param_validators"

module Dragonfly
module ImageMagick

# The ImageMagick Plugin registers an app with generators, analysers and processors.
# Look at the source code for #call to see exactly how it configures the app.
class Plugin

def call(app, opts={})
def call(app, opts = {})
# ENV
app.env[:convert_command] = opts[:convert_command] || 'convert'
app.env[:identify_command] = opts[:identify_command] || 'identify'
app.env[:convert_command] = opts[:convert_command] || "convert"
app.env[:identify_command] = opts[:identify_command] || "identify"

# Analysers
app.add_analyser :image_properties, ImageMagick::Analysers::ImageProperties.new
app.add_analyser :width do |content|
content.analyse(:image_properties)['width']
content.analyse(:image_properties)["width"]
end
app.add_analyser :height do |content|
content.analyse(:image_properties)['height']
content.analyse(:image_properties)["height"]
end
app.add_analyser :format do |content|
content.analyse(:image_properties)['format']
content.analyse(:image_properties)["format"]
end
app.add_analyser :aspect_ratio do |content|
attrs = content.analyse(:image_properties)
attrs['width'].to_f / attrs['height']
attrs["width"].to_f / attrs["height"]
end
app.add_analyser :portrait do |content|
attrs = content.analyse(:image_properties)
attrs['width'] <= attrs['height']
attrs["width"] <= attrs["height"]
end
app.add_analyser :landscape do |content|
!content.analyse(:portrait)
end
app.add_analyser :image do |content|
begin
content.analyse(:image_properties)['format'] != 'pdf'
content.analyse(:image_properties)["format"] != "pdf"
rescue Shell::CommandFailed
false
end
Expand All @@ -55,29 +54,25 @@ def call(app, opts={})
app.define(:image?) { image }

# Generators
app.add_generator :convert, ImageMagick::Generators::Convert.new
app.add_generator :plain, ImageMagick::Generators::Plain.new
app.add_generator :plasma, ImageMagick::Generators::Plasma.new
app.add_generator :text, ImageMagick::Generators::Text.new

# Processors
app.add_processor :convert, Processors::Convert.new
app.add_processor :encode, Processors::Encode.new
app.add_processor :thumb, Processors::Thumb.new
app.add_processor :rotate do |content, amount|
content.process!(:convert, "-rotate #{amount}")
ParamValidators.validate!(amount, &ParamValidators.is_number)
Commands.convert(content, "-rotate #{amount}")
end

# Extra methods
app.define :identify do |cli_args=nil|
app.define :identify do |cli_args = nil|
shell_eval do |path|
"#{app.env[:identify_command]} #{cli_args} #{path}"
end
end

end

end
end
end

33 changes: 0 additions & 33 deletions lib/dragonfly/image_magick/processors/convert.rb

This file was deleted.

21 changes: 16 additions & 5 deletions lib/dragonfly/image_magick/processors/encode.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
require "dragonfly/image_magick/commands"

module Dragonfly
module ImageMagick
module Processors
class Encode
include ParamValidators

WHITELISTED_ARGS = %w(quality)

IS_IN_WHITELISTED_ARGS = ->(args_string) {
args_string.scan(/-\w+/).all? { |arg|
WHITELISTED_ARGS.include?(arg.sub("-", ""))
}
}

def update_url(attrs, format, args="")
def update_url(attrs, format, args = "")
attrs.ext = format.to_s
end

def call(content, format, args="")
content.process!(:convert, args, 'format' => format)
def call(content, format, args = "")
validate!(format, &is_word)
validate!(args, &IS_IN_WHITELISTED_ARGS)
Commands.convert(content, args, "format" => format)
end

end
end
end
end

68 changes: 37 additions & 31 deletions lib/dragonfly/image_magick/processors/thumb.rb
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
require "dragonfly/image_magick/commands"

module Dragonfly
module ImageMagick
module Processors
class Thumb
include ParamValidators

GRAVITIES = {
'nw' => 'NorthWest',
'n' => 'North',
'ne' => 'NorthEast',
'w' => 'West',
'c' => 'Center',
'e' => 'East',
'sw' => 'SouthWest',
's' => 'South',
'se' => 'SouthEast'
"nw" => "NorthWest",
"n" => "North",
"ne" => "NorthEast",
"w" => "West",
"c" => "Center",
"e" => "East",
"sw" => "SouthWest",
"s" => "South",
"se" => "SouthEast",
}

# Geometry string patterns
RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
CROPPED_RESIZE_GEOMETRY = /\A(\d+)x(\d+)#(\w{1,2})?\z/ # e.g. '20x50#ne'
CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'
CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'

def update_url(url_attributes, geometry, opts={})
format = opts['format']
def update_url(url_attributes, geometry, opts = {})
format = opts["format"]
url_attributes.ext = format if format
end

def call(content, geometry, opts={})
content.process!(:convert, args_for_geometry(geometry), opts)
def call(content, geometry, opts = {})
validate!(opts["format"], &is_word)
validate!(opts["frame"], &is_number)
Commands.convert(content, args_for_geometry(geometry), {
"format" => opts["format"],
"frame" => opts["frame"],
})
end

def args_for_geometry(geometry)
Expand All @@ -37,11 +45,11 @@ def args_for_geometry(geometry)
resize_and_crop_args($1, $2, $3)
when CROP_GEOMETRY
crop_args(
'width' => $1,
'height' => $2,
'x' => $3,
'y' => $4,
'gravity' => $5
"width" => $1,
"height" => $2,
"x" => $3,
"y" => $4,
"gravity" => $5,
)
else raise ArgumentError, "Didn't recognise the geometry string #{geometry}"
end
Expand All @@ -54,26 +62,24 @@ def resize_args(geometry)
end

def crop_args(opts)
raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts['x'] && opts['gravity']
raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts["x"] && opts["gravity"]

width = opts['width']
height = opts['height']
gravity = GRAVITIES[opts['gravity']]
x = "#{opts['x'] || 0}"
x = '+' + x unless x[/\A[+-]/]
y = "#{opts['y'] || 0}"
y = '+' + y unless y[/\A[+-]/]
width = opts["width"]
height = opts["height"]
gravity = GRAVITIES[opts["gravity"]]
x = "#{opts["x"] || 0}"
x = "+" + x unless x[/\A[+-]/]
y = "#{opts["y"] || 0}"
y = "+" + y unless y[/\A[+-]/]

"#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} +repage"
end

def resize_and_crop_args(width, height, gravity)
gravity = GRAVITIES[gravity || 'c']
gravity = GRAVITIES[gravity || "c"]
"-resize #{width}x#{height}^^ -gravity #{gravity} -crop #{width}x#{height}+0+0 +repage"
end

end
end
end
end

37 changes: 37 additions & 0 deletions lib/dragonfly/param_validators.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module Dragonfly
module ParamValidators
class InvalidParameter < RuntimeError; end

module_function

IS_NUMBER = ->(param) {
param.is_a?(Numeric) || /\A[\d\.]+\z/ === param
}

IS_WORD = ->(param) {
/\A\w+\z/ === param
}

IS_WORDS = ->(param) {
/\A[\w ]+\z/ === param
}

def is_number; IS_NUMBER; end
def is_word; IS_WORD; end
def is_words; IS_WORDS; end

def validate!(parameter, &validator)
return if parameter.nil?
raise InvalidParameter unless validator.(parameter)
end

def validate_all!(parameters, &validator)
parameters.each { |p| validate!(p, &validator) }
end

def validate_all_keys!(obj, keys, &validator)
parameters = keys.map { |key| obj[key] }
validate_all!(parameters, &validator)
end
end
end
98 changes: 98 additions & 0 deletions spec/dragonfly/image_magick/commands_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
require "spec_helper"
require "dragonfly/image_magick/commands"

describe Dragonfly::ImageMagick::Commands do
include Dragonfly::ImageMagick::Commands

let(:app) { test_app }

def sample_content(name)
Dragonfly::Content.new(app, SAMPLES_DIR.join(name))
end

describe "convert" do
let(:image) { sample_content("beach.png") } # 280x355

it "should allow for general convert commands" do
convert(image, "-scale 56x71")
image.should have_width(56)
image.should have_height(71)
end

it "should allow for general convert commands with added format" do
convert(image, "-scale 56x71", "format" => "gif")
image.should have_width(56)
image.should have_height(71)
image.should have_format("gif")
image.meta["format"].should == "gif"
end

it "should work for commands with parenthesis" do
convert(image, "\\( +clone -sparse-color Barycentric '0,0 black 0,%[fx:h-1] white' -function polynomial 2,-2,0.5 \\) -compose Blur -set option:compose:args 15 -composite")
image.should have_width(280)
end

it "should work for files with spaces/apostrophes in the name" do
image = Dragonfly::Content.new(app, SAMPLES_DIR.join("mevs' white pixel.png"))
convert(image, "-resize 2x2!")
image.should have_width(2)
end

it "allows converting specific frames" do
gif = sample_content("gif.gif")
convert(gif, "-resize 50x50")
all_frames_size = gif.size

gif = sample_content("gif.gif")
convert(gif, "-resize 50x50", "frame" => 0)
one_frame_size = gif.size

one_frame_size.should < all_frames_size
end

it "accepts input arguments for convert commands" do
image2 = image.clone
convert(image, "")
convert(image2, "", "input_args" => "-extract 50x50+10+10")

image.should_not equal_image(image2)
image2.should have_width(50)
end

it "allows converting using specific delegates" do
expect {
convert(image, "", "format" => "jpg", "delegate" => "png")
}.to call_command(app.shell, %r{convert png:/[^']+?/beach\.png /[^']+?\.jpg})
end

it "maintains the mime_type meta if it exists already" do
convert(image, "-resize 10x")
image.meta["mime_type"].should be_nil

image.add_meta("mime_type" => "image/png")
convert(image, "-resize 5x")
image.meta["mime_type"].should == "image/png"
image.mime_type.should == "image/png" # sanity check
end

it "doesn't maintain the mime_type meta on format change" do
image.add_meta("mime_type" => "image/png")
convert(image, "", "format" => "gif")
image.meta["mime_type"].should be_nil
image.mime_type.should == "image/gif" # sanity check
end
end

describe "generate" do
let (:image) { Dragonfly::Content.new(app) }

before(:each) do
generate(image, "-size 1x1 xc:white", "png")
end

it { image.should have_width(1) }
it { image.should have_height(1) }
it { image.should have_format("png") }
it { image.meta.should == { "format" => "png" } }
end
end
19 changes: 0 additions & 19 deletions spec/dragonfly/image_magick/generators/convert_spec.rb

This file was deleted.

52 changes: 39 additions & 13 deletions spec/dragonfly/image_magick/generators/plain_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'spec_helper'
require "spec_helper"
require "dragonfly/param_validators"

describe Dragonfly::ImageMagick::Generators::Plain do
let (:generator) { Dragonfly::ImageMagick::Generators::Plain.new }
Expand All @@ -9,42 +10,67 @@
before(:each) do
generator.call(image, 3, 2)
end
it {image.should have_width(3)}
it {image.should have_height(2)}
it {image.should have_format('png')}
it {image.meta.should == {'format' => 'png', 'name' => 'plain.png'}}
it { image.should have_width(3) }
it { image.should have_height(2) }
it { image.should have_format("png") }
it { image.meta.should == { "format" => "png", "name" => "plain.png" } }
end

describe "specifying the format" do
before(:each) do
generator.call(image, 1, 1, 'format'=> 'gif')
generator.call(image, 1, 1, "format" => "gif")
end
it {image.should have_format('gif')}
it {image.meta.should == {'format' => 'gif', 'name' => 'plain.gif'}}
it { image.should have_format("gif") }
it { image.meta.should == { "format" => "gif", "name" => "plain.gif" } }
end

describe "specifying the colour" do
it "works with English spelling" do
generator.call(image, 1, 1, 'colour' => 'red')
generator.call(image, 1, 1, "colour" => "red")
end

it "works with American spelling" do
generator.call(image, 1, 1, 'color' => 'red')
generator.call(image, 1, 1, "color" => "red")
end

it "blows up with a bad colour" do
expect {
generator.call(image, 1, 1, 'colour' => 'lardoin')
generator.call(image, 1, 1, "colour" => "lardoin")
}.to raise_error(Dragonfly::Shell::CommandFailed)
end
end

describe "urls" do
it "updates the url" do
url_attributes = Dragonfly::UrlAttributes.new
generator.update_url(url_attributes, 1, 1, 'format' => 'gif')
url_attributes.name.should == 'plain.gif'
generator.update_url(url_attributes, 1, 1, "format" => "gif")
url_attributes.name.should == "plain.gif"
end
end

describe "param validations" do
{
"color" => "white -write bad.png",
"colour" => "white -write bad.png",
"format" => "png -write bad.png",
}.each do |opt, value|
it "validates bad opts like #{opt} = '#{value}'" do
expect {
generator.call(image, 1, 1, opt => value)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end

it "validates width" do
expect {
generator.call(image, "1 -write bad.png", 1)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end

it "validates height" do
expect {
generator.call(image, 1, "1 -write bad.png")
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end
37 changes: 28 additions & 9 deletions spec/dragonfly/image_magick/generators/plasma_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require 'spec_helper'
require "spec_helper"

describe Dragonfly::ImageMagick::Generators::Plasma do
let (:generator) { Dragonfly::ImageMagick::Generators::Plasma.new }
Expand All @@ -10,23 +10,42 @@
generator.call(image, 5, 3)
image.should have_width(5)
image.should have_height(3)
image.should have_format('png')
image.meta.should == {'format' => 'png', 'name' => 'plasma.png'}
image.should have_format("png")
image.meta.should == { "format" => "png", "name" => "plasma.png" }
end

it "allows changing the format" do
generator.call(image, 1, 1, 'format' => 'jpg')
image.should have_format('jpeg')
image.meta.should == {'format' => 'jpg', 'name' => 'plasma.jpg'}
generator.call(image, 1, 1, "format" => "jpg")
image.should have_format("jpeg")
image.meta.should == { "format" => "jpg", "name" => "plasma.jpg" }
end
end

describe "urls" do
it "updates the url" do
url_attributes = Dragonfly::UrlAttributes.new
generator.update_url(url_attributes, 1, 1, 'format' => 'jpg')
url_attributes.name.should == 'plasma.jpg'
generator.update_url(url_attributes, 1, 1, "format" => "jpg")
url_attributes.name.should == "plasma.jpg"
end
end
end

describe "param validations" do
it "validates format" do
expect {
generator.call(image, 1, 1, "format" => "png -write bad.png")
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end

it "validates width" do
expect {
generator.call(image, "1 -write bad.png", 1)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end

it "validates height" do
expect {
generator.call(image, 1, "1 -write bad.png")
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end
71 changes: 51 additions & 20 deletions spec/dragonfly/image_magick/generators/text_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require 'spec_helper'
require "spec_helper"

describe Dragonfly::ImageMagick::Generators::Text do
let (:generator) { Dragonfly::ImageMagick::Generators::Text.new }
Expand All @@ -7,71 +7,102 @@

describe "creating a text image" do
before(:each) do
generator.call(image, "mmm", 'font_size' => 12)
generator.call(image, "mmm", "font_size" => 12)
end
it {image.should have_width(20..40)} # approximate
it {image.should have_height(10..20)}
it {image.should have_format('png')}
it {image.meta.should == {'format' => 'png', 'name' => 'text.png'}}
it { image.should have_width(20..40) } # approximate
it { image.should have_height(10..20) }
it { image.should have_format("png") }
it { image.meta.should == { "format" => "png", "name" => "text.png" } }
end

describe "specifying the format" do
before(:each) do
generator.call(image, "mmm", 'format' => 'gif')
generator.call(image, "mmm", "format" => "gif")
end
it {image.should have_format('gif')}
it {image.meta.should == {'format' => 'gif', 'name' => 'text.gif'}}
it { image.should have_format("gif") }
it { image.meta.should == { "format" => "gif", "name" => "text.gif" } }
end

describe "padding" do
before(:each) do
image_without_padding = image.clone
generator.call(image_without_padding, "mmm", 'font_size' => 12)
generator.call(image_without_padding, "mmm", "font_size" => 12)
@width = image_properties(image_without_padding)[:width].to_i
@height = image_properties(image_without_padding)[:height].to_i
end
it "1 number shortcut" do
generator.call(image, "mmm", 'padding' => '10')
generator.call(image, "mmm", "padding" => "10")
image.should have_width(@width + 20)
image.should have_height(@height + 20)
end
it "2 numbers shortcut" do
generator.call(image, "mmm", 'padding' => '10 5')
generator.call(image, "mmm", "padding" => "10 5")
image.should have_width(@width + 10)
image.should have_height(@height + 20)
end
it "3 numbers shortcut" do
generator.call(image, "mmm", 'padding' => '10 5 8')
generator.call(image, "mmm", "padding" => "10 5 8")
image.should have_width(@width + 10)
image.should have_height(@height + 18)
end
it "4 numbers shortcut" do
generator.call(image, "mmm", 'padding' => '1 2 3 4')
generator.call(image, "mmm", "padding" => "1 2 3 4")
image.should have_width(@width + 6)
image.should have_height(@height + 4)
end
it "should override the general padding declaration with the specific one (e.g. 'padding-left')" do
generator.call(image, "mmm", 'padding' => '10', 'padding-left' => 9)
generator.call(image, "mmm", "padding" => "10", "padding-left" => 9)
image.should have_width(@width + 19)
image.should have_height(@height + 20)
end
it "should ignore 'px' suffixes" do
generator.call(image, "mmm", 'padding' => '1px 2px 3px 4px')
generator.call(image, "mmm", "padding" => "1px 2px 3px 4px")
image.should have_width(@width + 6)
image.should have_height(@height + 4)
end
it "bad padding string" do
lambda{
generator.call(image, "mmm", 'padding' => '1 2 3 4 5')
lambda {
generator.call(image, "mmm", "padding" => "1 2 3 4 5")
}.should raise_error(ArgumentError)
end
end

describe "urls" do
it "updates the url" do
url_attributes = Dragonfly::UrlAttributes.new
generator.update_url(url_attributes, "mmm", 'format' => 'gif')
url_attributes.name.should == 'text.gif'
generator.update_url(url_attributes, "mmm", "format" => "gif")
url_attributes.name.should == "text.gif"
end
end

describe "param validations" do
{
"font" => "Times New Roman -write bad.png",
"font_family" => "Times New Roman -write bad.png",
"color" => "rgb(255, 34, 55) -write bad.png",
"background_color" => "rgb(255, 52, 55) -write bad.png",
"stroke_color" => "rgb(255, 52, 55) -write bad.png",
"format" => "png -write bad.png",
}.each do |opt, value|
it "validates bad opts like #{opt} = '#{value}'" do
expect {
generator.call(image, "some text", opt => value)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end

["rgb(33,33,33)", "rgba(33,33,33,0.5)", "rgb(33.5,33.5,33.5)", "#fff", "#efefef", "blue"].each do |colour|
it "allows #{colour.inspect} as a colour specification" do
generator.call(image, "mmm", "color" => colour)
end
end

["rgb(33, 33, 33)", "something else", "blue:", "f#ff"].each do |colour|
it "disallows #{colour.inspect} as a colour specification" do
expect {
generator.call(image, "mmm", "color" => colour)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end
end
61 changes: 32 additions & 29 deletions spec/dragonfly/image_magick/plugin_spec.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
require 'spec_helper'
require "spec_helper"

describe "a configured imagemagick app" do

let(:app){ test_app.configure_with(:imagemagick) }
let(:image){ app.fetch_file(SAMPLES_DIR.join('beach.png')) }
let(:app) { test_app.configure_with(:imagemagick) }
let(:image) { app.fetch_file(SAMPLES_DIR.join("beach.png")) }

describe "env variables" do
let(:app){ test_app }
let(:app) { test_app }

it "allows setting the convert command" do
app.configure do
plugin :imagemagick, :convert_command => '/bin/convert'
plugin :imagemagick, :convert_command => "/bin/convert"
end
app.env[:convert_command].should == '/bin/convert'
app.env[:convert_command].should == "/bin/convert"
end

it "allows setting the identify command" do
app.configure do
plugin :imagemagick, :identify_command => '/bin/identify'
plugin :imagemagick, :identify_command => "/bin/identify"
end
app.env[:identify_command].should == '/bin/identify'
app.env[:identify_command].should == "/bin/identify"
end
end

Expand All @@ -33,7 +32,7 @@
end

it "should return the aspect ratio" do
image.aspect_ratio.should == (280.0/355.0)
image.aspect_ratio.should == (280.0 / 355.0)
end

it "should say if it's portrait" do
Expand All @@ -60,53 +59,53 @@
end

it "should return false for pdfs" do
image.encode('pdf').image?.should be_falsey
end unless ENV['SKIP_FLAKY_TESTS']
image.encode("pdf").image?.should be_falsey
end unless ENV["SKIP_FLAKY_TESTS"]
end

describe "processors that change the url" do
before do
app.configure{ url_format '/:name' }
app.configure { url_format "/:name" }
end

describe "convert" do
describe "thumb" do
it "sanity check with format" do
thumb = image.convert('-resize 1x1!', 'format' => 'jpg')
thumb = image.thumb("1x1!", "format" => "jpg")
thumb.url.should =~ /^\/beach\.jpg\?.*job=\w+/
thumb.width.should == 1
thumb.format.should == 'jpeg'
thumb.meta['format'].should == 'jpg'
thumb.format.should == "jpeg"
thumb.meta["format"].should == "jpg"
end

it "sanity check without format" do
thumb = image.convert('-resize 1x1!')
thumb = image.thumb("1x1!")
thumb.url.should =~ /^\/beach\.png\?.*job=\w+/
thumb.width.should == 1
thumb.format.should == 'png'
thumb.meta['format'].should be_nil
thumb.format.should == "png"
thumb.meta["format"].should be_nil
end
end

describe "encode" do
it "sanity check" do
thumb = image.encode('jpg')
thumb = image.encode("jpg")
thumb.url.should =~ /^\/beach\.jpg\?.*job=\w+/
thumb.format.should == 'jpeg'
thumb.meta['format'].should == 'jpg'
thumb.format.should == "jpeg"
thumb.meta["format"].should == "jpg"
end
end
end

describe "other processors" do
describe "encode" do
it "should encode the image to the correct format" do
image.encode!('gif')
image.format.should == 'gif'
image.encode!("gif")
image.format.should == "gif"
end

it "should allow for extra args" do
image.encode!('jpg', '-quality 1')
image.format.should == 'jpeg'
image.encode!("jpg", "-quality 1")
image.format.should == "jpeg"
image.size.should < 2000
end
end
Expand All @@ -117,8 +116,13 @@
image.width.should == 355
image.height.should == 280
end
end

it "disallows bad parameters" do
expect {
image.rotate!("90 -write bad.png").apply
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end

describe "identify" do
Expand All @@ -127,5 +131,4 @@
image.identify("-format %h").chomp.should == "355"
end
end

end
88 changes: 0 additions & 88 deletions spec/dragonfly/image_magick/processors/convert_spec.rb

This file was deleted.

30 changes: 30 additions & 0 deletions spec/dragonfly/image_magick/processors/encode_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "spec_helper"

describe Dragonfly::ImageMagick::Processors::Encode do
let (:app) { test_imagemagick_app }
let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("beach.png")) } # 280x355
let (:processor) { Dragonfly::ImageMagick::Processors::Encode.new }

it "encodes to a different format" do
processor.call(image, "jpeg")
image.should have_format("jpeg")
end

describe "param validations" do
it "validates the format param" do
expect {
processor.call(image, "jpeg -write bad.png")
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end

it "allows good args" do
processor.call(image, "jpeg", "-quality 10")
end

it "disallows bad args" do
expect {
processor.call(image, "jpeg", "-write bad.png")
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end
91 changes: 46 additions & 45 deletions spec/dragonfly/image_magick/processors/thumb_spec.rb
Original file line number Diff line number Diff line change
@@ -1,100 +1,92 @@
require 'spec_helper'
require 'ostruct'
require "spec_helper"
require "ostruct"

describe Dragonfly::ImageMagick::Processors::Thumb do

let (:app) { test_imagemagick_app }
let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join('beach.png')) } # 280x355
let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("beach.png")) } # 280x355
let (:processor) { Dragonfly::ImageMagick::Processors::Thumb.new }

it "raises an error if an unrecognized string is given" do
expect{
processor.call(image, '30x40#ne!')
expect {
processor.call(image, "30x40#ne!")
}.to raise_error(ArgumentError)
end

describe "resizing" do

it "works with xNN" do
processor.call(image, 'x30')
processor.call(image, "x30")
image.should have_width(24)
image.should have_height(30)
end

it "works with NNx" do
processor.call(image, '30x')
processor.call(image, "30x")
image.should have_width(30)
image.should have_height(38)
end

it "works with NNxNN" do
processor.call(image, '30x30')
processor.call(image, "30x30")
image.should have_width(24)
image.should have_height(30)
end

it "works with NNxNN!" do
processor.call(image, '30x30!')
processor.call(image, "30x30!")
image.should have_width(30)
image.should have_height(30)
end

it "works with NNxNN%" do
processor.call(image, '25x50%')
processor.call(image, "25x50%")
image.should have_width(70)
image.should have_height(178)
end

describe "NNxNN>" do

it "doesn't resize if the image is smaller than specified" do
processor.call(image, '1000x1000>')
processor.call(image, "1000x1000>")
image.should have_width(280)
image.should have_height(355)
end

it "resizes if the image is larger than specified" do
processor.call(image, '30x30>')
processor.call(image, "30x30>")
image.should have_width(24)
image.should have_height(30)
end

end

describe "NNxNN<" do

it "doesn't resize if the image is larger than specified" do
processor.call(image, '10x10<')
processor.call(image, "10x10<")
image.should have_width(280)
image.should have_height(355)
end

it "resizes if the image is smaller than specified" do
processor.call(image, '400x400<')
processor.call(image, "400x400<")
image.should have_width(315)
image.should have_height(400)
end

end

end

describe "cropping" do # Difficult to test here other than dimensions

it "crops" do
processor.call(image, '10x20+30+30')
processor.call(image, "10x20+30+30")
image.should have_width(10)
image.should have_height(20)
end

it "crops with gravity" do
image2 = image.clone

processor.call(image, '10x8nw')
processor.call(image, "10x8nw")
image.should have_width(10)
image.should have_height(8)

processor.call(image2, '10x8se')
processor.call(image2, "10x8se")
image2.should have_width(10)
image2.should have_height(8)

Expand All @@ -103,77 +95,86 @@

it "raises if given both gravity and offset" do
expect {
processor.call(image, '100x100+10+10se')
processor.call(image, "100x100+10+10se")
}.to raise_error(ArgumentError)
end

it "works when the crop area is outside the image" do
processor.call(image, '100x100+250+300')
processor.call(image, "100x100+250+300")
image.should have_width(30)
image.should have_height(55)
end

it "crops twice in a row correctly" do
processor.call(image, '100x100+10+10')
processor.call(image, '50x50+0+0')
processor.call(image, "100x100+10+10")
processor.call(image, "50x50+0+0")
image.should have_width(50)
image.should have_height(50)
end

end

describe "resize_and_crop" do

it "crops to the correct dimensions" do
processor.call(image, '100x100#')
processor.call(image, "100x100#")
image.should have_width(100)
image.should have_height(100)
end

it "resizes before cropping" do
image2 = image.clone
processor.call(image, '100x100#')
processor.call(image2, '100x100c')
processor.call(image, "100x100#")
processor.call(image2, "100x100c")
image2.should_not equal_image(image)
end

it "works with gravity" do
image2 = image.clone
processor.call(image, '10x10#nw')
processor.call(image, '10x10#se')
processor.call(image, "10x10#nw")
processor.call(image, "10x10#se")
image2.should_not equal_image(image)
end

end

describe "format" do
let (:url_attributes) { OpenStruct.new }

it "changes the format if passed in" do
processor.call(image, '2x2', 'format' => 'jpeg')
image.should have_format('jpeg')
processor.call(image, "2x2", "format" => "jpeg")
image.should have_format("jpeg")
end

it "doesn't change the format if not passed in" do
processor.call(image, '2x2')
image.should have_format('png')
processor.call(image, "2x2")
image.should have_format("png")
end

it "updates the url ext if passed in" do
processor.update_url(url_attributes, '2x2', 'format' => 'png')
url_attributes.ext.should == 'png'
processor.update_url(url_attributes, "2x2", "format" => "png")
url_attributes.ext.should == "png"
end

it "doesn't update the url ext if not passed in" do
processor.update_url(url_attributes, '2x2')
processor.update_url(url_attributes, "2x2")
url_attributes.ext.should be_nil
end
end

describe "args_for_geometry" do
it "returns the convert arguments used for a given geometry" do
expect(processor.args_for_geometry('30x40')).to eq('-resize 30x40')
expect(processor.args_for_geometry("30x40")).to eq("-resize 30x40")
end
end

describe "param validations" do
{
"format" => "png -write bad.png",
"frame" => "0] -write bad.png [",
}.each do |opt, value|
it "validates bad opts like #{opt} = '#{value}'" do
expect {
processor.call(image, "30x30", opt => value)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end
end
89 changes: 89 additions & 0 deletions spec/dragonfly/param_validators_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require "spec_helper"
require "dragonfly/param_validators"

describe Dragonfly::ParamValidators do
include Dragonfly::ParamValidators

describe "validate!" do
it "does nothing if the parameter meets the condition" do
validate!("thing") { |t| t === "thing" }
end

it "raises if the parameter doesn't meet the condition" do
expect {
validate!("thing") { |t| t === "ting" }
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end

it "does nothing if the parameter is nil" do
validate!(nil) { |t| t === "thing" }
end
end

describe "validate_all!" do
it "allows passing an array of parameters to validate" do
validate_all!(["a", "b"]) { |p| /\w/ === p }
expect {
validate_all!(["a", " "]) { |p| /\w/ === p }
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end

describe "validate_all_keys!" do
it "allows passing an array of parameters to validate" do
obj = { "a" => "A", "b" => "B" }
validate_all_keys!(obj, ["a", "b"]) { |p| /\w/ === p }
expect {
validate_all_keys!(obj, ["a", "b"]) { |p| /[a-z]/ === p }
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end

describe "is_number" do
[3, 3.14, "3", "3.2"].each do |val|
it "validates #{val.inspect}" do
validate!(val, &is_number)
end
end

["", "3 2", "hello4", {}, []].each do |val|
it "validates #{val.inspect}" do
expect {
validate!(val, &is_number)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end

describe "is_word" do
["hello", "helLo", "HELLO"].each do |val|
it "validates #{val.inspect}" do
validate!(val, &is_word)
end
end

["", "hel%$lo", "hel lo", "hel-lo", {}, []].each do |val|
it "validates #{val.inspect}" do
expect {
validate!(val, &is_word)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end

describe "is_words" do
["hello there", "Hi", " What is Up "].each do |val|
it "validates #{val.inspect}" do
validate!(val, &is_words)
end
end

["", "hel%$lo", "What's up", "hel-lo", {}, []].each do |val|
it "validates #{val.inspect}" do
expect {
validate!(val, &is_words)
}.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
end
end
end
end
15 changes: 6 additions & 9 deletions spec/functional/shell_commands_spec.rb
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
require 'spec_helper'
require "spec_helper"

describe "using the shell" do

let (:app) { test_app }

describe "shell injection" do
it "should not allow it!" do
app.configure_with(:imagemagick)
begin
app.generate(:plain, 10, 10, 'white').convert("-resize 5x5 ; touch tmp/stuff").apply
app.generate(:plain, 10, 10, "white; touch tmp/stuff").apply
rescue Dragonfly::Shell::CommandFailed
end
File.exist?('tmp/stuff').should be_falsey
File.exist?("tmp/stuff").should be_falsey
end
end

describe "env variables with imagemagick" do
it "allows configuring the convert path" do
app.configure_with(:imagemagick, :convert_command => '/bin/convert')
app.configure_with(:imagemagick, :convert_command => "/bin/convert")
app.shell.should_receive(:run).with(%r[/bin/convert], hash_including)
app.create("").thumb('30x30').apply
app.create("").thumb("30x30").apply
end

it "allows configuring the identify path" do
app.configure_with(:imagemagick, :identify_command => '/bin/identify')
app.configure_with(:imagemagick, :identify_command => "/bin/identify")
app.shell.should_receive(:run).with(%r[/bin/identify], hash_including).and_return("JPG 1 1")
app.create("").width
end
end

end

26 changes: 12 additions & 14 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
require "bundler"
Bundler.setup(:default, :test)

$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'rspec'
require 'dragonfly'
require 'fileutils'
require 'tempfile'
require 'webmock/rspec'
require 'pry'
require "rspec"
require "dragonfly"
require "fileutils"
require "tempfile"
require "webmock/rspec"
require "pry"

# Requires supporting files with custom matchers and macros, etc,
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }

SAMPLES_DIR = Pathname.new(File.expand_path('../../samples', __FILE__))
SAMPLES_DIR = Pathname.new(File.expand_path("../../samples", __FILE__))

RSpec.configure do |c|
c.include ModelHelpers
Expand All @@ -25,8 +25,8 @@ def todo
raise "TODO"
end

require 'logger'
LOG_FILE = 'tmp/test.log'
require "logger"
LOG_FILE = "tmp/test.log"
FileUtils.rm_rf(LOG_FILE)
Dragonfly.logger = Logger.new(LOG_FILE)

Expand All @@ -36,7 +36,7 @@ def todo
end
end

def test_app(name=nil)
def test_app(name = nil)
app = Dragonfly::App.instance(name)
app.datastore = Dragonfly::MemoryDataStore.new
app.secret = "test secret"
Expand All @@ -45,8 +45,6 @@ def test_app(name=nil)

def test_imagemagick_app
test_app.configure do
generator :convert, Dragonfly::ImageMagick::Generators::Convert.new
processor :convert, Dragonfly::ImageMagick::Processors::Convert.new
analyser :image_properties, Dragonfly::ImageMagick::Analysers::ImageProperties.new
end
end