jr-paperclip 7.3.0 → 8.0.0.beta.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{test.yml → tests.yml} +19 -9
  3. data/.rubocop.yml +2 -1
  4. data/CONTRIBUTING.md +1 -1
  5. data/Gemfile +1 -0
  6. data/NEWS +16 -1
  7. data/README.md +119 -8
  8. data/UPGRADING +5 -0
  9. data/VIPS_MIGRATION_GUIDE.md +131 -0
  10. data/features/basic_integration.feature +27 -0
  11. data/features/step_definitions/attachment_steps.rb +17 -0
  12. data/gemfiles/7.0.gemfile +1 -0
  13. data/gemfiles/7.1.gemfile +1 -0
  14. data/gemfiles/7.2.gemfile +1 -0
  15. data/gemfiles/8.0.gemfile +1 -0
  16. data/gemfiles/8.1.gemfile +1 -0
  17. data/lib/paperclip/attachment.rb +3 -2
  18. data/lib/paperclip/errors.rb +4 -5
  19. data/lib/paperclip/geometry.rb +3 -3
  20. data/lib/paperclip/geometry_detector_factory.rb +52 -12
  21. data/lib/paperclip/helpers.rb +18 -0
  22. data/lib/paperclip/processor.rb +36 -4
  23. data/lib/paperclip/thumbnail.rb +568 -62
  24. data/lib/paperclip/version.rb +1 -1
  25. data/lib/paperclip.rb +26 -9
  26. data/paperclip.gemspec +3 -2
  27. data/spec/paperclip/attachment_definitions_spec.rb +300 -0
  28. data/spec/paperclip/attachment_spec.rb +1 -1
  29. data/spec/paperclip/geometry_detector_spec.rb +81 -32
  30. data/spec/paperclip/geometry_spec.rb +8 -5
  31. data/spec/paperclip/helpers_spec.rb +49 -0
  32. data/spec/paperclip/lazy_thumbnail_compatibility_spec.rb +266 -0
  33. data/spec/paperclip/processor_spec.rb +35 -1
  34. data/spec/paperclip/style_spec.rb +58 -0
  35. data/spec/paperclip/thumbnail_custom_options_spec.rb +173 -0
  36. data/spec/paperclip/thumbnail_loader_options_spec.rb +53 -0
  37. data/spec/paperclip/thumbnail_security_spec.rb +42 -0
  38. data/spec/paperclip/thumbnail_spec.rb +1127 -172
  39. metadata +36 -4
@@ -1,3 +1,3 @@
1
1
  module Paperclip
2
- VERSION = "7.3.0" unless defined?(Paperclip::VERSION)
2
+ VERSION = "8.0.0.beta.1" unless defined?(Paperclip::VERSION)
3
3
  end
data/lib/paperclip.rb CHANGED
@@ -1,5 +1,5 @@
1
- # Paperclip allows file attachments that are stored in the filesystem. All graphical
2
- # transformations are done using the Graphics/ImageMagick command line utilities and
1
+ # Paperclip allows file attachments that are stored in the filesystem. Graphical
2
+ # transformations are done using command line utilities (ImageMagick or libvips) and
3
3
  # are stored in Tempfiles until the record is saved. Paperclip does not require a
4
4
  # separate model for storing the attachment's information, instead adding a few simple
5
5
  # columns to your table.
@@ -77,6 +77,17 @@ module Paperclip
77
77
  extend Logger
78
78
  extend ProcessorHelpers
79
79
 
80
+ AVAILABLE_BACKENDS = [:image_magick, :vips].freeze
81
+
82
+ def self.resolve_backend(backend)
83
+ backend ||= :image_magick
84
+ unless AVAILABLE_BACKENDS.include?(backend)
85
+ log("Warning: Invalid backend: #{backend}. Falling back to :image_magick. Allowed backends: #{AVAILABLE_BACKENDS.join(', ')}")
86
+ backend = :image_magick
87
+ end
88
+ backend
89
+ end
90
+
80
91
  # Provides configurability to Paperclip. The options available are:
81
92
  # * whiny: Will raise an error if Paperclip cannot process thumbnails of
82
93
  # an uploaded image. Defaults to true.
@@ -98,7 +109,8 @@ module Paperclip
98
109
  use_exif_orientation: true,
99
110
  whiny: true,
100
111
  is_windows: Gem.win_platform?,
101
- add_validation_errors_to: :both
112
+ add_validation_errors_to: :both,
113
+ backend: :image_magick
102
114
  }
103
115
  end
104
116
 
@@ -155,12 +167,17 @@ module Paperclip
155
167
  # * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
156
168
  # to a command line error. This will override the global setting for this attachment.
157
169
  # Defaults to true.
158
- # * +convert_options+: When creating thumbnails, use this free-form options
159
- # array to pass in various convert command options. Typical options are "-strip" to
160
- # remove all Exif data from the image (save space for thumbnails and avatars) or
161
- # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
162
- # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
163
- # Note that this option takes a hash of options, each of which correspond to the style
170
+ # * +backend+: Chooses the image processing backend. Options are :image_magick (default)
171
+ # or :vips. The vips backend uses libvips which is significantly faster and uses less
172
+ # memory. Can be set globally via Paperclip.options[:backend] or per-attachment.
173
+ # has_attached_file :avatar, :styles => { :thumb => "100x100#" }, :backend => :vips
174
+ # * +convert_options+: Options passed to the image processor when creating thumbnails.
175
+ # Works with both ImageMagick and libvips backends. Common cross-platform options:
176
+ # "-strip" (remove metadata), "-quality N" (output quality), "-rotate N" (rotation),
177
+ # "-flip"/"-flop" (mirror), "-blur 0xN" (gaussian blur), "-sharpen" (sharpen),
178
+ # "-colorspace Gray" (grayscale). Some options are ImageMagick-only and will be
179
+ # skipped with a warning when using the vips backend.
180
+ # This option takes a hash of options, each of which correspond to the style
164
181
  # of thumbnail being generated. You can also specify :all as a key, which will apply
165
182
  # to all of the thumbnails being generated. If you specify options for the :original,
166
183
  # it would be best if you did not specify destructive options, as the intent of keeping
data/paperclip.gemspec CHANGED
@@ -19,11 +19,12 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.post_install_message = File.read("UPGRADING") if File.exist?("UPGRADING")
21
21
 
22
- s.requirements << "ImageMagick"
23
- s.required_ruby_version = ">= 3.2.0"
22
+ s.requirements << "ImageMagick or libvips"
23
+ s.required_ruby_version = ">= 3.0.0"
24
24
 
25
25
  s.add_dependency("activemodel", ">= 7.0.0")
26
26
  s.add_dependency("activesupport", ">= 7.0.0")
27
+ s.add_dependency("image_processing", "~> 1.14")
27
28
  s.add_dependency("marcel", ">= 1.0.1")
28
29
  s.add_dependency("mime-types")
29
30
  s.add_dependency("terrapin", ">= 0.6.0", "< 2.0")
@@ -10,4 +10,304 @@ describe "Attachment Definitions" do
10
10
 
11
11
  expect(Dummy.attachment_definitions).to eq expected
12
12
  end
13
+
14
+ describe "configuration isolation between models" do
15
+ # Helper method to safely remove a constant and clean up its registry entries
16
+ def cleanup_model(class_name)
17
+ return unless Object.const_defined?(class_name)
18
+
19
+ # Remove from Paperclip's AttachmentRegistry to prevent test pollution
20
+ Paperclip::AttachmentRegistry.clear
21
+ # Remove the constant
22
+ Object.send(:remove_const, class_name)
23
+ end
24
+
25
+ before do
26
+ # Clean up any existing class definitions before each test
27
+ cleanup_model(:ModelA)
28
+ cleanup_model(:ModelB)
29
+
30
+ # Create table for ModelA
31
+ ActiveRecord::Migration.create_table :model_as, force: true do |table|
32
+ table.column :attachment_file_name, :string
33
+ table.column :attachment_content_type, :string
34
+ table.column :attachment_file_size, :bigint
35
+ table.column :attachment_updated_at, :datetime
36
+ end
37
+
38
+ # Create table for ModelB
39
+ ActiveRecord::Migration.create_table :model_bs, force: true do |table|
40
+ table.column :attachment_file_name, :string
41
+ table.column :attachment_content_type, :string
42
+ table.column :attachment_file_size, :bigint
43
+ table.column :attachment_updated_at, :datetime
44
+ end
45
+ end
46
+
47
+ after do
48
+ # Clean up model classes to prevent test pollution
49
+ cleanup_model(:ModelA)
50
+ cleanup_model(:ModelB)
51
+
52
+ # Drop tables
53
+ ActiveRecord::Migration.drop_table :model_as, if_exists: true
54
+ ActiveRecord::Migration.drop_table :model_bs, if_exists: true
55
+ end
56
+
57
+ it "does not mutate default_options when configuring multiple models" do
58
+ # Capture original values of mutable options
59
+ original_path = Paperclip::Attachment.default_options[:path].dup
60
+ original_url = Paperclip::Attachment.default_options[:url].dup
61
+ original_default_url = Paperclip::Attachment.default_options[:default_url].dup
62
+ original_styles = Paperclip::Attachment.default_options[:styles].deep_dup
63
+ original_convert_options = Paperclip::Attachment.default_options[:convert_options].deep_dup
64
+
65
+ # Define ModelA with specific configuration
66
+ class ::ModelA < ActiveRecord::Base
67
+ include Paperclip::Glue
68
+ has_attached_file :attachment,
69
+ path: "/model_a/:filename",
70
+ styles: { thumb: "100x100" }
71
+ do_not_validate_attachment_file_type :attachment
72
+ end
73
+
74
+ # Define ModelB with different configuration
75
+ class ::ModelB < ActiveRecord::Base
76
+ include Paperclip::Glue
77
+ has_attached_file :attachment,
78
+ path: "/model_b/:filename",
79
+ styles: { large: "500x500" }
80
+ do_not_validate_attachment_file_type :attachment
81
+ end
82
+
83
+ # Verify default_options was not mutated
84
+ expect(Paperclip::Attachment.default_options[:path]).to eq original_path
85
+ expect(Paperclip::Attachment.default_options[:url]).to eq original_url
86
+ expect(Paperclip::Attachment.default_options[:default_url]).to eq original_default_url
87
+ expect(Paperclip::Attachment.default_options[:styles]).to eq original_styles
88
+ expect(Paperclip::Attachment.default_options[:convert_options]).to eq original_convert_options
89
+ end
90
+
91
+ it "keeps attachment configurations isolated between models" do
92
+ # Define ModelA with specific configuration
93
+ class ::ModelA < ActiveRecord::Base
94
+ include Paperclip::Glue
95
+ has_attached_file :attachment,
96
+ path: "/model_a/:filename",
97
+ styles: { thumb: "100x100" },
98
+ default_url: "/missing_a.png"
99
+ do_not_validate_attachment_file_type :attachment
100
+ end
101
+
102
+ # Define ModelB with different configuration
103
+ class ::ModelB < ActiveRecord::Base
104
+ include Paperclip::Glue
105
+ has_attached_file :attachment,
106
+ path: "/model_b/:filename",
107
+ styles: { large: "500x500" },
108
+ default_url: "/missing_b.png"
109
+ do_not_validate_attachment_file_type :attachment
110
+ end
111
+
112
+ # Create instances and access attachments
113
+ model_a = ModelA.new
114
+ model_b = ModelB.new
115
+
116
+ # Access attachment on ModelA first
117
+ attachment_a = model_a.attachment
118
+
119
+ # Access attachment on ModelB
120
+ attachment_b = model_b.attachment
121
+
122
+ # Verify configurations are isolated
123
+ expect(attachment_a.options[:path]).to eq "/model_a/:filename"
124
+ expect(attachment_a.options[:styles]).to eq({ thumb: "100x100" })
125
+ expect(attachment_a.options[:default_url]).to eq "/missing_a.png"
126
+
127
+ expect(attachment_b.options[:path]).to eq "/model_b/:filename"
128
+ expect(attachment_b.options[:styles]).to eq({ large: "500x500" })
129
+ expect(attachment_b.options[:default_url]).to eq "/missing_b.png"
130
+
131
+ # Verify attachment_definitions are also isolated
132
+ expect(ModelA.attachment_definitions[:attachment][:path]).to eq "/model_a/:filename"
133
+ expect(ModelB.attachment_definitions[:attachment][:path]).to eq "/model_b/:filename"
134
+ end
135
+
136
+ it "does not leak configuration when accessing attachments in different order" do
137
+ # Define ModelA with specific configuration
138
+ class ::ModelA < ActiveRecord::Base
139
+ include Paperclip::Glue
140
+ has_attached_file :attachment,
141
+ path: "/model_a/:filename",
142
+ styles: { thumb: "100x100" }
143
+ do_not_validate_attachment_file_type :attachment
144
+ end
145
+
146
+ # Define ModelB with different configuration
147
+ class ::ModelB < ActiveRecord::Base
148
+ include Paperclip::Glue
149
+ has_attached_file :attachment,
150
+ path: "/model_b/:filename",
151
+ styles: { large: "500x500" }
152
+ do_not_validate_attachment_file_type :attachment
153
+ end
154
+
155
+ # Access ModelB first, then ModelA (reverse order of definition)
156
+ model_b = ModelB.new
157
+ attachment_b = model_b.attachment
158
+
159
+ model_a = ModelA.new
160
+ attachment_a = model_a.attachment
161
+
162
+ # Verify configurations remain correct
163
+ expect(attachment_a.options[:path]).to eq "/model_a/:filename"
164
+ expect(attachment_a.options[:styles]).to eq({ thumb: "100x100" })
165
+
166
+ expect(attachment_b.options[:path]).to eq "/model_b/:filename"
167
+ expect(attachment_b.options[:styles]).to eq({ large: "500x500" })
168
+ end
169
+
170
+ it "does not share options between multiple instances of the same model" do
171
+ class ::ModelA < ActiveRecord::Base
172
+ include Paperclip::Glue
173
+ has_attached_file :attachment,
174
+ path: "/model_a/:filename",
175
+ styles: { thumb: "100x100" }
176
+ do_not_validate_attachment_file_type :attachment
177
+ end
178
+
179
+ instance1 = ModelA.new
180
+ instance2 = ModelA.new
181
+
182
+ attachment1 = instance1.attachment
183
+ attachment2 = instance2.attachment
184
+
185
+ # Verify they have the same configuration values
186
+ expect(attachment1.options[:path]).to eq attachment2.options[:path]
187
+ expect(attachment1.options[:styles]).to eq attachment2.options[:styles]
188
+
189
+ # Verify the values are correct for both instances
190
+ expect(attachment1.options[:path]).to eq "/model_a/:filename"
191
+ expect(attachment2.options[:path]).to eq "/model_a/:filename"
192
+ end
193
+
194
+ context "with global default_options configured first" do
195
+ let(:original_default_options) { Paperclip::Attachment.default_options.deep_dup }
196
+
197
+ before do
198
+ # Simulate a Rails initializer setting global defaults
199
+ Paperclip::Attachment.default_options[:path] = "/global/:class/:attachment/:id/:style/:filename"
200
+ Paperclip::Attachment.default_options[:url] = "/global/:class/:attachment/:id/:style/:filename"
201
+ Paperclip::Attachment.default_options[:default_url] = "/global/missing.png"
202
+ Paperclip::Attachment.default_options[:styles] = { global_thumb: "50x50" }
203
+ end
204
+
205
+ after do
206
+ # Restore original default_options
207
+ Paperclip::Attachment.instance_variable_set(:@default_options, nil)
208
+ end
209
+
210
+ it "keeps model configurations isolated when global defaults are set" do
211
+ # Define ModelA that overrides some global defaults
212
+ class ::ModelA < ActiveRecord::Base
213
+ include Paperclip::Glue
214
+ has_attached_file :attachment,
215
+ path: "/model_a/:filename",
216
+ styles: { thumb: "100x100" }
217
+ do_not_validate_attachment_file_type :attachment
218
+ end
219
+
220
+ # Define ModelB that overrides different global defaults
221
+ class ::ModelB < ActiveRecord::Base
222
+ include Paperclip::Glue
223
+ has_attached_file :attachment,
224
+ path: "/model_b/:filename",
225
+ styles: { large: "500x500" }
226
+ do_not_validate_attachment_file_type :attachment
227
+ end
228
+
229
+ model_a = ModelA.new
230
+ model_b = ModelB.new
231
+
232
+ attachment_a = model_a.attachment
233
+ attachment_b = model_b.attachment
234
+
235
+ # Verify ModelA has its own path, and styles are deep_merged with global defaults
236
+ expect(attachment_a.options[:path]).to eq "/model_a/:filename"
237
+ expect(attachment_a.options[:styles]).to include(thumb: "100x100")
238
+ expect(attachment_a.options[:styles]).to include(global_thumb: "50x50") # inherited from global
239
+ expect(attachment_a.options[:url]).to eq "/global/:class/:attachment/:id/:style/:filename"
240
+ expect(attachment_a.options[:default_url]).to eq "/global/missing.png"
241
+
242
+ # Verify ModelB has its own path, and styles are deep_merged with global defaults
243
+ expect(attachment_b.options[:path]).to eq "/model_b/:filename"
244
+ expect(attachment_b.options[:styles]).to include(large: "500x500")
245
+ expect(attachment_b.options[:styles]).to include(global_thumb: "50x50") # inherited from global
246
+ expect(attachment_b.options[:url]).to eq "/global/:class/:attachment/:id/:style/:filename"
247
+ expect(attachment_b.options[:default_url]).to eq "/global/missing.png"
248
+
249
+ # Verify ModelA's styles do NOT leak to ModelB and vice versa
250
+ expect(attachment_a.options[:styles]).not_to have_key(:large)
251
+ expect(attachment_b.options[:styles]).not_to have_key(:thumb)
252
+ end
253
+
254
+ it "does not mutate global defaults when models override them" do
255
+ # Capture global defaults before defining models
256
+ global_path = Paperclip::Attachment.default_options[:path]
257
+ global_styles = Paperclip::Attachment.default_options[:styles].deep_dup
258
+
259
+ class ::ModelA < ActiveRecord::Base
260
+ include Paperclip::Glue
261
+ has_attached_file :attachment,
262
+ path: "/model_a/:filename",
263
+ styles: { thumb: "100x100" }
264
+ do_not_validate_attachment_file_type :attachment
265
+ end
266
+
267
+ # Access the attachment to trigger any potential mutation
268
+ model_a = ModelA.new
269
+ _attachment_a = model_a.attachment
270
+
271
+ # Global defaults should remain unchanged
272
+ expect(Paperclip::Attachment.default_options[:path]).to eq global_path
273
+ expect(Paperclip::Attachment.default_options[:styles]).to eq global_styles
274
+ end
275
+
276
+ it "does not leak ModelA config to ModelB when both override global defaults" do
277
+ class ::ModelA < ActiveRecord::Base
278
+ include Paperclip::Glue
279
+ has_attached_file :attachment,
280
+ path: "/model_a/:filename",
281
+ styles: { thumb: "100x100" },
282
+ convert_options: { all: "-quality 80" }
283
+ do_not_validate_attachment_file_type :attachment
284
+ end
285
+
286
+ class ::ModelB < ActiveRecord::Base
287
+ include Paperclip::Glue
288
+ has_attached_file :attachment,
289
+ path: "/model_b/:filename",
290
+ styles: { large: "500x500" },
291
+ convert_options: { all: "-quality 90" }
292
+ do_not_validate_attachment_file_type :attachment
293
+ end
294
+
295
+ # Access ModelA first
296
+ model_a = ModelA.new
297
+ attachment_a = model_a.attachment
298
+
299
+ # Then access ModelB
300
+ model_b = ModelB.new
301
+ attachment_b = model_b.attachment
302
+
303
+ # Verify convert_options are isolated
304
+ expect(attachment_a.options[:convert_options]).to eq({ all: "-quality 80" })
305
+ expect(attachment_b.options[:convert_options]).to eq({ all: "-quality 90" })
306
+
307
+ # Verify other options remain isolated
308
+ expect(attachment_a.options[:path]).to eq "/model_a/:filename"
309
+ expect(attachment_b.options[:path]).to eq "/model_b/:filename"
310
+ end
311
+ end
312
+ end
13
313
  end
@@ -670,7 +670,7 @@ describe Paperclip::Attachment do
670
670
  context "when error is meaningful for the end user" do
671
671
  before do
672
672
  expect(Paperclip::Thumbnail).to receive(:make).and_raise(
673
- Paperclip::Errors::NotIdentifiedByImageMagickError,
673
+ Paperclip::Errors::NotIdentifiedByBackendError,
674
674
  "cannot be processed."
675
675
  )
676
676
  end
@@ -1,47 +1,96 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe Paperclip::GeometryDetector do
4
- it "identifies an image and extract its dimensions" do
5
- allow_any_instance_of(Paperclip::GeometryParser).to receive(:make).and_return(:correct)
6
- file = fixture_file("5k.png")
7
- factory = Paperclip::GeometryDetector.new(file)
4
+ [:image_magick, :vips].each do |backend|
5
+ context "when configured to use #{backend}" do
6
+ let(:original_backend) { Paperclip.options[:backend] }
8
7
 
9
- output = factory.make
8
+ before do
9
+ Paperclip.options[:backend] = backend
10
+ end
10
11
 
11
- expect(output).to eq :correct
12
- end
12
+ after do
13
+ Paperclip.options[:backend] = original_backend
14
+ end
13
15
 
14
- it "identifies an image and extract its dimensions and orientation" do
15
- allow_any_instance_of(Paperclip::GeometryParser).to receive(:make).and_return(:correct)
16
- file = fixture_file("rotated.jpg")
17
- factory = Paperclip::GeometryDetector.new(file)
16
+ it "identifies an image and extract its dimensions" do
17
+ allow_any_instance_of(Paperclip::GeometryParser).to receive(:make).and_return(:correct)
18
+ file = fixture_file("5k.png")
19
+ factory = Paperclip::GeometryDetector.new(file)
18
20
 
19
- output = factory.make
21
+ output = factory.make
20
22
 
21
- expect(output).to eq :correct
22
- end
23
+ expect(output).to eq :correct
24
+ end
23
25
 
24
- it "avoids reading EXIF orientation if so configured" do
25
- begin
26
- Paperclip.options[:use_exif_orientation] = false
27
- allow_any_instance_of(Paperclip::GeometryParser).to receive(:make).and_return(:correct)
28
- file = fixture_file("rotated.jpg")
29
- factory = Paperclip::GeometryDetector.new(file)
26
+ it "identifies an image and extract its dimensions and orientation" do
27
+ allow_any_instance_of(Paperclip::GeometryParser).to receive(:make).and_return(:correct)
28
+ file = fixture_file("rotated.jpg")
29
+ factory = Paperclip::GeometryDetector.new(file)
30
30
 
31
- output = factory.make
31
+ output = factory.make
32
32
 
33
- expect(output).to eq :correct
34
- ensure
35
- Paperclip.options[:use_exif_orientation] = true
36
- end
37
- end
33
+ expect(output).to eq :correct
34
+ end
35
+
36
+ it "avoids reading EXIF orientation if so configured" do
37
+ begin
38
+ Paperclip.options[:use_exif_orientation] = false
39
+ allow_any_instance_of(Paperclip::GeometryParser).to receive(:make).and_return(:correct)
40
+ file = fixture_file("rotated.jpg")
41
+ factory = Paperclip::GeometryDetector.new(file)
42
+
43
+ output = factory.make
44
+
45
+ expect(output).to eq :correct
46
+ ensure
47
+ Paperclip.options[:use_exif_orientation] = true
48
+ end
49
+ end
38
50
 
39
- it "raises an exception with a message when the file is not an image" do
40
- file = fixture_file("text.txt")
41
- factory = Paperclip::GeometryDetector.new(file)
51
+ it "raises an exception with a message when the file is not an image" do
52
+ file = fixture_file("text.txt")
53
+ factory = Paperclip::GeometryDetector.new(file)
42
54
 
43
- expect do
44
- factory.make
45
- end.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError, "Could not identify image size")
55
+ expect do
56
+ factory.make
57
+ end.to raise_error(Paperclip::Errors::NotIdentifiedByBackendError, "Could not identify image size")
58
+ end
59
+
60
+ it "uses the correct backend to identify the image" do
61
+ if backend == :vips
62
+ begin
63
+ require "vips"
64
+ rescue LoadError
65
+ skip "ruby-vips gem not available"
66
+ end
67
+ expect(Vips::Image).to receive(:new_from_file).and_call_original
68
+ expect(Paperclip).not_to receive(:run).with(include("identify"), any_args)
69
+ else
70
+ expect(Paperclip).to receive(:run).with(include("identify"), any_args).and_call_original
71
+ end
72
+
73
+ file = fixture_file("5k.png")
74
+ factory = Paperclip::GeometryDetector.new(file)
75
+
76
+ geometry = factory.make
77
+ expect(geometry.width).to eq(434)
78
+ expect(geometry.height).to eq(66)
79
+ end
80
+
81
+ if backend == :vips
82
+ it "raises CommandNotFoundError (and not NameError) when vips is missing" do
83
+ hide_const("Vips")
84
+ allow_any_instance_of(Paperclip::GeometryDetector).to receive(:require).with("vips").and_raise(LoadError)
85
+
86
+ file = fixture_file("5k.png")
87
+ factory = Paperclip::GeometryDetector.new(file)
88
+
89
+ expect {
90
+ factory.make
91
+ }.to raise_error(Paperclip::Errors::CommandNotFoundError, /Could not load ruby-vips/)
92
+ end
93
+ end
94
+ end
46
95
  end
47
96
  end
@@ -149,7 +149,7 @@ describe Paperclip::Geometry do
149
149
  file = "/home/This File Does Not Exist.omg"
150
150
  expect do
151
151
  @geo = Paperclip::Geometry.from_file(file)
152
- end.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError,
152
+ end.to raise_error(Paperclip::Errors::NotIdentifiedByBackendError,
153
153
  "Could not identify image size")
154
154
  end
155
155
 
@@ -157,7 +157,7 @@ describe Paperclip::Geometry do
157
157
  file = ""
158
158
  expect do
159
159
  @geo = Paperclip::Geometry.from_file(file)
160
- end.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError,
160
+ end.to raise_error(Paperclip::Errors::NotIdentifiedByBackendError,
161
161
  "Cannot find the geometry of a file with a blank name")
162
162
  end
163
163
 
@@ -165,7 +165,7 @@ describe Paperclip::Geometry do
165
165
  file = nil
166
166
  expect do
167
167
  @geo = Paperclip::Geometry.from_file(file)
168
- end.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError,
168
+ end.to raise_error(Paperclip::Errors::NotIdentifiedByBackendError,
169
169
  "Cannot find the geometry of a file with a blank name")
170
170
  end
171
171
 
@@ -174,13 +174,15 @@ describe Paperclip::Geometry do
174
174
  allow(file).to receive(:respond_to?).with(:path).and_return(true)
175
175
  expect do
176
176
  @geo = Paperclip::Geometry.from_file(file)
177
- end.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError,
177
+ end.to raise_error(Paperclip::Errors::NotIdentifiedByBackendError,
178
178
  "Cannot find the geometry of a file with a blank name")
179
179
  end
180
180
 
181
- it "lets us know when a command isn't found versus a processing error" do
181
+ it "lets us know when a command isn't found versus a processing error when using imagemagick" do
182
182
  old_path = ENV["PATH"]
183
+ old_backend = Paperclip.options[:backend]
183
184
  begin
185
+ Paperclip.options[:backend] = :image_magick
184
186
  ENV["PATH"] = ""
185
187
  assert_raises(Paperclip::Errors::CommandNotFoundError) do
186
188
  file = fixture_file("5k.png")
@@ -188,6 +190,7 @@ describe Paperclip::Geometry do
188
190
  end
189
191
  ensure
190
192
  ENV["PATH"] = old_path
193
+ Paperclip.options[:backend] = old_backend
191
194
  end
192
195
  end
193
196
 
@@ -0,0 +1,49 @@
1
+ require "spec_helper"
2
+
3
+ describe Paperclip::Helpers do
4
+ describe ".imagemagick7?" do
5
+ before do
6
+ if Paperclip.instance_variable_defined?(:@imagemagick7)
7
+ Paperclip.send(:remove_instance_variable, :@imagemagick7)
8
+ end
9
+ end
10
+
11
+ after do
12
+ if Paperclip.instance_variable_defined?(:@imagemagick7)
13
+ Paperclip.send(:remove_instance_variable, :@imagemagick7)
14
+ end
15
+ end
16
+
17
+ it "returns true if magick command is found" do
18
+ allow(Paperclip).to receive(:which).with("magick").and_return("/usr/bin/magick")
19
+ expect(Paperclip.imagemagick7?).to be true
20
+ end
21
+
22
+ it "returns false if magick command is not found" do
23
+ allow(Paperclip).to receive(:which).with("magick").and_return(nil)
24
+ expect(Paperclip.imagemagick7?).to be false
25
+ end
26
+
27
+ it "caches the result" do
28
+ expect(Paperclip).to receive(:which).with("magick").once.and_return("/usr/bin/magick")
29
+ Paperclip.imagemagick7?
30
+ Paperclip.imagemagick7?
31
+ end
32
+ end
33
+
34
+ describe ".which" do
35
+ it "finds an executable in the PATH" do
36
+ allow(ENV).to receive(:[]).with("PATH").and_return("/usr/bin:/bin")
37
+ allow(ENV).to receive(:[]).with("PATHEXT").and_return(nil)
38
+ allow(File).to receive(:executable?).with("/usr/bin/ruby").and_return(true)
39
+ expect(Paperclip.which("ruby")).to eq("/usr/bin/ruby")
40
+ end
41
+
42
+ it "returns nil if executable is not found" do
43
+ allow(ENV).to receive(:[]).with("PATH").and_return("/usr/bin:/bin")
44
+ allow(ENV).to receive(:[]).with("PATHEXT").and_return(nil)
45
+ allow(File).to receive(:executable?).and_return(false)
46
+ expect(Paperclip.which("nonexistent")).to be_nil
47
+ end
48
+ end
49
+ end