dragonfly 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/History.md +10 -0
  4. data/lib/dragonfly/content.rb +17 -18
  5. data/lib/dragonfly/image_magick/commands.rb +35 -0
  6. data/lib/dragonfly/image_magick/generators/plain.rb +13 -7
  7. data/lib/dragonfly/image_magick/generators/plasma.rb +10 -6
  8. data/lib/dragonfly/image_magick/generators/text.rb +67 -58
  9. data/lib/dragonfly/image_magick/plugin.rb +26 -25
  10. data/lib/dragonfly/image_magick/processors/encode.rb +16 -5
  11. data/lib/dragonfly/image_magick/processors/thumb.rb +37 -31
  12. data/lib/dragonfly/param_validators.rb +37 -0
  13. data/lib/dragonfly/response.rb +2 -2
  14. data/lib/dragonfly/version.rb +1 -1
  15. data/spec/dragonfly/image_magick/commands_spec.rb +98 -0
  16. data/spec/dragonfly/image_magick/generators/plain_spec.rb +39 -13
  17. data/spec/dragonfly/image_magick/generators/plasma_spec.rb +28 -9
  18. data/spec/dragonfly/image_magick/generators/text_spec.rb +51 -20
  19. data/spec/dragonfly/image_magick/plugin_spec.rb +45 -28
  20. data/spec/dragonfly/image_magick/processors/encode_spec.rb +30 -0
  21. data/spec/dragonfly/image_magick/processors/thumb_spec.rb +46 -45
  22. data/spec/dragonfly/param_validators_spec.rb +89 -0
  23. data/spec/functional/shell_commands_spec.rb +6 -9
  24. data/spec/spec_helper.rb +12 -14
  25. metadata +11 -10
  26. data/lib/dragonfly/image_magick/generators/convert.rb +0 -19
  27. data/lib/dragonfly/image_magick/processors/convert.rb +0 -33
  28. data/spec/dragonfly/image_magick/generators/convert_spec.rb +0 -19
  29. data/spec/dragonfly/image_magick/processors/convert_spec.rb +0 -88
@@ -1,11 +1,11 @@
1
- require 'dragonfly/image_magick/analysers/image_properties'
2
- require 'dragonfly/image_magick/generators/convert'
3
- require 'dragonfly/image_magick/generators/plain'
4
- require 'dragonfly/image_magick/generators/plasma'
5
- require 'dragonfly/image_magick/generators/text'
6
- require 'dragonfly/image_magick/processors/convert'
7
- require 'dragonfly/image_magick/processors/encode'
8
- require 'dragonfly/image_magick/processors/thumb'
1
+ require "dragonfly/image_magick/analysers/image_properties"
2
+ require "dragonfly/image_magick/generators/plain"
3
+ require "dragonfly/image_magick/generators/plasma"
4
+ require "dragonfly/image_magick/generators/text"
5
+ require "dragonfly/image_magick/processors/encode"
6
+ require "dragonfly/image_magick/processors/thumb"
7
+ require "dragonfly/image_magick/commands"
8
+ require "dragonfly/param_validators"
9
9
 
10
10
  module Dragonfly
11
11
  module ImageMagick
@@ -13,37 +13,36 @@ module Dragonfly
13
13
  # The ImageMagick Plugin registers an app with generators, analysers and processors.
14
14
  # Look at the source code for #call to see exactly how it configures the app.
15
15
  class Plugin
16
-
17
- def call(app, opts={})
16
+ def call(app, opts = {})
18
17
  # ENV
19
- app.env[:convert_command] = opts[:convert_command] || 'convert'
20
- app.env[:identify_command] = opts[:identify_command] || 'identify'
18
+ app.env[:convert_command] = opts[:convert_command] || "convert"
19
+ app.env[:identify_command] = opts[:identify_command] || "identify"
21
20
 
22
21
  # Analysers
23
22
  app.add_analyser :image_properties, ImageMagick::Analysers::ImageProperties.new
24
23
  app.add_analyser :width do |content|
25
- content.analyse(:image_properties)['width']
24
+ content.analyse(:image_properties)["width"]
26
25
  end
27
26
  app.add_analyser :height do |content|
28
- content.analyse(:image_properties)['height']
27
+ content.analyse(:image_properties)["height"]
29
28
  end
30
29
  app.add_analyser :format do |content|
31
- content.analyse(:image_properties)['format']
30
+ content.analyse(:image_properties)["format"]
32
31
  end
33
32
  app.add_analyser :aspect_ratio do |content|
34
33
  attrs = content.analyse(:image_properties)
35
- attrs['width'].to_f / attrs['height']
34
+ attrs["width"].to_f / attrs["height"]
36
35
  end
37
36
  app.add_analyser :portrait do |content|
38
37
  attrs = content.analyse(:image_properties)
39
- attrs['width'] <= attrs['height']
38
+ attrs["width"] <= attrs["height"]
40
39
  end
41
40
  app.add_analyser :landscape do |content|
42
41
  !content.analyse(:portrait)
43
42
  end
44
43
  app.add_analyser :image do |content|
45
44
  begin
46
- content.analyse(:image_properties)['format'] != 'pdf'
45
+ content.analyse(:image_properties)["format"] != "pdf"
47
46
  rescue Shell::CommandFailed
48
47
  false
49
48
  end
@@ -55,29 +54,31 @@ module Dragonfly
55
54
  app.define(:image?) { image }
56
55
 
57
56
  # Generators
58
- app.add_generator :convert, ImageMagick::Generators::Convert.new
59
57
  app.add_generator :plain, ImageMagick::Generators::Plain.new
60
58
  app.add_generator :plasma, ImageMagick::Generators::Plasma.new
61
59
  app.add_generator :text, ImageMagick::Generators::Text.new
60
+ app.add_generator :convert do
61
+ raise "The convert generator is deprecated for better security - use Dragonfly::ImageMagick::Commands.generate(content, args, format) instead."
62
+ end
62
63
 
63
64
  # Processors
64
- app.add_processor :convert, Processors::Convert.new
65
65
  app.add_processor :encode, Processors::Encode.new
66
66
  app.add_processor :thumb, Processors::Thumb.new
67
67
  app.add_processor :rotate do |content, amount|
68
- content.process!(:convert, "-rotate #{amount}")
68
+ ParamValidators.validate!(amount, &ParamValidators.is_number)
69
+ Commands.convert(content, "-rotate #{amount}")
70
+ end
71
+ app.add_processor :convert do
72
+ raise "The convert processor is deprecated for better security - use Dragonfly::ImageMagick::Commands.convert(content, args, opts) instead."
69
73
  end
70
74
 
71
75
  # Extra methods
72
- app.define :identify do |cli_args=nil|
76
+ app.define :identify do |cli_args = nil|
73
77
  shell_eval do |path|
74
78
  "#{app.env[:identify_command]} #{cli_args} #{path}"
75
79
  end
76
80
  end
77
-
78
81
  end
79
-
80
82
  end
81
83
  end
82
84
  end
83
-
@@ -1,18 +1,29 @@
1
+ require "dragonfly/image_magick/commands"
2
+
1
3
  module Dragonfly
2
4
  module ImageMagick
3
5
  module Processors
4
6
  class Encode
7
+ include ParamValidators
8
+
9
+ WHITELISTED_ARGS = %w(quality)
10
+
11
+ IS_IN_WHITELISTED_ARGS = ->(args_string) {
12
+ args_string.scan(/-\w+/).all? { |arg|
13
+ WHITELISTED_ARGS.include?(arg.sub("-", ""))
14
+ }
15
+ }
5
16
 
6
- def update_url(attrs, format, args="")
17
+ def update_url(attrs, format, args = "")
7
18
  attrs.ext = format.to_s
8
19
  end
9
20
 
10
- def call(content, format, args="")
11
- content.process!(:convert, args, 'format' => format)
21
+ def call(content, format, args = "")
22
+ validate!(format, &is_word)
23
+ validate!(args, &IS_IN_WHITELISTED_ARGS)
24
+ Commands.convert(content, args, "format" => format)
12
25
  end
13
-
14
26
  end
15
27
  end
16
28
  end
17
29
  end
18
-
@@ -1,32 +1,40 @@
1
+ require "dragonfly/image_magick/commands"
2
+
1
3
  module Dragonfly
2
4
  module ImageMagick
3
5
  module Processors
4
6
  class Thumb
7
+ include ParamValidators
5
8
 
6
9
  GRAVITIES = {
7
- 'nw' => 'NorthWest',
8
- 'n' => 'North',
9
- 'ne' => 'NorthEast',
10
- 'w' => 'West',
11
- 'c' => 'Center',
12
- 'e' => 'East',
13
- 'sw' => 'SouthWest',
14
- 's' => 'South',
15
- 'se' => 'SouthEast'
10
+ "nw" => "NorthWest",
11
+ "n" => "North",
12
+ "ne" => "NorthEast",
13
+ "w" => "West",
14
+ "c" => "Center",
15
+ "e" => "East",
16
+ "sw" => "SouthWest",
17
+ "s" => "South",
18
+ "se" => "SouthEast",
16
19
  }
17
20
 
18
21
  # Geometry string patterns
19
- RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
22
+ RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
20
23
  CROPPED_RESIZE_GEOMETRY = /\A(\d+)x(\d+)#(\w{1,2})?\z/ # e.g. '20x50#ne'
21
- CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'
24
+ CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'
22
25
 
23
- def update_url(url_attributes, geometry, opts={})
24
- format = opts['format']
26
+ def update_url(url_attributes, geometry, opts = {})
27
+ format = opts["format"]
25
28
  url_attributes.ext = format if format
26
29
  end
27
30
 
28
- def call(content, geometry, opts={})
29
- content.process!(:convert, args_for_geometry(geometry), opts)
31
+ def call(content, geometry, opts = {})
32
+ validate!(opts["format"], &is_word)
33
+ validate!(opts["frame"], &is_number)
34
+ Commands.convert(content, args_for_geometry(geometry), {
35
+ "format" => opts["format"],
36
+ "frame" => opts["frame"],
37
+ })
30
38
  end
31
39
 
32
40
  def args_for_geometry(geometry)
@@ -37,11 +45,11 @@ module Dragonfly
37
45
  resize_and_crop_args($1, $2, $3)
38
46
  when CROP_GEOMETRY
39
47
  crop_args(
40
- 'width' => $1,
41
- 'height' => $2,
42
- 'x' => $3,
43
- 'y' => $4,
44
- 'gravity' => $5
48
+ "width" => $1,
49
+ "height" => $2,
50
+ "x" => $3,
51
+ "y" => $4,
52
+ "gravity" => $5,
45
53
  )
46
54
  else raise ArgumentError, "Didn't recognise the geometry string #{geometry}"
47
55
  end
@@ -54,26 +62,24 @@ module Dragonfly
54
62
  end
55
63
 
56
64
  def crop_args(opts)
57
- raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts['x'] && opts['gravity']
65
+ raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts["x"] && opts["gravity"]
58
66
 
59
- width = opts['width']
60
- height = opts['height']
61
- gravity = GRAVITIES[opts['gravity']]
62
- x = "#{opts['x'] || 0}"
63
- x = '+' + x unless x[/\A[+-]/]
64
- y = "#{opts['y'] || 0}"
65
- y = '+' + y unless y[/\A[+-]/]
67
+ width = opts["width"]
68
+ height = opts["height"]
69
+ gravity = GRAVITIES[opts["gravity"]]
70
+ x = "#{opts["x"] || 0}"
71
+ x = "+" + x unless x[/\A[+-]/]
72
+ y = "#{opts["y"] || 0}"
73
+ y = "+" + y unless y[/\A[+-]/]
66
74
 
67
75
  "#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} +repage"
68
76
  end
69
77
 
70
78
  def resize_and_crop_args(width, height, gravity)
71
- gravity = GRAVITIES[gravity || 'c']
79
+ gravity = GRAVITIES[gravity || "c"]
72
80
  "-resize #{width}x#{height}^^ -gravity #{gravity} -crop #{width}x#{height}+0+0 +repage"
73
81
  end
74
-
75
82
  end
76
83
  end
77
84
  end
78
85
  end
79
-
@@ -0,0 +1,37 @@
1
+ module Dragonfly
2
+ module ParamValidators
3
+ class InvalidParameter < RuntimeError; end
4
+
5
+ module_function
6
+
7
+ IS_NUMBER = ->(param) {
8
+ param.is_a?(Numeric) || /\A[\d\.]+\z/ === param
9
+ }
10
+
11
+ IS_WORD = ->(param) {
12
+ /\A\w+\z/ === param
13
+ }
14
+
15
+ IS_WORDS = ->(param) {
16
+ /\A[\w ]+\z/ === param
17
+ }
18
+
19
+ def is_number; IS_NUMBER; end
20
+ def is_word; IS_WORD; end
21
+ def is_words; IS_WORDS; end
22
+
23
+ def validate!(parameter, &validator)
24
+ return if parameter.nil?
25
+ raise InvalidParameter unless validator.(parameter)
26
+ end
27
+
28
+ def validate_all!(parameters, &validator)
29
+ parameters.each { |p| validate!(p, &validator) }
30
+ end
31
+
32
+ def validate_all_keys!(obj, keys, &validator)
33
+ parameters = keys.map { |key| obj[key] }
34
+ validate_all!(parameters, &validator)
35
+ end
36
+ end
37
+ end
@@ -1,4 +1,4 @@
1
- require 'uri'
1
+ require 'cgi'
2
2
  require 'rack'
3
3
 
4
4
  module Dragonfly
@@ -99,7 +99,7 @@ module Dragonfly
99
99
 
100
100
  def filename_string
101
101
  return unless job.name
102
- filename = request_from_msie? ? URI.encode(job.name) : job.name
102
+ filename = request_from_msie? ? CGI.escape(job.name) : job.name
103
103
  %(filename="#{filename}")
104
104
  end
105
105
 
@@ -1,3 +1,3 @@
1
1
  module Dragonfly
2
- VERSION = "1.3.0"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -0,0 +1,98 @@
1
+ require "spec_helper"
2
+ require "dragonfly/image_magick/commands"
3
+
4
+ describe Dragonfly::ImageMagick::Commands do
5
+ include Dragonfly::ImageMagick::Commands
6
+
7
+ let(:app) { test_app }
8
+
9
+ def sample_content(name)
10
+ Dragonfly::Content.new(app, SAMPLES_DIR.join(name))
11
+ end
12
+
13
+ describe "convert" do
14
+ let(:image) { sample_content("beach.png") } # 280x355
15
+
16
+ it "should allow for general convert commands" do
17
+ convert(image, "-scale 56x71")
18
+ image.should have_width(56)
19
+ image.should have_height(71)
20
+ end
21
+
22
+ it "should allow for general convert commands with added format" do
23
+ convert(image, "-scale 56x71", "format" => "gif")
24
+ image.should have_width(56)
25
+ image.should have_height(71)
26
+ image.should have_format("gif")
27
+ image.meta["format"].should == "gif"
28
+ end
29
+
30
+ it "should work for commands with parenthesis" do
31
+ 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")
32
+ image.should have_width(280)
33
+ end
34
+
35
+ it "should work for files with spaces/apostrophes in the name" do
36
+ image = Dragonfly::Content.new(app, SAMPLES_DIR.join("mevs' white pixel.png"))
37
+ convert(image, "-resize 2x2!")
38
+ image.should have_width(2)
39
+ end
40
+
41
+ it "allows converting specific frames" do
42
+ gif = sample_content("gif.gif")
43
+ convert(gif, "-resize 50x50")
44
+ all_frames_size = gif.size
45
+
46
+ gif = sample_content("gif.gif")
47
+ convert(gif, "-resize 50x50", "frame" => 0)
48
+ one_frame_size = gif.size
49
+
50
+ one_frame_size.should < all_frames_size
51
+ end
52
+
53
+ it "accepts input arguments for convert commands" do
54
+ image2 = image.clone
55
+ convert(image, "")
56
+ convert(image2, "", "input_args" => "-extract 50x50+10+10")
57
+
58
+ image.should_not equal_image(image2)
59
+ image2.should have_width(50)
60
+ end
61
+
62
+ it "allows converting using specific delegates" do
63
+ expect {
64
+ convert(image, "", "format" => "jpg", "delegate" => "png")
65
+ }.to call_command(app.shell, %r{convert png:/[^']+?/beach\.png /[^']+?\.jpg})
66
+ end
67
+
68
+ it "maintains the mime_type meta if it exists already" do
69
+ convert(image, "-resize 10x")
70
+ image.meta["mime_type"].should be_nil
71
+
72
+ image.add_meta("mime_type" => "image/png")
73
+ convert(image, "-resize 5x")
74
+ image.meta["mime_type"].should == "image/png"
75
+ image.mime_type.should == "image/png" # sanity check
76
+ end
77
+
78
+ it "doesn't maintain the mime_type meta on format change" do
79
+ image.add_meta("mime_type" => "image/png")
80
+ convert(image, "", "format" => "gif")
81
+ image.meta["mime_type"].should be_nil
82
+ image.mime_type.should == "image/gif" # sanity check
83
+ end
84
+ end
85
+
86
+ describe "generate" do
87
+ let (:image) { Dragonfly::Content.new(app) }
88
+
89
+ before(:each) do
90
+ generate(image, "-size 1x1 xc:white", "png")
91
+ end
92
+
93
+ it { image.should have_width(1) }
94
+ it { image.should have_height(1) }
95
+ it { image.should have_format("png") }
96
+ it { image.meta.should == { "format" => "png" } }
97
+ end
98
+ end
@@ -1,4 +1,5 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
+ require "dragonfly/param_validators"
2
3
 
3
4
  describe Dragonfly::ImageMagick::Generators::Plain do
4
5
  let (:generator) { Dragonfly::ImageMagick::Generators::Plain.new }
@@ -9,32 +10,32 @@ describe Dragonfly::ImageMagick::Generators::Plain do
9
10
  before(:each) do
10
11
  generator.call(image, 3, 2)
11
12
  end
12
- it {image.should have_width(3)}
13
- it {image.should have_height(2)}
14
- it {image.should have_format('png')}
15
- it {image.meta.should == {'format' => 'png', 'name' => 'plain.png'}}
13
+ it { image.should have_width(3) }
14
+ it { image.should have_height(2) }
15
+ it { image.should have_format("png") }
16
+ it { image.meta.should == { "format" => "png", "name" => "plain.png" } }
16
17
  end
17
18
 
18
19
  describe "specifying the format" do
19
20
  before(:each) do
20
- generator.call(image, 1, 1, 'format'=> 'gif')
21
+ generator.call(image, 1, 1, "format" => "gif")
21
22
  end
22
- it {image.should have_format('gif')}
23
- it {image.meta.should == {'format' => 'gif', 'name' => 'plain.gif'}}
23
+ it { image.should have_format("gif") }
24
+ it { image.meta.should == { "format" => "gif", "name" => "plain.gif" } }
24
25
  end
25
26
 
26
27
  describe "specifying the colour" do
27
28
  it "works with English spelling" do
28
- generator.call(image, 1, 1, 'colour' => 'red')
29
+ generator.call(image, 1, 1, "colour" => "red")
29
30
  end
30
31
 
31
32
  it "works with American spelling" do
32
- generator.call(image, 1, 1, 'color' => 'red')
33
+ generator.call(image, 1, 1, "color" => "red")
33
34
  end
34
35
 
35
36
  it "blows up with a bad colour" do
36
37
  expect {
37
- generator.call(image, 1, 1, 'colour' => 'lardoin')
38
+ generator.call(image, 1, 1, "colour" => "lardoin")
38
39
  }.to raise_error(Dragonfly::Shell::CommandFailed)
39
40
  end
40
41
  end
@@ -42,9 +43,34 @@ describe Dragonfly::ImageMagick::Generators::Plain do
42
43
  describe "urls" do
43
44
  it "updates the url" do
44
45
  url_attributes = Dragonfly::UrlAttributes.new
45
- generator.update_url(url_attributes, 1, 1, 'format' => 'gif')
46
- url_attributes.name.should == 'plain.gif'
46
+ generator.update_url(url_attributes, 1, 1, "format" => "gif")
47
+ url_attributes.name.should == "plain.gif"
47
48
  end
48
49
  end
49
50
 
51
+ describe "param validations" do
52
+ {
53
+ "color" => "white -write bad.png",
54
+ "colour" => "white -write bad.png",
55
+ "format" => "png -write bad.png",
56
+ }.each do |opt, value|
57
+ it "validates bad opts like #{opt} = '#{value}'" do
58
+ expect {
59
+ generator.call(image, 1, 1, opt => value)
60
+ }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
61
+ end
62
+ end
63
+
64
+ it "validates width" do
65
+ expect {
66
+ generator.call(image, "1 -write bad.png", 1)
67
+ }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
68
+ end
69
+
70
+ it "validates height" do
71
+ expect {
72
+ generator.call(image, 1, "1 -write bad.png")
73
+ }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
74
+ end
75
+ end
50
76
  end