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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5385c46218c68349adc9f33a89600ed52901b483fc4b8b7ad3bab0ea1e3aa579
4
- data.tar.gz: bd4341674548d3526d786be990912b527745000b6d887148e91bfe88e7a9618c
3
+ metadata.gz: 16341445bb67f27ae4dc7492244b038df290440a5f4b64d71cca3c637fd8b4d0
4
+ data.tar.gz: '0285f83e005198572fb9a52dea16eef2acca80ded36e9fa905b88d66701221fe'
5
5
  SHA512:
6
- metadata.gz: 28b284e2ca3d15914787357c2035f900a2c0c61c5fdc0b36302b2bcd24b01683b073774b70a9583ef446ba764dbb7019e3e92eab8cd83b042d428236498ccaf4
7
- data.tar.gz: 2fc17f0f4587fe85ccf6af91798a0a89aa58086a9518445d2684abcffd16ab9d53c10fdea2e7f0a5cb2f07a24774b3865b80e7ddd293f5a53bfea4f04b0b24bd
6
+ metadata.gz: ea5f8440e718e3521eaca6824b3dd600817994ed144b5c69a9e609fa24ff8e9583a415a9a2ee9a309e7f622d7d33e0712525ce18ed020942a1585b80ef889cef
7
+ data.tar.gz: 1c81c3c4ed0bfe6421d3ca6ca69432929a4e8233b468d582586fca5d308758bbc9c6042d830e42bd4421c198452df019e802fcf151c277e3ac7248942cf30af0
data/.travis.yml CHANGED
@@ -6,6 +6,7 @@ rvm:
6
6
  - "2.5"
7
7
  - "2.6"
8
8
  - "2.7"
9
+ - "3.0"
9
10
  - "jruby"
10
11
 
11
12
  env:
data/History.md CHANGED
@@ -1,3 +1,13 @@
1
+ # 1.4.0 (2021-05-19)
2
+
3
+ ## Changes
4
+
5
+ - Removed `convert` processor and generator (which were quite insecure), in favour of utility commands in `Dragonfly::ImageMagick::Commands`
6
+
7
+ ## Fixes
8
+
9
+ - Better security for all job steps with parameter validations
10
+
1
11
  # 1.3.0 (2021-01-09)
2
12
 
3
13
  ## Changes
@@ -1,8 +1,8 @@
1
- require 'base64'
2
- require 'forwardable'
3
- require 'dragonfly/has_filename'
4
- require 'dragonfly/temp_object'
5
- require 'dragonfly/utils'
1
+ require "base64"
2
+ require "forwardable"
3
+ require "dragonfly/has_filename"
4
+ require "dragonfly/temp_object"
5
+ require "dragonfly/utils"
6
6
 
7
7
  module Dragonfly
8
8
 
@@ -16,11 +16,10 @@ module Dragonfly
16
16
  # It is acted upon in generator, processor, analyser and datastore methods and provides a standard interface for updating content,
17
17
  # no matter how that content first got there (whether in the form of a String/Pathname/File/etc.)
18
18
  class Content
19
-
20
19
  include HasFilename
21
20
  extend Forwardable
22
21
 
23
- def initialize(app, obj="", meta=nil)
22
+ def initialize(app, obj = "", meta = nil)
24
23
  @app = app
25
24
  @meta = {}
26
25
  @previous_temp_objects = []
@@ -79,7 +78,7 @@ module Dragonfly
79
78
  # @example "image/jpeg"
80
79
  # @return [String]
81
80
  def mime_type
82
- meta['mime_type'] || app.mime_type_for(ext)
81
+ meta["mime_type"] || app.mime_type_for(ext)
83
82
  end
84
83
 
85
84
  # Set the content using a pre-registered generator
@@ -93,7 +92,7 @@ module Dragonfly
93
92
 
94
93
  # Update the content using a pre-registered processor
95
94
  # @example
96
- # content.process!(:convert, "-resize 300x300")
95
+ # content.process!(:thumb, "300x300")
97
96
  # @return [Content] self
98
97
  def process!(name, *args)
99
98
  app.get_processor(name).call(self, *args)
@@ -111,10 +110,10 @@ module Dragonfly
111
110
  # @param obj [String, Pathname, Tempfile, File, Content, TempObject] can be any of these types
112
111
  # @param meta [Hash] - should be json-like, i.e. contain no types other than String, Number, Boolean
113
112
  # @return [Content] self
114
- def update(obj, meta=nil)
113
+ def update(obj, meta = nil)
115
114
  meta ||= {}
116
- self.temp_object = TempObject.new(obj, meta['name'])
117
- self.meta['name'] ||= temp_object.name if temp_object.name
115
+ self.temp_object = TempObject.new(obj, meta["name"])
116
+ self.meta["name"] ||= temp_object.name if temp_object.name
118
117
  clear_analyser_cache
119
118
  add_meta(obj.meta) if obj.respond_to?(:meta)
120
119
  add_meta(meta)
@@ -135,7 +134,7 @@ module Dragonfly
135
134
  # "file --mime-type #{path}"
136
135
  # end
137
136
  # # ===> "beach.jpg: image/jpeg"
138
- def shell_eval(opts={})
137
+ def shell_eval(opts = {})
139
138
  should_escape = opts[:escape] != false
140
139
  command = yield(should_escape ? shell.escape(path) : path)
141
140
  run command, :escape => should_escape
@@ -148,7 +147,7 @@ module Dragonfly
148
147
  # "/usr/local/bin/generate_text gumfry -o #{path}"
149
148
  # end
150
149
  # @return [Content] self
151
- def shell_generate(opts={})
150
+ def shell_generate(opts = {})
152
151
  ext = opts[:ext] || self.ext
153
152
  should_escape = opts[:escape] != false
154
153
  tempfile = Utils.new_tempfile(ext)
@@ -165,7 +164,7 @@ module Dragonfly
165
164
  # "convert -resize 20x10 #{old_path} #{new_path}"
166
165
  # end
167
166
  # @return [Content] self
168
- def shell_update(opts={})
167
+ def shell_update(opts = {})
169
168
  ext = opts[:ext] || self.ext
170
169
  should_escape = opts[:escape] != false
171
170
  tempfile = Utils.new_tempfile(ext)
@@ -176,7 +175,7 @@ module Dragonfly
176
175
  update(tempfile)
177
176
  end
178
177
 
179
- def store(opts={})
178
+ def store(opts = {})
180
179
  datastore.write(self, opts)
181
180
  end
182
181
 
@@ -188,7 +187,7 @@ module Dragonfly
188
187
  end
189
188
 
190
189
  def close
191
- previous_temp_objects.each{|temp_object| temp_object.close }
190
+ previous_temp_objects.each { |temp_object| temp_object.close }
192
191
  temp_object.close
193
192
  end
194
193
 
@@ -199,6 +198,7 @@ module Dragonfly
199
198
  private
200
199
 
201
200
  attr_reader :previous_temp_objects
201
+
202
202
  def temp_object=(temp_object)
203
203
  previous_temp_objects.push(@temp_object) if @temp_object
204
204
  @temp_object = temp_object
@@ -215,6 +215,5 @@ module Dragonfly
215
215
  def run(command, opts)
216
216
  shell.run(command, opts)
217
217
  end
218
-
219
218
  end
220
219
  end
@@ -0,0 +1,35 @@
1
+ module Dragonfly
2
+ module ImageMagick
3
+ module Commands
4
+ module_function
5
+
6
+ def convert(content, args = "", opts = {})
7
+ convert_command = content.env[:convert_command] || "convert"
8
+ format = opts["format"]
9
+
10
+ input_args = opts["input_args"] if opts["input_args"]
11
+ delegate_string = "#{opts["delegate"]}:" if opts["delegate"]
12
+ frame_string = "[#{opts["frame"]}]" if opts["frame"]
13
+
14
+ content.shell_update :ext => format do |old_path, new_path|
15
+ "#{convert_command} #{input_args} #{delegate_string}#{old_path}#{frame_string} #{args} #{new_path}"
16
+ end
17
+
18
+ if format
19
+ content.meta["format"] = format.to_s
20
+ content.ext = format
21
+ content.meta["mime_type"] = nil # don't need it as we have ext now
22
+ end
23
+ end
24
+
25
+ def generate(content, args, format)
26
+ format = format.to_s
27
+ convert_command = content.env[:convert_command] || "convert"
28
+ content.shell_generate :ext => format do |path|
29
+ "#{convert_command} #{args} #{path}"
30
+ end
31
+ content.add_meta("format" => format)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,25 +1,31 @@
1
+ require "dragonfly/image_magick/commands"
2
+ require "dragonfly/param_validators"
3
+
1
4
  module Dragonfly
2
5
  module ImageMagick
3
6
  module Generators
4
7
  class Plain
8
+ include ParamValidators
5
9
 
6
- def call(content, width, height, opts={})
10
+ def call(content, width, height, opts = {})
11
+ validate_all!([width, height], &is_number)
12
+ validate_all_keys!(opts, %w(colour color format), &is_word)
7
13
  format = extract_format(opts)
8
- colour = opts['colour'] || opts['color'] || 'white'
9
- content.generate!(:convert, "-size #{width}x#{height} xc:#{colour}", format)
10
- content.add_meta('format' => format, 'name' => "plain.#{format}")
14
+
15
+ colour = opts["colour"] || opts["color"] || "white"
16
+ Commands.generate(content, "-size #{width}x#{height} xc:#{colour}", format)
17
+ content.add_meta("format" => format, "name" => "plain.#{format}")
11
18
  end
12
19
 
13
- def update_url(url_attributes, width, height, opts={})
20
+ def update_url(url_attributes, width, height, opts = {})
14
21
  url_attributes.name = "plain.#{extract_format(opts)}"
15
22
  end
16
23
 
17
24
  private
18
25
 
19
26
  def extract_format(opts)
20
- opts['format'] || 'png'
27
+ opts["format"] || "png"
21
28
  end
22
-
23
29
  end
24
30
  end
25
31
  end
@@ -1,24 +1,28 @@
1
+ require "dragonfly/image_magick/commands"
2
+
1
3
  module Dragonfly
2
4
  module ImageMagick
3
5
  module Generators
4
6
  class Plasma
7
+ include ParamValidators
5
8
 
6
- def call(content, width, height, opts={})
9
+ def call(content, width, height, opts = {})
10
+ validate_all!([width, height], &is_number)
11
+ validate!(opts["format"], &is_word)
7
12
  format = extract_format(opts)
8
- content.generate!(:convert, "-size #{width}x#{height} plasma:fractal", format)
9
- content.add_meta('format' => format, 'name' => "plasma.#{format}")
13
+ Commands.generate(content, "-size #{width}x#{height} plasma:fractal", format)
14
+ content.add_meta("format" => format, "name" => "plasma.#{format}")
10
15
  end
11
16
 
12
- def update_url(url_attributes, width, height, opts={})
17
+ def update_url(url_attributes, width, height, opts = {})
13
18
  url_attributes.name = "plasma.#{extract_format(opts)}"
14
19
  end
15
20
 
16
21
  private
17
22
 
18
23
  def extract_format(opts)
19
- opts['format'] || 'png'
24
+ opts["format"] || "png"
20
25
  end
21
-
22
26
  end
23
27
  end
24
28
  end
@@ -1,100 +1,111 @@
1
- require 'dragonfly/hash_with_css_style_keys'
1
+ require "dragonfly/hash_with_css_style_keys"
2
+ require "dragonfly/image_magick/commands"
3
+ require "dragonfly/param_validators"
2
4
 
3
5
  module Dragonfly
4
6
  module ImageMagick
5
7
  module Generators
6
8
  class Text
9
+ include ParamValidators
7
10
 
8
11
  FONT_STYLES = {
9
- 'normal' => 'normal',
10
- 'italic' => 'italic',
11
- 'oblique' => 'oblique'
12
+ "normal" => "normal",
13
+ "italic" => "italic",
14
+ "oblique" => "oblique",
12
15
  }
13
16
 
14
17
  FONT_STRETCHES = {
15
- 'normal' => 'normal',
16
- 'semi-condensed' => 'semi-condensed',
17
- 'condensed' => 'condensed',
18
- 'extra-condensed' => 'extra-condensed',
19
- 'ultra-condensed' => 'ultra-condensed',
20
- 'semi-expanded' => 'semi-expanded',
21
- 'expanded' => 'expanded',
22
- 'extra-expanded' => 'extra-expanded',
23
- 'ultra-expanded' => 'ultra-expanded'
18
+ "normal" => "normal",
19
+ "semi-condensed" => "semi-condensed",
20
+ "condensed" => "condensed",
21
+ "extra-condensed" => "extra-condensed",
22
+ "ultra-condensed" => "ultra-condensed",
23
+ "semi-expanded" => "semi-expanded",
24
+ "expanded" => "expanded",
25
+ "extra-expanded" => "extra-expanded",
26
+ "ultra-expanded" => "ultra-expanded",
24
27
  }
25
28
 
26
29
  FONT_WEIGHTS = {
27
- 'normal' => 'normal',
28
- 'bold' => 'bold',
29
- 'bolder' => 'bolder',
30
- 'lighter' => 'lighter',
31
- '100' => 100,
32
- '200' => 200,
33
- '300' => 300,
34
- '400' => 400,
35
- '500' => 500,
36
- '600' => 600,
37
- '700' => 700,
38
- '800' => 800,
39
- '900' => 900
30
+ "normal" => "normal",
31
+ "bold" => "bold",
32
+ "bolder" => "bolder",
33
+ "lighter" => "lighter",
34
+ "100" => 100,
35
+ "200" => 200,
36
+ "300" => 300,
37
+ "400" => 400,
38
+ "500" => 500,
39
+ "600" => 600,
40
+ "700" => 700,
41
+ "800" => 800,
42
+ "900" => 900,
40
43
  }
41
44
 
42
- def update_url(url_attributes, string, opts={})
45
+ IS_COLOUR = ->(param) {
46
+ /\A(#\w+|rgba?\([\d\.,]+\)|\w+)\z/ === param
47
+ }
48
+
49
+ def update_url(url_attributes, string, opts = {})
43
50
  url_attributes.name = "text.#{extract_format(opts)}"
44
51
  end
45
52
 
46
- def call(content, string, opts={})
53
+ def call(content, string, opts = {})
54
+ validate_all_keys!(opts, %w(font font_family), &is_words)
55
+ validate_all_keys!(opts, %w(color background_color stroke_color), &IS_COLOUR)
56
+ validate!(opts["format"], &is_word)
57
+
47
58
  opts = HashWithCssStyleKeys[opts]
48
59
  args = []
49
60
  format = extract_format(opts)
50
- background = opts['background_color'] || 'none'
51
- font_size = (opts['font_size'] || 12).to_i
61
+ background = opts["background_color"] || "none"
62
+ font_size = (opts["font_size"] || 12).to_i
63
+ font_family = opts["font_family"] || opts["font"]
52
64
  escaped_string = "\"#{string.gsub(/"/, '\"')}\""
53
65
 
54
66
  # Settings
55
67
  args.push("-gravity NorthWest")
56
68
  args.push("-antialias")
57
69
  args.push("-pointsize #{font_size}")
58
- args.push("-font \"#{opts['font']}\"") if opts['font']
59
- args.push("-family '#{opts['font_family']}'") if opts['font_family']
60
- args.push("-fill #{opts['color']}") if opts['color']
61
- args.push("-stroke #{opts['stroke_color']}") if opts['stroke_color']
62
- args.push("-style #{FONT_STYLES[opts['font_style']]}") if opts['font_style']
63
- args.push("-stretch #{FONT_STRETCHES[opts['font_stretch']]}") if opts['font_stretch']
64
- args.push("-weight #{FONT_WEIGHTS[opts['font_weight']]}") if opts['font_weight']
70
+ args.push("-family '#{font_family}'") if font_family
71
+ args.push("-fill #{opts["color"]}") if opts["color"]
72
+ args.push("-stroke #{opts["stroke_color"]}") if opts["stroke_color"]
73
+ args.push("-style #{FONT_STYLES[opts["font_style"]]}") if opts["font_style"]
74
+ args.push("-stretch #{FONT_STRETCHES[opts["font_stretch"]]}") if opts["font_stretch"]
75
+ args.push("-weight #{FONT_WEIGHTS[opts["font_weight"]]}") if opts["font_weight"]
65
76
  args.push("-background #{background}")
66
77
  args.push("label:#{escaped_string}")
67
78
 
68
79
  # Padding
69
- pt, pr, pb, pl = parse_padding_string(opts['padding']) if opts['padding']
70
- padding_top = (opts['padding_top'] || pt || 0)
71
- padding_right = (opts['padding_right'] || pr || 0)
72
- padding_bottom = (opts['padding_bottom'] || pb || 0)
73
- padding_left = (opts['padding_left'] || pl || 0)
80
+ pt, pr, pb, pl = parse_padding_string(opts["padding"]) if opts["padding"]
81
+ padding_top = (opts["padding_top"] || pt).to_i
82
+ padding_right = (opts["padding_right"] || pr).to_i
83
+ padding_bottom = (opts["padding_bottom"] || pb).to_i
84
+ padding_left = (opts["padding_left"] || pl).to_i
74
85
 
75
- content.generate!(:convert, args.join(' '), format)
86
+ Commands.generate(content, args.join(" "), format)
76
87
 
77
88
  if (padding_top || padding_right || padding_bottom || padding_left)
78
89
  dimensions = content.analyse(:image_properties)
79
- text_width = dimensions['width']
80
- text_height = dimensions['height']
81
- width = padding_left + text_width + padding_right
82
- height = padding_top + text_height + padding_bottom
90
+ text_width = dimensions["width"]
91
+ text_height = dimensions["height"]
92
+ width = padding_left + text_width + padding_right
93
+ height = padding_top + text_height + padding_bottom
83
94
 
84
95
  args = args.slice(0, args.length - 2)
85
96
  args.push("-size #{width}x#{height}")
86
97
  args.push("xc:#{background}")
87
98
  args.push("-annotate 0x0+#{padding_left}+#{padding_top} #{escaped_string}")
88
- content.generate!(:convert, args.join(' '), format)
99
+ Commands.generate(content, args.join(" "), format)
89
100
  end
90
101
 
91
- content.add_meta('format' => format, 'name' => "text.#{format}")
102
+ content.add_meta("format" => format, "name" => "text.#{format}")
92
103
  end
93
104
 
94
105
  private
95
106
 
96
107
  def extract_format(opts)
97
- opts['format'] || 'png'
108
+ opts["format"] || "png"
98
109
  end
99
110
 
100
111
  # Use css-style padding declaration, i.e.
@@ -103,25 +114,23 @@ module Dragonfly
103
114
  # 10 5 10 (top, left/right, bottom)
104
115
  # 10 5 10 5 (top, right, bottom, left)
105
116
  def parse_padding_string(str)
106
- padding_parts = str.gsub('px','').split(/\s+/).map{|px| px.to_i}
117
+ padding_parts = str.gsub("px", "").split(/\s+/).map { |px| px.to_i }
107
118
  case padding_parts.size
108
119
  when 1
109
120
  p = padding_parts.first
110
- [p,p,p,p]
121
+ [p, p, p, p]
111
122
  when 2
112
- p,q = padding_parts
113
- [p,q,p,q]
123
+ p, q = padding_parts
124
+ [p, q, p, q]
114
125
  when 3
115
- p,q,r = padding_parts
116
- [p,q,r,q]
126
+ p, q, r = padding_parts
127
+ [p, q, r, q]
117
128
  when 4
118
129
  padding_parts
119
130
  else raise ArgumentError, "Couldn't parse padding string '#{str}' - should be a css-style string"
120
131
  end
121
132
  end
122
133
  end
123
-
124
134
  end
125
135
  end
126
136
  end
127
-