dragonfly 1.3.0 → 1.4.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.
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