imageproxy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.txt +21 -0
- data/README.mdown +296 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/config.ru +7 -0
- data/imageproxy.gemspec +90 -0
- data/imageproxy.rb +4 -0
- data/lib/command.rb +22 -0
- data/lib/compare.rb +12 -0
- data/lib/convert.rb +72 -0
- data/lib/identify.rb +11 -0
- data/lib/options.rb +73 -0
- data/lib/selftest.rb +72 -0
- data/lib/server.rb +105 -0
- data/lib/signature.rb +24 -0
- data/public/background.png +0 -0
- data/public/sample.png +0 -0
- data/public/sample_10x20.png +0 -0
- data/spec/command_spec.rb +19 -0
- data/spec/convert_spec.rb +151 -0
- data/spec/options_spec.rb +87 -0
- data/spec/server_spec.rb +122 -0
- data/spec/signature_spec.rb +74 -0
- data/spec/spec_helper.rb +23 -0
- metadata +184 -0
data/imageproxy.rb
ADDED
data/lib/command.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
class Command
|
2
|
+
protected
|
3
|
+
|
4
|
+
def execute_command(command_line)
|
5
|
+
stdin, stdout, stderr = Open3.popen3(command_line)
|
6
|
+
[output_to_string(stdout), output_to_string(stderr)].join("")
|
7
|
+
end
|
8
|
+
|
9
|
+
def curl(url, options={})
|
10
|
+
user_agent = options[:user_agent] || "imageproxy"
|
11
|
+
%|curl -s -A "#{user_agent}" "#{url}"|
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_path(obj)
|
15
|
+
obj.respond_to?(:path) ? obj.path : obj.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def output_to_string(output)
|
19
|
+
output.readlines.join("").chomp
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/lib/compare.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "command")
|
2
|
+
|
3
|
+
class Compare < Command
|
4
|
+
def initialize(a, b)
|
5
|
+
@path_a = to_path(a)
|
6
|
+
@path_b = to_path(b)
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute
|
10
|
+
execute_command %'compare -metric AE -fuzz 10% "#{@path_a}" "#{@path_b}" "#{Tempfile.new("compare").path}"'
|
11
|
+
end
|
12
|
+
end
|
data/lib/convert.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "command")
|
2
|
+
|
3
|
+
class Convert < Command
|
4
|
+
attr_reader :options
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@options = options
|
8
|
+
if (!(options.resize || options.thumbnail || options.rotate || options.flip || options.format || options.quality))
|
9
|
+
raise "Missing action or illegal parameter value"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(user_agent=nil)
|
14
|
+
execute_command %'#{curl options.source, :user_agent => user_agent} | convert - #{convert_options} #{new_format}#{file.path}'
|
15
|
+
file
|
16
|
+
end
|
17
|
+
|
18
|
+
def convert_options
|
19
|
+
convert_options = []
|
20
|
+
convert_options << "-resize #{resize_thumbnail_options(options.resize)}" if options.resize
|
21
|
+
convert_options << "-thumbnail #{resize_thumbnail_options(options.thumbnail)}" if options.thumbnail
|
22
|
+
convert_options << "-flop" if options.flip == "horizontal"
|
23
|
+
convert_options << "-flip" if options.flip == "vertical"
|
24
|
+
convert_options << rotate_options if options.rotate
|
25
|
+
convert_options << "-colors 256" if options.format == "png8"
|
26
|
+
convert_options << "-quality #{options.quality}" if options.quality
|
27
|
+
convert_options << interlace_options if options.progressive
|
28
|
+
convert_options.join " "
|
29
|
+
end
|
30
|
+
|
31
|
+
def resize_thumbnail_options(size)
|
32
|
+
case options.shape
|
33
|
+
when "cut"
|
34
|
+
"#{size}^ -gravity center -extent #{size}"
|
35
|
+
when "preserve"
|
36
|
+
size
|
37
|
+
when "pad"
|
38
|
+
background = options.background ? %|"#{options.background}"| : %|none -matte|
|
39
|
+
"#{size} -background #{background} -gravity center -extent #{size}"
|
40
|
+
else
|
41
|
+
size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def rotate_options
|
46
|
+
if options.rotate.to_f % 90 == 0
|
47
|
+
"-rotate #{options.rotate}"
|
48
|
+
else
|
49
|
+
background = options.background ? %|"#{options.background}"| : %|none|
|
50
|
+
"-background #{background} -matte -rotate #{options.rotate}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def interlace_options
|
55
|
+
case options.progressive
|
56
|
+
when "true"
|
57
|
+
"-interlace JPEG"
|
58
|
+
when "false"
|
59
|
+
"-interlace none"
|
60
|
+
else
|
61
|
+
""
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def new_format
|
66
|
+
options.format ? "#{options.format}:" : ""
|
67
|
+
end
|
68
|
+
|
69
|
+
def file
|
70
|
+
@tempfile ||= Tempfile.new("imageproxy").tap(&:close)
|
71
|
+
end
|
72
|
+
end
|
data/lib/identify.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "command")
|
2
|
+
|
3
|
+
class Identify < Command
|
4
|
+
def initialize(options)
|
5
|
+
@options = options
|
6
|
+
end
|
7
|
+
|
8
|
+
def execute(user_agent=nil)
|
9
|
+
execute_command %'#{curl @options.source, :user_agent => user_agent} | identify -verbose -'
|
10
|
+
end
|
11
|
+
end
|
data/lib/options.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'cgi'
|
3
|
+
require 'mime/types'
|
4
|
+
|
5
|
+
class Options
|
6
|
+
def initialize(path, query_params)
|
7
|
+
params_from_path = path.split('/').reject { |s| s.nil? || s.empty? }
|
8
|
+
command = params_from_path.shift
|
9
|
+
|
10
|
+
@hash = Hash[*params_from_path]
|
11
|
+
@hash['command'] = command
|
12
|
+
@hash.merge! query_params
|
13
|
+
merge_obfuscated
|
14
|
+
@hash["source"] = @hash.delete("src") if @hash.has_key?("src")
|
15
|
+
|
16
|
+
unescape_source
|
17
|
+
unescape_signature
|
18
|
+
check_parameters
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_parameters
|
22
|
+
check_param('resize',/^[0-9]{1,5}(x[0-9]{1,5})?$/)
|
23
|
+
check_param('thumbnail',/^[0-9]{1,5}(x[0-9]{1,5})?$/)
|
24
|
+
check_param('rotate',/^(-)?[0-9]{1,3}(\.[0-9]+)?$/)
|
25
|
+
check_param('format',/^[0-9a-zA-Z]{2,6}$/)
|
26
|
+
check_param('progressive',/^true|false$/i)
|
27
|
+
check_param('background',/^#[0-9a-f]{3}([0-9a-f]{3})?|rgba\([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},[0-1](.[0-9]+)?\)$/)
|
28
|
+
check_param('shape',/^preserve|pad|cut$/i)
|
29
|
+
@hash['quality'] = [[@hash['quality'].to_i, 100].min, 0].max.to_s if @hash.has_key?('quality')
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_param(param, rega)
|
33
|
+
if @hash.has_key? param
|
34
|
+
if (! rega.match(@hash[param]))
|
35
|
+
@hash.delete(param)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def method_missing(symbol)
|
41
|
+
@hash[symbol.to_s] || @hash[symbol]
|
42
|
+
end
|
43
|
+
|
44
|
+
def content_type
|
45
|
+
MIME::Types.of(@hash['source']).first.content_type
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def unescape_source
|
51
|
+
@hash['source'] &&= CGI.unescape(CGI.unescape(@hash['source']))
|
52
|
+
end
|
53
|
+
|
54
|
+
def unescape_signature
|
55
|
+
@hash['signature'] &&= URI.unescape(@hash['signature'])
|
56
|
+
end
|
57
|
+
|
58
|
+
def merge_obfuscated
|
59
|
+
if @hash["_"]
|
60
|
+
decoded = Base64.decode64(CGI.unescape(@hash["_"]))
|
61
|
+
decoded_hash = CGI.parse(decoded)
|
62
|
+
@hash.delete "_"
|
63
|
+
decoded_hash.map { |k, v| @hash[k] = (v.class == Array) ? v.first : v }
|
64
|
+
end
|
65
|
+
|
66
|
+
if @hash["-"]
|
67
|
+
decoded = Base64.decode64(CGI.unescape(@hash["-"]))
|
68
|
+
decoded_hash = Hash[*decoded.split('/').reject { |s| s.nil? || s.empty? }]
|
69
|
+
@hash.delete "-"
|
70
|
+
decoded_hash.map { |k, v| @hash[k] = (v.class == Array) ? v.first : v }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/selftest.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
class Selftest
|
2
|
+
def self.html(request, signature_required, signature_secret)
|
3
|
+
html = <<-HTML
|
4
|
+
<html>
|
5
|
+
<head>
|
6
|
+
<title>imageproxy selftest</title>
|
7
|
+
<style type="text/css">
|
8
|
+
body { background: url(/background.png); font-family: "Helvetica", sans-serif; font-size: smaller; }
|
9
|
+
h3 { margin: 2em 0 0 0; }
|
10
|
+
img { display: block; border: 1px solid black; margin: 1em 0; }
|
11
|
+
.footer { margin-top: 2em; border-top: 1px solid #999; padding-top: 0.5em; font-size: smallest; }
|
12
|
+
</style>
|
13
|
+
</head>
|
14
|
+
<body>
|
15
|
+
HTML
|
16
|
+
|
17
|
+
url_prefix = "#{request.scheme}://#{request.host_with_port}"
|
18
|
+
raw_source = "http://eahanson.s3.amazonaws.com/imageproxy/sample.png"
|
19
|
+
source = CGI.escape(URI.escape(URI.escape(raw_source)))
|
20
|
+
|
21
|
+
html += <<-HTML
|
22
|
+
<h3>Original Image</h3>
|
23
|
+
<a href="#{raw_source}">#{raw_source}</a>
|
24
|
+
<img src="#{raw_source}">
|
25
|
+
HTML
|
26
|
+
|
27
|
+
examples = [
|
28
|
+
["Resize (regular query-string URL format)", "/convert?resize=100x100&source=#{source}"],
|
29
|
+
["Resize (CloudFront-compatible URL format)", "/convert/resize/100x100/source/#{source}"],
|
30
|
+
|
31
|
+
["Resize with padding", "/convert?resize=100x100&shape=pad&source=#{source}"],
|
32
|
+
["Resize with padding & background color", "/convert?resize=100x100&shape=pad&background=%23ff00ff&source=#{source}"],
|
33
|
+
|
34
|
+
["Resize with cutting", "/convert?resize=100x100&shape=cut&source=#{source}"],
|
35
|
+
|
36
|
+
["Flipping horizontally", "/convert?flip=horizontal&source=#{source}"],
|
37
|
+
["Flipping vertically", "/convert?flip=vertical&source=#{source}"],
|
38
|
+
|
39
|
+
["Rotating to a 90-degree increment", "/convert?rotate=90&source=#{source}"],
|
40
|
+
["Rotating to a non-90-degree increment", "/convert?rotate=120&source=#{source}"],
|
41
|
+
["Rotating to a non-90-degree increment with a background color", "/convert?rotate=120&background=%23ff00ff&source=#{source}"],
|
42
|
+
|
43
|
+
["Combo", "/convert?resize=100x100&shape=cut&rotate=45&background=%23ff00ff&source=#{source}"]
|
44
|
+
]
|
45
|
+
|
46
|
+
examples.each do |example|
|
47
|
+
path = example[1]
|
48
|
+
if (signature_required)
|
49
|
+
signature = CGI.escape(Signature.create(path, signature_secret))
|
50
|
+
if path.include?("&")
|
51
|
+
path += "&signature=#{signature}"
|
52
|
+
else
|
53
|
+
path += "/signature/#{signature}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
example_url = url_prefix + path
|
57
|
+
html += <<-HTML
|
58
|
+
<h3>#{example[0]}</h3>
|
59
|
+
<a href="#{example_url}">#{example_url}</a>
|
60
|
+
<img src="#{example_url}">
|
61
|
+
HTML
|
62
|
+
end
|
63
|
+
|
64
|
+
html += <<-HTML
|
65
|
+
<div class="footer"><a href="https://github.com/eahanson/imageproxy">imageproxy</a> selftest</div>
|
66
|
+
</body>
|
67
|
+
</html>
|
68
|
+
HTML
|
69
|
+
|
70
|
+
html
|
71
|
+
end
|
72
|
+
end
|
data/lib/server.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "options")
|
2
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "convert")
|
3
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "identify")
|
4
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "selftest")
|
5
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "signature")
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
class Server
|
9
|
+
def initialize
|
10
|
+
@file_server = Rack::File.new(File.join(File.expand_path(File.dirname(__FILE__)), "..", "public"))
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
request = Rack::Request.new(env)
|
15
|
+
options = Options.new(request.path_info, request.params)
|
16
|
+
user_agent = request.env["HTTP_USER_AGENT"]
|
17
|
+
cachetime = config(:cache_time) ? config(:cache_time) : 86400
|
18
|
+
|
19
|
+
case options.command
|
20
|
+
when "convert", "process", nil
|
21
|
+
check_signature request, options
|
22
|
+
check_domain options
|
23
|
+
check_size options
|
24
|
+
|
25
|
+
file = Convert.new(options).execute(user_agent)
|
26
|
+
class << file
|
27
|
+
alias to_path path
|
28
|
+
end
|
29
|
+
|
30
|
+
file.open
|
31
|
+
[200, {"Content-Type" => options.content_type, "Cache-Control" => "max-age=#{cachetime}, must-revalidate"}, file]
|
32
|
+
when "identify"
|
33
|
+
check_signature request, options
|
34
|
+
check_domain options
|
35
|
+
|
36
|
+
[200, {"Content-Type" => "text/plain"}, [Identify.new(options).execute(user_agent)]]
|
37
|
+
when "selftest"
|
38
|
+
[200, {"Content-Type" => "text/html"}, [Selftest.html(request, config?(:signature_required), config(:signature_secret))]]
|
39
|
+
else
|
40
|
+
@file_server.call(env)
|
41
|
+
end
|
42
|
+
rescue
|
43
|
+
STDERR.puts $!
|
44
|
+
[500, {"Content-Type" => "text/plain"}, ["Error (#{$!})"]]
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def config(symbol)
|
50
|
+
ENV["IMAGEPROXY_#{symbol.to_s.upcase}"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def config?(symbol)
|
54
|
+
config(symbol) && config(symbol).casecmp("TRUE") == 0
|
55
|
+
end
|
56
|
+
|
57
|
+
def check_signature(request, options)
|
58
|
+
if config?(:signature_required)
|
59
|
+
raise "Missing siganture" if options.signature.nil?
|
60
|
+
|
61
|
+
valid_signature = Signature.correct?(options.signature, request.fullpath, config(:signature_secret))
|
62
|
+
raise "Invalid signature #{options.signature} for #{request.url}" unless valid_signature
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def check_domain(options)
|
67
|
+
raise "Invalid domain" unless domain_allowed? options.source
|
68
|
+
end
|
69
|
+
|
70
|
+
def check_size(options)
|
71
|
+
raise "Image size too large" if exceeds_max_size(options.resize, options.thumbnail)
|
72
|
+
end
|
73
|
+
|
74
|
+
def domain_allowed?(url)
|
75
|
+
return true unless allowed_domains
|
76
|
+
allowed_domains.include?(url_to_domain url)
|
77
|
+
end
|
78
|
+
|
79
|
+
def url_to_domain(url)
|
80
|
+
URI::parse(url).host.split(".")[-2, 2].join(".")
|
81
|
+
rescue
|
82
|
+
""
|
83
|
+
end
|
84
|
+
|
85
|
+
def allowed_domains
|
86
|
+
config(:allowed_domains) && config(:allowed_domains).split(",").map(&:strip)
|
87
|
+
end
|
88
|
+
|
89
|
+
def exceeds_max_size(*sizes)
|
90
|
+
max_size && sizes.any? { |size| size && requested_size(size) > max_size }
|
91
|
+
end
|
92
|
+
|
93
|
+
def max_size
|
94
|
+
config(:max_size) && config(:max_size).to_i
|
95
|
+
end
|
96
|
+
|
97
|
+
def requested_size(req_size)
|
98
|
+
sizes = req_size.scan(/\d*/)
|
99
|
+
if sizes[2] && (sizes[2].to_i > sizes[0].to_i)
|
100
|
+
sizes[2].to_i
|
101
|
+
else
|
102
|
+
sizes[0].to_i
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/signature.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'openssl'
|
3
|
+
|
4
|
+
class Signature
|
5
|
+
def self.create(path, secret)
|
6
|
+
Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), secret, remove_signature_from(path))).strip.tr('+/', '-_')
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.remove_signature_from(path)
|
10
|
+
#TODO: do this in fewer passes
|
11
|
+
path.
|
12
|
+
sub(%r{&signature(=[^&]*)?(?=&|$)}, "").
|
13
|
+
sub(%r{\?signature(=[^&]*)?&}, "?").
|
14
|
+
sub(%r{\?signature(=[^&]*)?$}, "").
|
15
|
+
sub(%r{/signature/[^\?/]+/}, "/").
|
16
|
+
sub(%r{/signature/[^\?/]+\?}, "?").
|
17
|
+
sub(%r{/signature/[^\?/]+}, "")
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.correct?(signature, path, secret)
|
21
|
+
created = create(path, secret)
|
22
|
+
signature != nil && path != nil && secret != nil && (created == signature || created == signature.tr('+/', '-_'))
|
23
|
+
end
|
24
|
+
end
|
Binary file
|
data/public/sample.png
ADDED
Binary file
|
Binary file
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Command do
|
4
|
+
describe "#curl" do
|
5
|
+
context "when a user agent is supplied" do
|
6
|
+
it "should send that user agent" do
|
7
|
+
Command.new.send(:curl, "http://example.com/dog.jpg", :user_agent => "some user agent").should ==
|
8
|
+
%|curl -s -A "some user agent" "http://example.com/dog.jpg"|
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
context "when no user agent is supplied" do
|
13
|
+
it "should send a default user agent" do
|
14
|
+
Command.new.send(:curl, "http://example.com/dog.jpg").should ==
|
15
|
+
%|curl -s -A "imageproxy" "http://example.com/dog.jpg"|
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Convert do
|
4
|
+
before do
|
5
|
+
@mock_file = mock("file")
|
6
|
+
@mock_file.stub!(:path).and_return("/mock/file/path")
|
7
|
+
end
|
8
|
+
|
9
|
+
def command(options)
|
10
|
+
command = Convert.new(Options.new("", {:source => "http%3A%2F%2Fexample.com%2Fdog.jpg"}.merge(options)))
|
11
|
+
command.stub!(:file).and_return(@mock_file)
|
12
|
+
command.stub!(:system)
|
13
|
+
command
|
14
|
+
end
|
15
|
+
|
16
|
+
context "general" do
|
17
|
+
before do
|
18
|
+
@command = Convert.new(Options.new("/convert/format/png/resize/10x20/source/http%3A%2F%2Fexample.com%2Fdog.jpg", {}))
|
19
|
+
@command.stub!(:file).and_return(@mock_file)
|
20
|
+
@command.stub!(:system)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should generate the proper command-line" do
|
24
|
+
@command.should_receive(:execute_command).with(%'curl -s -A "imageproxy" "http://example.com/dog.jpg" | convert - -resize 10x20 png:/mock/file/path')
|
25
|
+
@command.execute
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should return the output file" do
|
29
|
+
@command.stub!(:execute_command)
|
30
|
+
@command.execute.should == @mock_file
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when resizing" do
|
35
|
+
it("with no extra args") do
|
36
|
+
command(:resize => "10x20").convert_options.should ==
|
37
|
+
'-resize 10x20'
|
38
|
+
end
|
39
|
+
|
40
|
+
it("with a different size") do
|
41
|
+
command(:resize => "50x50").convert_options.should ==
|
42
|
+
'-resize 50x50'
|
43
|
+
end
|
44
|
+
|
45
|
+
it("when preserving shape") do
|
46
|
+
command(:resize => "10x20", :shape => "preserve").convert_options.should ==
|
47
|
+
'-resize 10x20'
|
48
|
+
end
|
49
|
+
|
50
|
+
it("when padding") do
|
51
|
+
command(:resize => "10x20", :shape => "pad").convert_options.should ==
|
52
|
+
'-resize 10x20 -background none -matte -gravity center -extent 10x20'
|
53
|
+
end
|
54
|
+
|
55
|
+
it("when padding with a background color") do
|
56
|
+
command(:resize => "10x20", :shape => "pad", :background => "#ff00ff").convert_options.should ==
|
57
|
+
'-resize 10x20 -background "#ff00ff" -gravity center -extent 10x20'
|
58
|
+
end
|
59
|
+
|
60
|
+
it("when cutting") do
|
61
|
+
command(:resize => "10x20", :shape => "cut").convert_options.should ==
|
62
|
+
'-resize 10x20^ -gravity center -extent 10x20'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "when thumbnailing" do
|
67
|
+
it("when preserving shape") do
|
68
|
+
command(:thumbnail => "10x20", :shape => "preserve").convert_options.should ==
|
69
|
+
'-thumbnail 10x20'
|
70
|
+
end
|
71
|
+
|
72
|
+
it("when padding") do
|
73
|
+
command(:thumbnail => "10x20", :shape => "pad", :background => "#ff00ff").convert_options.should ==
|
74
|
+
'-thumbnail 10x20 -background "#ff00ff" -gravity center -extent 10x20'
|
75
|
+
end
|
76
|
+
|
77
|
+
it("when cutting") do
|
78
|
+
command(:thumbnail => "10x20", :shape => "cut").convert_options.should ==
|
79
|
+
'-thumbnail 10x20^ -gravity center -extent 10x20'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when flipping" do
|
84
|
+
it("should flip horizontal") do
|
85
|
+
command(:flip => "horizontal").convert_options.should ==
|
86
|
+
"-flop"
|
87
|
+
end
|
88
|
+
|
89
|
+
it("should flip vertical") do
|
90
|
+
command(:flip => "vertical").convert_options.should ==
|
91
|
+
"-flip"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "when rotating" do
|
96
|
+
it("should rotate to a right angle") do
|
97
|
+
command(:rotate => "90").convert_options.should ==
|
98
|
+
"-rotate 90"
|
99
|
+
end
|
100
|
+
|
101
|
+
it("should rotate to a non-right angle") do
|
102
|
+
command(:rotate => "92.1").convert_options.should ==
|
103
|
+
"-background none -matte -rotate 92.1"
|
104
|
+
end
|
105
|
+
|
106
|
+
it("should rotate to a non-right angle with a background") do
|
107
|
+
command(:rotate => "92.1", :background => "#ff00ff").convert_options.should ==
|
108
|
+
'-background "#ff00ff" -matte -rotate 92.1'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when changing format" do
|
113
|
+
it("should not change the format if not requested") do
|
114
|
+
command(:rotate => "90").new_format.should ==
|
115
|
+
""
|
116
|
+
end
|
117
|
+
|
118
|
+
it("should not change the format if not requested") do
|
119
|
+
command(:rotate => "90", :format => "png").new_format.should ==
|
120
|
+
"png:"
|
121
|
+
end
|
122
|
+
|
123
|
+
it("should set the colors when converting to png8") do
|
124
|
+
command(:rotate => "90", :format => "png8").convert_options.should ==
|
125
|
+
"-rotate 90 -colors 256"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "when changing quality" do
|
130
|
+
it("should set the quality") do
|
131
|
+
command(:quality => "85").convert_options.should ==
|
132
|
+
"-quality 85"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context "when converting to progressive" do
|
137
|
+
it("should be 'JPEG' if progressive is 'true'") do
|
138
|
+
command(:resize => "10x10", :progressive => "true").convert_options.should ==
|
139
|
+
"-resize 10x10 -interlace JPEG"
|
140
|
+
end
|
141
|
+
|
142
|
+
it("should be 'none' if progressive is 'false'") do
|
143
|
+
command(:resize => "10x10", :progressive => "false").convert_options.should ==
|
144
|
+
"-resize 10x10 -interlace none"
|
145
|
+
end
|
146
|
+
|
147
|
+
it("should not be set if progressive isn't supplied") do
|
148
|
+
command({:resize => "10x10"}).convert_options.should_not match /interlace/
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|