jr-paperclip 8.0.1 → 8.0.3

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/NEWS +9 -0
  3. data/lib/paperclip/thumbnail.rb +18 -15
  4. data/lib/paperclip/version.rb +1 -1
  5. metadata +3 -245
  6. data/.github/FUNDING.yml +0 -3
  7. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -18
  8. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  9. data/.github/workflows/reviewdog.yml +0 -23
  10. data/.github/workflows/tests.yml +0 -56
  11. data/.gitignore +0 -19
  12. data/.qlty/.gitignore +0 -7
  13. data/.qlty/qlty.toml +0 -89
  14. data/Appraisals +0 -29
  15. data/Gemfile +0 -18
  16. data/bin/console +0 -11
  17. data/features/basic_integration.feature +0 -112
  18. data/features/migration.feature +0 -29
  19. data/features/rake_tasks.feature +0 -62
  20. data/features/step_definitions/attachment_steps.rb +0 -138
  21. data/features/step_definitions/html_steps.rb +0 -15
  22. data/features/step_definitions/rails_steps.rb +0 -271
  23. data/features/step_definitions/s3_steps.rb +0 -16
  24. data/features/step_definitions/web_steps.rb +0 -106
  25. data/features/support/env.rb +0 -12
  26. data/features/support/file_helpers.rb +0 -34
  27. data/features/support/fixtures/boot_config.txt +0 -15
  28. data/features/support/fixtures/gemfile.txt +0 -5
  29. data/features/support/fixtures/preinitializer.txt +0 -20
  30. data/features/support/paths.rb +0 -28
  31. data/features/support/rails.rb +0 -39
  32. data/features/support/selectors.rb +0 -19
  33. data/features/support/webmock_setup.rb +0 -8
  34. data/gemfiles/7.0.gemfile +0 -21
  35. data/gemfiles/7.1.gemfile +0 -21
  36. data/gemfiles/7.2.gemfile +0 -21
  37. data/gemfiles/8.0.gemfile +0 -21
  38. data/gemfiles/8.1.gemfile +0 -21
  39. data/paperclip.gemspec +0 -52
  40. data/spec/database.yml +0 -4
  41. data/spec/paperclip/attachment_definitions_spec.rb +0 -313
  42. data/spec/paperclip/attachment_processing_spec.rb +0 -79
  43. data/spec/paperclip/attachment_registry_spec.rb +0 -158
  44. data/spec/paperclip/attachment_spec.rb +0 -1617
  45. data/spec/paperclip/content_type_detector_spec.rb +0 -58
  46. data/spec/paperclip/file_command_content_type_detector_spec.rb +0 -40
  47. data/spec/paperclip/filename_cleaner_spec.rb +0 -13
  48. data/spec/paperclip/geometry_detector_spec.rb +0 -96
  49. data/spec/paperclip/geometry_parser_spec.rb +0 -73
  50. data/spec/paperclip/geometry_spec.rb +0 -270
  51. data/spec/paperclip/glue_spec.rb +0 -63
  52. data/spec/paperclip/has_attached_file_spec.rb +0 -78
  53. data/spec/paperclip/helpers_spec.rb +0 -49
  54. data/spec/paperclip/integration_spec.rb +0 -702
  55. data/spec/paperclip/interpolations_spec.rb +0 -270
  56. data/spec/paperclip/io_adapters/abstract_adapter_spec.rb +0 -160
  57. data/spec/paperclip/io_adapters/attachment_adapter_spec.rb +0 -167
  58. data/spec/paperclip/io_adapters/data_uri_adapter_spec.rb +0 -88
  59. data/spec/paperclip/io_adapters/empty_string_adapter_spec.rb +0 -17
  60. data/spec/paperclip/io_adapters/file_adapter_spec.rb +0 -134
  61. data/spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb +0 -142
  62. data/spec/paperclip/io_adapters/identity_adapter_spec.rb +0 -8
  63. data/spec/paperclip/io_adapters/nil_adapter_spec.rb +0 -25
  64. data/spec/paperclip/io_adapters/registry_spec.rb +0 -35
  65. data/spec/paperclip/io_adapters/stringio_adapter_spec.rb +0 -64
  66. data/spec/paperclip/io_adapters/uploaded_file_adapter_spec.rb +0 -146
  67. data/spec/paperclip/io_adapters/uri_adapter_spec.rb +0 -231
  68. data/spec/paperclip/lazy_thumbnail_compatibility_spec.rb +0 -266
  69. data/spec/paperclip/matchers/have_attached_file_matcher_spec.rb +0 -19
  70. data/spec/paperclip/matchers/validate_attachment_content_type_matcher_spec.rb +0 -108
  71. data/spec/paperclip/matchers/validate_attachment_presence_matcher_spec.rb +0 -69
  72. data/spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb +0 -88
  73. data/spec/paperclip/media_type_spoof_detector_spec.rb +0 -126
  74. data/spec/paperclip/meta_class_spec.rb +0 -30
  75. data/spec/paperclip/migration_guide_example_spec.rb +0 -44
  76. data/spec/paperclip/paperclip_missing_attachment_styles_spec.rb +0 -88
  77. data/spec/paperclip/paperclip_spec.rb +0 -196
  78. data/spec/paperclip/plural_cache_spec.rb +0 -37
  79. data/spec/paperclip/processor_helpers_spec.rb +0 -57
  80. data/spec/paperclip/processor_spec.rb +0 -60
  81. data/spec/paperclip/rails_environment_spec.rb +0 -30
  82. data/spec/paperclip/rake_spec.rb +0 -103
  83. data/spec/paperclip/schema_spec.rb +0 -298
  84. data/spec/paperclip/storage/filesystem_spec.rb +0 -102
  85. data/spec/paperclip/storage/fog_spec.rb +0 -606
  86. data/spec/paperclip/storage/s3_live_spec.rb +0 -188
  87. data/spec/paperclip/storage/s3_spec.rb +0 -1974
  88. data/spec/paperclip/style_spec.rb +0 -309
  89. data/spec/paperclip/tempfile_factory_spec.rb +0 -33
  90. data/spec/paperclip/tempfile_spec.rb +0 -35
  91. data/spec/paperclip/thumbnail_custom_options_spec.rb +0 -225
  92. data/spec/paperclip/thumbnail_loader_options_spec.rb +0 -53
  93. data/spec/paperclip/thumbnail_security_spec.rb +0 -42
  94. data/spec/paperclip/thumbnail_spec.rb +0 -1460
  95. data/spec/paperclip/url_generator_spec.rb +0 -231
  96. data/spec/paperclip/validators/attachment_content_type_validator_spec.rb +0 -410
  97. data/spec/paperclip/validators/attachment_file_name_validator_spec.rb +0 -249
  98. data/spec/paperclip/validators/attachment_presence_validator_spec.rb +0 -85
  99. data/spec/paperclip/validators/attachment_size_validator_spec.rb +0 -325
  100. data/spec/paperclip/validators/media_type_spoof_detection_validator_spec.rb +0 -48
  101. data/spec/paperclip/validators_spec.rb +0 -179
  102. data/spec/spec_helper.rb +0 -52
  103. data/spec/support/assertions.rb +0 -84
  104. data/spec/support/fake_model.rb +0 -24
  105. data/spec/support/fake_rails.rb +0 -12
  106. data/spec/support/fixtures/12k.png +0 -0
  107. data/spec/support/fixtures/50x50.png +0 -0
  108. data/spec/support/fixtures/5k.png +0 -0
  109. data/spec/support/fixtures/animated +0 -0
  110. data/spec/support/fixtures/animated.gif +0 -0
  111. data/spec/support/fixtures/animated.unknown +0 -0
  112. data/spec/support/fixtures/aws_s3.yml +0 -13
  113. data/spec/support/fixtures/bad.png +0 -1
  114. data/spec/support/fixtures/empty.html +0 -1
  115. data/spec/support/fixtures/empty.xlsx +0 -0
  116. data/spec/support/fixtures/fog.yml +0 -8
  117. data/spec/support/fixtures/rotated.jpg +0 -0
  118. data/spec/support/fixtures/s3.yml +0 -8
  119. data/spec/support/fixtures/sample.xlsm +0 -0
  120. data/spec/support/fixtures/spaced file.jpg +0 -0
  121. data/spec/support/fixtures/spaced file.png +0 -0
  122. data/spec/support/fixtures/text.txt +0 -1
  123. data/spec/support/fixtures/twopage.pdf +0 -0
  124. data/spec/support/fixtures/uppercase.PNG +0 -0
  125. data/spec/support/matchers/accept.rb +0 -5
  126. data/spec/support/matchers/exist.rb +0 -5
  127. data/spec/support/matchers/have_column.rb +0 -23
  128. data/spec/support/mock_attachment.rb +0 -24
  129. data/spec/support/mock_interpolator.rb +0 -24
  130. data/spec/support/mock_url_generator_builder.rb +0 -26
  131. data/spec/support/model_reconstruction.rb +0 -72
  132. data/spec/support/reporting.rb +0 -11
  133. data/spec/support/test_data.rb +0 -13
  134. data/spec/support/version_helper.rb +0 -9
@@ -1,1460 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe Paperclip::Thumbnail do
4
- context "An image" do
5
- before do
6
- @file = File.new(fixture_file("5k.png"), "rb")
7
- end
8
-
9
- after { @file.close }
10
-
11
- describe "backend selection" do
12
- let(:attachment) { double("Attachment", options: {}) }
13
-
14
- it "uses vips when specified" do
15
- processor = described_class.new(@file, { geometry: "25x25", backend: :vips }, attachment)
16
- expect(processor.backend).to eq(:vips)
17
- end
18
-
19
- it "uses global default when no backend specified" do
20
- Paperclip.options[:backend] = :image_magick
21
- processor = described_class.new(@file, { geometry: "25x25" }, attachment)
22
- expect(processor.backend).to eq(:image_magick)
23
- end
24
-
25
- it "defaults to image_magick when backend is nil" do
26
- original_backend = Paperclip.options[:backend]
27
- Paperclip.options[:backend] = nil
28
- processor = described_class.new(@file, { geometry: "25x25" }, attachment)
29
- expect(processor.backend).to eq(:image_magick)
30
- Paperclip.options[:backend] = original_backend
31
- end
32
-
33
- it "defaults to image_magick when backend is invalid" do
34
- processor = described_class.new(@file, { geometry: "25x25", backend: :invalid_backend }, attachment)
35
- expect(processor.backend).to eq(:image_magick)
36
- end
37
-
38
- it "logs a warning when backend is invalid" do
39
- expect(Paperclip).to receive(:log).with(/Warning: Invalid backend: invalid_backend/)
40
- described_class.new(@file, { geometry: "25x25", backend: :invalid_backend }, attachment)
41
- end
42
-
43
- it "uses backend from attachment options when not specified in processor options" do
44
- attachment_with_backend = double("Attachment", options: { backend: :vips })
45
- processor = described_class.new(@file, { geometry: "25x25" }, attachment_with_backend)
46
- expect(processor.backend).to eq(:vips)
47
- end
48
-
49
- it "prefers processor option backend over attachment option backend" do
50
- attachment_with_backend = double("Attachment", options: { backend: :vips })
51
- processor = described_class.new(@file, { geometry: "25x25", backend: :image_magick }, attachment_with_backend)
52
- expect(processor.backend).to eq(:image_magick)
53
- end
54
- end
55
-
56
- describe "per-style backend selection (integration)" do
57
- let(:attachment) { double("Attachment", options: {}) }
58
-
59
- it "processes same image with different backends per style" do
60
- begin
61
- require "vips"
62
- rescue LoadError
63
- skip "libvips not installed"
64
- end
65
-
66
- # Simulate per-style backend selection as would happen with:
67
- # styles: {
68
- # vips_thumb: { geometry: "50x50#", backend: :vips },
69
- # magick_thumb: { geometry: "50x50#", backend: :image_magick }
70
- # }
71
-
72
- # Process with vips backend
73
- vips_processor = described_class.new(@file, {
74
- geometry: "50x50#",
75
- backend: :vips,
76
- style: :vips_thumb,
77
- }, attachment)
78
- vips_result = vips_processor.make
79
-
80
- @file.rewind
81
-
82
- # Process with image_magick backend
83
- magick_processor = described_class.new(@file, {
84
- geometry: "50x50#",
85
- backend: :image_magick,
86
- style: :magick_thumb,
87
- }, attachment)
88
- magick_result = magick_processor.make
89
-
90
- # Both should produce valid 50x50 images
91
- vips_dims = `identify -format "%wx%h" "#{vips_result.path}"`.strip
92
- magick_dims = `identify -format "%wx%h" "#{magick_result.path}"`.strip
93
-
94
- expect(vips_dims).to eq("50x50")
95
- expect(magick_dims).to eq("50x50")
96
-
97
- # Verify they used different backends
98
- expect(vips_processor.backend).to eq(:vips)
99
- expect(magick_processor.backend).to eq(:image_magick)
100
- end
101
-
102
- it "allows mixing backends with different geometries" do
103
- begin
104
- require "vips"
105
- rescue LoadError
106
- skip "libvips not installed"
107
- end
108
-
109
- # Large preview with vips (faster for large images)
110
- vips_processor = described_class.new(@file, {
111
- geometry: "200x200>",
112
- backend: :vips,
113
- style: :preview,
114
- }, attachment)
115
- vips_result = vips_processor.make
116
-
117
- @file.rewind
118
-
119
- # Small thumbnail with image_magick
120
- magick_processor = described_class.new(@file, {
121
- geometry: "32x32#",
122
- backend: :image_magick,
123
- style: :icon,
124
- }, attachment)
125
- magick_result = magick_processor.make
126
-
127
- # Verify dimensions
128
- vips_dims = `identify -format "%wx%h" "#{vips_result.path}"`.strip
129
- magick_dims = `identify -format "%wx%h" "#{magick_result.path}"`.strip
130
-
131
- # Original is 434x66, so 200x200> should give 200x30 (shrink to fit)
132
- expect(vips_dims).to eq("200x30")
133
- # 32x32# should give exactly 32x32 (crop to fill)
134
- expect(magick_dims).to eq("32x32")
135
- end
136
- end
137
-
138
- describe "#convert_options?" do
139
- it "returns false when convert_options is nil" do
140
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50")
141
- expect(thumb.convert_options?).to be false
142
- end
143
-
144
- it "returns false when convert_options is empty" do
145
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", convert_options: "")
146
- expect(thumb.convert_options?).to be false
147
- end
148
-
149
- it "returns true when convert_options is set" do
150
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", convert_options: "-strip")
151
- expect(thumb.convert_options?).to be true
152
- end
153
- end
154
-
155
- describe "#transformation_command" do
156
- it "returns an array with resize command" do
157
- thumb = Paperclip::Thumbnail.new(@file, geometry: "100x100")
158
- cmd = thumb.transformation_command
159
- expect(cmd).to be_an(Array)
160
- expect(cmd).to include("-auto-orient")
161
- expect(cmd).to include("-resize")
162
- end
163
-
164
- it "includes crop command when cropping" do
165
- thumb = Paperclip::Thumbnail.new(@file, geometry: "100x100#")
166
- cmd = thumb.transformation_command
167
- expect(cmd).to include("-crop")
168
- expect(cmd).to include("+repage")
169
- end
170
-
171
- it "logs warning when called with vips backend" do
172
- begin
173
- require "vips"
174
- rescue LoadError
175
- skip "libvips not installed"
176
- end
177
-
178
- thumb = Paperclip::Thumbnail.new(@file, geometry: "100x100", backend: :vips)
179
- expect(Paperclip).to receive(:log).with(/Warning.*transformation_command.*vips/)
180
- thumb.transformation_command
181
- end
182
- end
183
-
184
- describe "#make" do
185
- let(:attachment) { double("Attachment", options: {}) }
186
-
187
- context "with vips backend" do
188
- before do
189
- require "vips"
190
- rescue LoadError
191
- skip "libvips not installed"
192
- end
193
-
194
- it "resizes image to specified dimensions" do
195
- processor = described_class.new(@file, { geometry: "25x25>", backend: :vips }, attachment)
196
- result = processor.make
197
-
198
- cmd = %[identify -format "%wx%h" "#{result.path}"]
199
- expect(`#{cmd}`.chomp).to eq("25x4")
200
- end
201
-
202
- it "crops to fill with # modifier" do
203
- processor = described_class.new(@file, { geometry: "30x20#", backend: :vips }, attachment)
204
- result = processor.make
205
-
206
- cmd = %[identify -format "%wx%h" "#{result.path}"]
207
- expect(`#{cmd}`.chomp).to eq("30x20")
208
- end
209
-
210
- it "auto-orients an image using autorot" do
211
- file = File.new(fixture_file("rotated.jpg"), "rb")
212
- processor = described_class.new(file, { geometry: "50x50", backend: :vips }, attachment)
213
-
214
- # Verify it detects logical dimensions (rotated from 300x200 to 200x300)
215
- expect(processor.current_geometry.width).to eq(200)
216
- result = processor.make
217
-
218
- cmd = %[identify -format "%wx%h" "#{result.path}"]
219
- # Original 300x200 with orientation 6 (90 deg CW).
220
- # Post autorot: 200x300.
221
- # Resize to fit 50x50: 33x50.
222
- expect(`#{cmd}`.chomp).to eq("33x50")
223
- end
224
-
225
- it "strips metadata when requested via convert_options" do
226
- processor = described_class.new(@file, { geometry: "50x50", convert_options: "-strip", backend: :vips },
227
- attachment)
228
- result = processor.make
229
-
230
- # identify -verbose shows less output when stripped
231
- expect(`identify -verbose "#{result.path}"`).not_to include("exif:")
232
- end
233
-
234
- it "handles exact dimensions with ! modifier" do
235
- processor = described_class.new(@file, { geometry: "100x50!", backend: :vips }, attachment)
236
- result = processor.make
237
-
238
- cmd = %[identify -format "%wx%h" "#{result.path}"]
239
- expect(`#{cmd}`.chomp).to eq("100x50")
240
- end
241
-
242
- it "stretches the image with ! modifier matching ImageMagick behavior" do
243
- # Create a 3-stripe image: Red, Green, Blue
244
- stripe_file = Tempfile.new(["stripe", ".png"])
245
- Paperclip.run("convert", "-size 100x100 xc:red xc:green xc:blue +append #{stripe_file.path}")
246
- file = File.new(stripe_file.path, "rb")
247
-
248
- processor = described_class.new(file, { geometry: "100x100!", backend: :vips }, attachment)
249
- result = processor.make
250
-
251
- # Check color at x=10 (Red stripe).
252
- # If cropped (Green center), it would be Green.
253
- # If stretched, it is Red.
254
- color = Paperclip.run("convert", "#{result.path}[1x1+10+50] -format \"%[pixel:p{0,0}]\" info:")
255
- expect(color).to match(/red|#FF0000|rgb\(255,0,0\)|srgb\(255,0,0\)/i)
256
-
257
- file.close
258
- stripe_file.close
259
- stripe_file.unlink
260
- end
261
-
262
- it "handles percentage with % modifier" do
263
- processor = described_class.new(@file, { geometry: "50%", backend: :vips }, attachment)
264
- result = processor.make
265
-
266
- cmd = %[identify -format "%wx%h" "#{result.path}"]
267
- # Original is 434x66. 50% is 217x33.
268
- expect(`#{cmd}`.chomp).to eq("217x33")
269
- end
270
-
271
- it "handles minimum dimensions with ^ modifier" do
272
- processor = described_class.new(@file, { geometry: "100x100^", backend: :vips }, attachment)
273
- result = processor.make
274
-
275
- cmd = %[identify -format "%wx%h" "#{result.path}"]
276
- # Original 434x66.
277
- # Resize to fill 100x100 means height becomes 100, width becomes 434 * (100/66) = 658.
278
- expect(`#{cmd}`.chomp).to eq("658x100")
279
- end
280
-
281
- it "handles enlarge only with < modifier" do
282
- # Smaller image: 50x50.png
283
- small_file = File.new(fixture_file("50x50.png"), "rb")
284
- processor = described_class.new(small_file, { geometry: "100x100<", backend: :vips }, attachment)
285
- result = processor.make
286
-
287
- cmd = %[identify -format "%wx%h" "#{result.path}"]
288
- expect(`#{cmd}`.chomp).to eq("100x100")
289
-
290
- # Larger image: 5k.png (434x66)
291
- processor = described_class.new(@file, { geometry: "100x100<", backend: :vips }, attachment)
292
- result = processor.make
293
- expect(`identify -format "%wx%h" "#{result.path}"`.chomp).to eq("434x66")
294
- end
295
-
296
- it "takes only the first frame of a PDF by default" do
297
- pdf_file = File.new(fixture_file("twopage.pdf"), "rb")
298
- processor = described_class.new(pdf_file, { geometry: "100x100", format: :png, backend: :vips }, attachment)
299
- result = processor.make
300
-
301
- cmd = %[identify -format "%n" "#{result.path}"]
302
- expect(`#{cmd}`.chomp).to eq("1")
303
- end
304
-
305
- it "detects animated source correctly" do
306
- animated_file = File.new(fixture_file("animated.gif"), "rb")
307
- processor = described_class.new(animated_file, { geometry: "50x50", backend: :vips }, attachment)
308
- expect(processor.send(:animated_source?)).to be true
309
-
310
- static_file = File.new(fixture_file("5k.png"), "rb")
311
- processor = described_class.new(static_file, { geometry: "50x50", backend: :vips }, attachment)
312
- expect(processor.send(:animated_source?)).to be false
313
- end
314
-
315
- it "handles area-based resize with @ modifier" do
316
- # Original is 434x66 = 28644 pixels
317
- # 10000@ should resize to ~sqrt(10000/28644) * dimensions
318
- processor = described_class.new(@file, { geometry: "10000@", backend: :vips }, attachment)
319
- result = processor.make
320
-
321
- cmd = %[identify -format "%wx%h" "#{result.path}"]
322
- dimensions = `#{cmd}`.chomp
323
- width, height = dimensions.split("x").map(&:to_i)
324
- area = width * height
325
- # Should be approximately 10000 pixels (with some tolerance)
326
- expect(area).to be_within(500).of(10000)
327
- end
328
-
329
- it "handles area-based shrink-only with @> modifier" do
330
- # Original is 434x66 = 28644 pixels
331
- # 10000@> should resize (smaller than 28644)
332
- processor = described_class.new(@file, { geometry: "10000@>", backend: :vips }, attachment)
333
- result = processor.make
334
-
335
- cmd = %[identify -format "%wx%h" "#{result.path}"]
336
- dimensions = `#{cmd}`.chomp
337
- width, height = dimensions.split("x").map(&:to_i)
338
- area = width * height
339
- expect(area).to be_within(500).of(10000)
340
-
341
- # 50000@> should NOT resize (larger than 28644, and only_shrink is true)
342
- processor = described_class.new(@file, { geometry: "50000@>", backend: :vips }, attachment)
343
- result = processor.make
344
- cmd = %[identify -format "%wx%h" "#{result.path}"]
345
- expect(`#{cmd}`.chomp).to eq("434x66")
346
- end
347
-
348
- it "handles area-based shrink-only with >@ modifier" do
349
- # Same as @> but different syntax
350
- processor = described_class.new(@file, { geometry: "10000>@", backend: :vips }, attachment)
351
- result = processor.make
352
-
353
- cmd = %[identify -format "%wx%h" "#{result.path}"]
354
- dimensions = `#{cmd}`.chomp
355
- width, height = dimensions.split("x").map(&:to_i)
356
- area = width * height
357
- expect(area).to be_within(500).of(10000)
358
- end
359
- end
360
-
361
- context "with image_magick backend" do
362
- it "resizes image to specified dimensions" do
363
- processor = described_class.new(@file, { geometry: "25x25>", backend: :image_magick }, attachment)
364
- result = processor.make
365
-
366
- cmd = %[identify -format "%wx%h" "#{result.path}"]
367
- expect(`#{cmd}`.chomp).to eq("25x4")
368
- end
369
-
370
- it "handles area-based resize with @ modifier" do
371
- # Original is 434x66 = 28644 pixels
372
- processor = described_class.new(@file, { geometry: "10000@", backend: :image_magick }, attachment)
373
- result = processor.make
374
-
375
- cmd = %[identify -format "%wx%h" "#{result.path}"]
376
- dimensions = `#{cmd}`.chomp
377
- width, height = dimensions.split("x").map(&:to_i)
378
- area = width * height
379
- expect(area).to be_within(500).of(10000)
380
- end
381
-
382
- it "handles area-based shrink-only with @> modifier" do
383
- processor = described_class.new(@file, { geometry: "10000@>", backend: :image_magick }, attachment)
384
- result = processor.make
385
-
386
- cmd = %[identify -format "%wx%h" "#{result.path}"]
387
- dimensions = `#{cmd}`.chomp
388
- width, height = dimensions.split("x").map(&:to_i)
389
- area = width * height
390
- expect(area).to be_within(500).of(10000)
391
-
392
- # Larger area should NOT resize
393
- processor = described_class.new(@file, { geometry: "50000@>", backend: :image_magick }, attachment)
394
- result = processor.make
395
- cmd = %[identify -format "%wx%h" "#{result.path}"]
396
- expect(`#{cmd}`.chomp).to eq("434x66")
397
- end
398
-
399
- it "applies cross-platform convert_options with vips without warning" do
400
- begin
401
- require "vips"
402
- rescue LoadError
403
- skip "libvips not installed"
404
- end
405
-
406
- # -strip is cross-platform, should work without warning
407
- expect(Paperclip).not_to receive(:log).with(/Warning/)
408
- processor = described_class.new(@file, {
409
- geometry: "50x50",
410
- backend: :vips,
411
- convert_options: "-strip",
412
- }, attachment)
413
- processor.make
414
- end
415
-
416
- it "logs warning for ImageMagick-only convert_options with vips" do
417
- begin
418
- require "vips"
419
- rescue LoadError
420
- skip "libvips not installed"
421
- end
422
-
423
- # -density is ImageMagick-only, should warn
424
- expect(Paperclip).to receive(:log).with(/Warning.*density.*not supported.*vips/)
425
- processor = described_class.new(@file, {
426
- geometry: "50x50",
427
- backend: :vips,
428
- convert_options: "-density 150",
429
- }, attachment)
430
- processor.make
431
- end
432
- end
433
- end
434
-
435
- describe "convert_options - individual options" do
436
- let(:attachment) { double("Attachment", options: {}) }
437
-
438
- # Helper to create a thumbnail with specific convert_options
439
- def make_thumb_with_options(file, options_string)
440
- thumb = Paperclip::Thumbnail.new(file, {
441
- geometry: "100x100",
442
- convert_options: options_string,
443
- backend: :image_magick,
444
- }, attachment)
445
- thumb.make
446
- end
447
-
448
- describe "-strip" do
449
- it "removes EXIF metadata" do
450
- file = File.new(fixture_file("rotated.jpg"), "rb")
451
- result = make_thumb_with_options(file, "-strip")
452
-
453
- exif = `identify -format "%[exif:orientation]" "#{result.path}" 2>/dev/null`.strip
454
- expect(exif).to be_empty
455
- file.close
456
- end
457
- end
458
-
459
- describe "-quality" do
460
- it "sets JPEG quality" do
461
- file = File.new(fixture_file("rotated.jpg"), "rb")
462
- result_low = make_thumb_with_options(file, "-quality 20")
463
- file.rewind
464
- result_high = make_thumb_with_options(file, "-quality 95")
465
-
466
- # Lower quality should produce smaller file
467
- expect(File.size(result_low.path)).to be < File.size(result_high.path)
468
- file.close
469
- end
470
- end
471
-
472
- describe "-colorspace" do
473
- it "converts to grayscale" do
474
- result = make_thumb_with_options(@file, "-colorspace Gray")
475
-
476
- colorspace = `identify -format "%[colorspace]" "#{result.path}"`.strip
477
- expect(colorspace).to eq("Gray")
478
- end
479
-
480
- it "converts to sRGB" do
481
- result = make_thumb_with_options(@file, "-colorspace sRGB")
482
-
483
- colorspace = `identify -format "%[colorspace]" "#{result.path}"`.strip
484
- expect(colorspace).to eq("sRGB")
485
- end
486
- end
487
-
488
- describe "-rotate" do
489
- it "rotates the image by specified degrees" do
490
- # Original is 434x66, after 90 degree rotation should be 66x434
491
- result = make_thumb_with_options(@file, "-rotate 90")
492
-
493
- dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
494
- # After resize to 100x100 and rotate, dimensions change
495
- expect(dimensions).to match(/\d+x\d+/)
496
- end
497
- end
498
-
499
- describe "-flip" do
500
- it "flips the image vertically" do
501
- result = make_thumb_with_options(@file, "-flip")
502
- expect(File.exist?(result.path)).to be true
503
- end
504
- end
505
-
506
- describe "-flop" do
507
- it "flops the image horizontally" do
508
- result = make_thumb_with_options(@file, "-flop")
509
- expect(File.exist?(result.path)).to be true
510
- end
511
- end
512
-
513
- describe "-negate" do
514
- it "negates the image colors" do
515
- result = make_thumb_with_options(@file, "-negate")
516
- expect(File.exist?(result.path)).to be true
517
- end
518
- end
519
-
520
- describe "-normalize" do
521
- it "normalizes the image" do
522
- result = make_thumb_with_options(@file, "-normalize")
523
- expect(File.exist?(result.path)).to be true
524
- end
525
- end
526
-
527
- describe "-equalize" do
528
- it "equalizes the image histogram" do
529
- result = make_thumb_with_options(@file, "-equalize")
530
- expect(File.exist?(result.path)).to be true
531
- end
532
- end
533
-
534
- describe "-auto_orient" do
535
- it "auto-orients the image" do
536
- file = File.new(fixture_file("rotated.jpg"), "rb")
537
- result = make_thumb_with_options(file, "-auto-orient")
538
- expect(File.exist?(result.path)).to be true
539
- file.close
540
- end
541
- end
542
-
543
- describe "-blur" do
544
- it "blurs the image" do
545
- result = make_thumb_with_options(@file, "-blur 0x2")
546
- expect(File.exist?(result.path)).to be true
547
- end
548
- end
549
-
550
- describe "-sharpen" do
551
- it "sharpens the image" do
552
- result = make_thumb_with_options(@file, "-sharpen 0x1")
553
- expect(File.exist?(result.path)).to be true
554
- end
555
- end
556
-
557
- describe "-density" do
558
- it "sets the image density" do
559
- result = make_thumb_with_options(@file, "-density 150")
560
-
561
- density = `identify -format "%x" "#{result.path}"`.strip
562
- expect(density).to start_with("150")
563
- end
564
- end
565
-
566
- describe "-depth" do
567
- it "sets the bit depth" do
568
- result = make_thumb_with_options(@file, "-depth 8")
569
- expect(File.exist?(result.path)).to be true
570
- end
571
- end
572
-
573
- describe "-interlace" do
574
- it "sets interlacing mode" do
575
- # Use JPEG to test interlacing (PNG reports format name instead)
576
- file = File.new(fixture_file("rotated.jpg"), "rb")
577
- thumb = Paperclip::Thumbnail.new(file, {
578
- geometry: "100x100",
579
- convert_options: "-interlace Plane",
580
- backend: :image_magick,
581
- format: :jpg,
582
- }, attachment)
583
- result = thumb.make
584
-
585
- interlace = `identify -format "%[interlace]" "#{result.path}"`.strip
586
- # Different ImageMagick versions report this differently
587
- expect(interlace).to match(/Plane|JPEG|Line|None/i).or eq("")
588
- file.close
589
- end
590
- end
591
-
592
- describe "-gravity" do
593
- it "sets gravity for subsequent operations" do
594
- result = make_thumb_with_options(@file, "-gravity center")
595
- expect(File.exist?(result.path)).to be true
596
- end
597
- end
598
-
599
- describe "-crop" do
600
- it "crops the image" do
601
- # Use a square image for predictable crop results
602
- file = File.new(fixture_file("50x50.png"), "rb")
603
- thumb = Paperclip::Thumbnail.new(file, {
604
- geometry: "50x50", # Keep original size
605
- convert_options: "-crop 25x25+0+0 +repage",
606
- backend: :image_magick,
607
- }, attachment)
608
- result = thumb.make
609
-
610
- dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
611
- expect(dimensions).to eq("25x25")
612
- file.close
613
- end
614
- end
615
-
616
- describe "-extent" do
617
- it "sets the image extent with padding" do
618
- result = make_thumb_with_options(@file, "-background white -gravity center -extent 150x150")
619
-
620
- dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
621
- expect(dimensions).to eq("150x150")
622
- end
623
- end
624
-
625
- describe "-background" do
626
- it "sets the background color" do
627
- result = make_thumb_with_options(@file, "-background red -flatten")
628
- expect(File.exist?(result.path)).to be true
629
- end
630
- end
631
-
632
- describe "-flatten" do
633
- it "flattens the image layers" do
634
- result = make_thumb_with_options(@file, "-flatten")
635
- expect(File.exist?(result.path)).to be true
636
- end
637
- end
638
-
639
- describe "-alpha" do
640
- it "modifies alpha channel" do
641
- result = make_thumb_with_options(@file, "-alpha remove")
642
- expect(File.exist?(result.path)).to be true
643
- end
644
- end
645
-
646
- describe "-type" do
647
- it "sets the image type" do
648
- result = make_thumb_with_options(@file, "-type Grayscale")
649
- expect(File.exist?(result.path)).to be true
650
- end
651
- end
652
-
653
- describe "-monochrome" do
654
- it "converts to black and white" do
655
- result = make_thumb_with_options(@file, "-monochrome")
656
- expect(File.exist?(result.path)).to be true
657
- end
658
- end
659
-
660
- describe "-posterize" do
661
- it "reduces color levels" do
662
- result = make_thumb_with_options(@file, "-posterize 4")
663
- expect(File.exist?(result.path)).to be true
664
- end
665
- end
666
-
667
- describe "-colors" do
668
- it "reduces the number of colors" do
669
- result = make_thumb_with_options(@file, "-colors 16")
670
- expect(File.exist?(result.path)).to be true
671
- end
672
- end
673
-
674
- describe "-channel" do
675
- it "selects image channels" do
676
- result = make_thumb_with_options(@file, "-channel RGB")
677
- expect(File.exist?(result.path)).to be true
678
- end
679
- end
680
-
681
- describe "-transpose" do
682
- it "transposes the image" do
683
- result = make_thumb_with_options(@file, "-transpose")
684
- expect(File.exist?(result.path)).to be true
685
- end
686
- end
687
-
688
- describe "-transverse" do
689
- it "transverses the image" do
690
- result = make_thumb_with_options(@file, "-transverse")
691
- expect(File.exist?(result.path)).to be true
692
- end
693
- end
694
-
695
- describe "-trim" do
696
- it "trims image edges" do
697
- result = make_thumb_with_options(@file, "-trim")
698
- expect(File.exist?(result.path)).to be true
699
- end
700
- end
701
-
702
- describe "-dither" do
703
- it "applies dithering" do
704
- result = make_thumb_with_options(@file, "-dither FloydSteinberg")
705
- expect(File.exist?(result.path)).to be true
706
- end
707
- end
708
-
709
- describe "-sampling_factor" do
710
- it "sets chroma subsampling" do
711
- file = File.new(fixture_file("rotated.jpg"), "rb")
712
- result = make_thumb_with_options(file, "-sampling-factor 4:2:0")
713
- expect(File.exist?(result.path)).to be true
714
- file.close
715
- end
716
- end
717
-
718
- describe "-units" do
719
- it "sets resolution units" do
720
- result = make_thumb_with_options(@file, "-units PixelsPerInch")
721
- expect(File.exist?(result.path)).to be true
722
- end
723
- end
724
-
725
- describe "unknown options (fallback to append)" do
726
- it "passes unknown options through to ImageMagick" do
727
- # -modulate is not in our explicit list, should use append fallback
728
- result = make_thumb_with_options(@file, "-modulate 100,50,100")
729
- expect(File.exist?(result.path)).to be true
730
- end
731
-
732
- it "handles unknown flag-only options" do
733
- # Use a harmless option that's not in our list
734
- result = make_thumb_with_options(@file, "-verbose")
735
- expect(File.exist?(result.path)).to be true
736
- end
737
- end
738
-
739
- describe "multiple options combined" do
740
- it "applies multiple options in sequence" do
741
- result = make_thumb_with_options(@file, "-strip -quality 80 -colorspace Gray -sharpen 0x1")
742
-
743
- colorspace = `identify -format "%[colorspace]" "#{result.path}"`.strip
744
- expect(colorspace).to eq("Gray")
745
- end
746
-
747
- it "handles complex option combinations" do
748
- result = make_thumb_with_options(@file, "-strip -density 72 -depth 8 -colorspace sRGB")
749
- expect(File.exist?(result.path)).to be true
750
-
751
- density = `identify -format "%x" "#{result.path}"`.strip
752
- expect(density).to start_with("72")
753
- end
754
- end
755
- end
756
-
757
- describe "convert_options - cross-platform options with vips backend" do
758
- let(:attachment) { double("Attachment", options: {}) }
759
-
760
- before do
761
- require "vips"
762
- rescue LoadError
763
- skip "libvips not installed"
764
- end
765
-
766
- # Helper to create a thumbnail with vips backend and specific convert_options
767
- def make_vips_thumb_with_options(file, options_string, extra_opts = {})
768
- thumb = Paperclip::Thumbnail.new(file, {
769
- geometry: "100x100",
770
- convert_options: options_string,
771
- backend: :vips,
772
- }.merge(extra_opts), attachment)
773
- thumb.make
774
- end
775
-
776
- describe "-strip" do
777
- it "removes metadata from the image" do
778
- file = File.new(fixture_file("rotated.jpg"), "rb")
779
- result = make_vips_thumb_with_options(file, "-strip")
780
-
781
- # Check that EXIF orientation is removed
782
- exif = `identify -format "%[exif:orientation]" "#{result.path}" 2>/dev/null`.strip
783
- expect(exif).to be_empty
784
- file.close
785
- end
786
-
787
- it "produces a valid image" do
788
- result = make_vips_thumb_with_options(@file, "-strip")
789
- expect(File.exist?(result.path)).to be true
790
- expect(File.size(result.path)).to be > 0
791
- end
792
- end
793
-
794
- describe "-quality" do
795
- it "sets output quality for JPEG" do
796
- file = File.new(fixture_file("rotated.jpg"), "rb")
797
- result_low = make_vips_thumb_with_options(file, "-quality 20", format: :jpg)
798
- file.rewind
799
- result_high = make_vips_thumb_with_options(file, "-quality 95", format: :jpg)
800
-
801
- # Lower quality should produce smaller file
802
- expect(File.size(result_low.path)).to be < File.size(result_high.path)
803
- file.close
804
- end
805
-
806
- it "produces a valid image" do
807
- result = make_vips_thumb_with_options(@file, "-quality 80")
808
- expect(File.exist?(result.path)).to be true
809
- end
810
- end
811
-
812
- describe "-rotate" do
813
- it "rotates the image by specified degrees" do
814
- # Original 434x66, after 90 degree rotation dimensions swap
815
- result = make_vips_thumb_with_options(@file, "-rotate 90")
816
-
817
- dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
818
- # After resize to fit 100x100, then rotate 90 degrees
819
- # Original aspect ratio is ~6.5:1, fitting in 100x100 gives ~100x15
820
- # After 90 degree rotation: ~15x100
821
- expect(dimensions).to match(/\d+x\d+/)
822
- width, height = dimensions.split("x").map(&:to_i)
823
- # Width should be smaller than height after rotation
824
- expect(width).to be < height
825
- end
826
-
827
- it "rotates by arbitrary angle" do
828
- result = make_vips_thumb_with_options(@file, "-rotate 45")
829
- expect(File.exist?(result.path)).to be true
830
- end
831
- end
832
-
833
- describe "-flip" do
834
- it "flips the image vertically" do
835
- result = make_vips_thumb_with_options(@file, "-flip")
836
- expect(File.exist?(result.path)).to be true
837
- expect(File.size(result.path)).to be > 0
838
- end
839
- end
840
-
841
- describe "-flop" do
842
- it "flops the image horizontally" do
843
- result = make_vips_thumb_with_options(@file, "-flop")
844
- expect(File.exist?(result.path)).to be true
845
- expect(File.size(result.path)).to be > 0
846
- end
847
- end
848
-
849
- describe "-blur" do
850
- it "applies gaussian blur to the image" do
851
- result = make_vips_thumb_with_options(@file, "-blur 0x2")
852
- expect(File.exist?(result.path)).to be true
853
- expect(File.size(result.path)).to be > 0
854
- end
855
-
856
- it "handles different blur sigma values" do
857
- result = make_vips_thumb_with_options(@file, "-blur 0x5")
858
- expect(File.exist?(result.path)).to be true
859
- end
860
- end
861
-
862
- describe "-gaussian_blur" do
863
- it "applies gaussian blur" do
864
- result = make_vips_thumb_with_options(@file, "-gaussian-blur 0x3")
865
- expect(File.exist?(result.path)).to be true
866
- expect(File.size(result.path)).to be > 0
867
- end
868
- end
869
-
870
- describe "-sharpen" do
871
- it "sharpens the image" do
872
- result = make_vips_thumb_with_options(@file, "-sharpen 0x1")
873
- expect(File.exist?(result.path)).to be true
874
- expect(File.size(result.path)).to be > 0
875
- end
876
- end
877
-
878
- describe "-colorspace" do
879
- it "converts to grayscale (b-w)" do
880
- result = make_vips_thumb_with_options(@file, "-colorspace Gray")
881
-
882
- colorspace = `identify -format "%[colorspace]" "#{result.path}"`.strip
883
- expect(colorspace).to eq("Gray")
884
- end
885
-
886
- it "converts to sRGB" do
887
- result = make_vips_thumb_with_options(@file, "-colorspace sRGB")
888
-
889
- colorspace = `identify -format "%[colorspace]" "#{result.path}"`.strip
890
- expect(colorspace).to eq("sRGB")
891
- end
892
-
893
- it "converts to CMYK" do
894
- file = File.new(fixture_file("rotated.jpg"), "rb")
895
- result = make_vips_thumb_with_options(file, "-colorspace CMYK", format: :jpg)
896
-
897
- colorspace = `identify -format "%[colorspace]" "#{result.path}"`.strip
898
- expect(colorspace).to eq("CMYK")
899
- file.close
900
- end
901
- end
902
-
903
- describe "-flatten" do
904
- it "flattens transparency to white background" do
905
- result = make_vips_thumb_with_options(@file, "-flatten")
906
- expect(File.exist?(result.path)).to be true
907
- expect(File.size(result.path)).to be > 0
908
- end
909
- end
910
-
911
- describe "-negate" do
912
- it "inverts the image colors" do
913
- result = make_vips_thumb_with_options(@file, "-negate")
914
- expect(File.exist?(result.path)).to be true
915
- expect(File.size(result.path)).to be > 0
916
- end
917
- end
918
-
919
- describe "-auto-orient" do
920
- it "auto-orients the image based on EXIF" do
921
- file = File.new(fixture_file("rotated.jpg"), "rb")
922
- result = make_vips_thumb_with_options(file, "-auto-orient")
923
- expect(File.exist?(result.path)).to be true
924
-
925
- # The rotated.jpg has orientation 6 (90 CW), so auto-orient should correct it
926
- dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
927
- width, height = dimensions.split("x").map(&:to_i)
928
- # After auto-orient, portrait orientation should be maintained
929
- expect(height).to be > width
930
- file.close
931
- end
932
- end
933
-
934
- describe "-interlace" do
935
- it "creates progressive/interlaced output for JPEG" do
936
- file = File.new(fixture_file("rotated.jpg"), "rb")
937
- result = make_vips_thumb_with_options(file, "-interlace Plane", format: :jpg)
938
- expect(File.exist?(result.path)).to be true
939
- expect(File.size(result.path)).to be > 0
940
- file.close
941
- end
942
-
943
- it "creates interlaced output for PNG" do
944
- result = make_vips_thumb_with_options(@file, "-interlace Line", format: :png)
945
- expect(File.exist?(result.path)).to be true
946
- end
947
- end
948
-
949
- describe "multiple cross-platform options combined" do
950
- it "applies multiple options in sequence" do
951
- result = make_vips_thumb_with_options(@file, "-strip -quality 80 -colorspace Gray")
952
-
953
- colorspace = `identify -format "%[colorspace]" "#{result.path}"`.strip
954
- expect(colorspace).to eq("Gray")
955
- end
956
-
957
- it "combines flip, flop, and rotate" do
958
- result = make_vips_thumb_with_options(@file, "-flip -flop -rotate 180")
959
- expect(File.exist?(result.path)).to be true
960
- end
961
-
962
- it "applies strip with quality and sharpen" do
963
- file = File.new(fixture_file("rotated.jpg"), "rb")
964
- result = make_vips_thumb_with_options(file, "-strip -quality 85 -sharpen 0x1", format: :jpg)
965
-
966
- # Verify EXIF is stripped
967
- exif = `identify -format "%[exif:orientation]" "#{result.path}" 2>/dev/null`.strip
968
- expect(exif).to be_empty
969
- file.close
970
- end
971
- end
972
-
973
- describe "ImageMagick-only options with vips (should warn)" do
974
- it "logs warning for -density" do
975
- expect(Paperclip).to receive(:log).with(/Warning.*density.*not supported.*vips/)
976
- make_vips_thumb_with_options(@file, "-density 150")
977
- end
978
-
979
- it "logs warning for -depth" do
980
- expect(Paperclip).to receive(:log).with(/Warning.*depth.*not supported.*vips/)
981
- make_vips_thumb_with_options(@file, "-depth 8")
982
- end
983
-
984
- it "logs warning for -gravity" do
985
- expect(Paperclip).to receive(:log).with(/Warning.*gravity.*not supported.*vips/)
986
- make_vips_thumb_with_options(@file, "-gravity center")
987
- end
988
-
989
- it "logs warning for -crop" do
990
- expect(Paperclip).to receive(:log).with(/Warning.*crop.*not supported.*vips/)
991
- make_vips_thumb_with_options(@file, "-crop 50x50+0+0")
992
- end
993
-
994
- it "logs warning for -trim" do
995
- expect(Paperclip).to receive(:log).with(/Warning.*trim.*not supported.*vips/)
996
- make_vips_thumb_with_options(@file, "-trim")
997
- end
998
-
999
- it "logs warning for -normalize" do
1000
- expect(Paperclip).to receive(:log).with(/Warning.*normalize.*not supported.*vips/)
1001
- make_vips_thumb_with_options(@file, "-normalize")
1002
- end
1003
-
1004
- it "logs warning for -monochrome" do
1005
- expect(Paperclip).to receive(:log).with(/Warning.*monochrome.*not supported.*vips/)
1006
- make_vips_thumb_with_options(@file, "-monochrome")
1007
- end
1008
- end
1009
- end
1010
-
1011
- [%w[600x600> 434x66],
1012
- %w[400x400> 400x61],
1013
- %w[32x32< 434x66],
1014
- [nil, "434x66"]].each do |args|
1015
- context "being thumbnailed with a geometry of #{args[0]}" do
1016
- before do
1017
- @thumb = Paperclip::Thumbnail.new(@file, geometry: args[0])
1018
- end
1019
-
1020
- it "starts with dimensions of 434x66" do
1021
- cmd = %[identify -format "%wx%h" "#{@file.path}"]
1022
- assert_equal "434x66", `#{cmd}`.chomp
1023
- end
1024
-
1025
- it "reports the correct target geometry" do
1026
- assert_equal args[0].to_s, @thumb.target_geometry.to_s
1027
- end
1028
-
1029
- context "when made" do
1030
- before do
1031
- @thumb_result = @thumb.make
1032
- end
1033
-
1034
- it "is the size we expect it to be" do
1035
- cmd = %[identify -format "%wx%h" "#{@thumb_result.path}"]
1036
- assert_equal args[1], `#{cmd}`.chomp
1037
- end
1038
- end
1039
- end
1040
- end
1041
-
1042
- context "being thumbnailed at 100x50 with cropping" do
1043
- before do
1044
- @thumb = Paperclip::Thumbnail.new(@file, geometry: "100x50#")
1045
- end
1046
-
1047
- it "reports its correct current and target geometries" do
1048
- assert_equal "100x50#", @thumb.target_geometry.to_s
1049
- assert_equal "434x66", @thumb.current_geometry.to_s
1050
- end
1051
-
1052
- it "reports its correct format" do
1053
- assert_nil @thumb.format
1054
- end
1055
-
1056
- it "has whiny turned on by default" do
1057
- assert @thumb.whiny
1058
- end
1059
-
1060
- it "has convert_options set to nil by default" do
1061
- assert_equal nil, @thumb.convert_options
1062
- end
1063
-
1064
- it "has source_file_options set to nil by default" do
1065
- assert_equal nil, @thumb.source_file_options
1066
- end
1067
-
1068
- it "creates the thumbnail when sent #make" do
1069
- dst = @thumb.make
1070
- assert_match /100x50/, `identify "#{dst.path}"`
1071
- end
1072
- end
1073
-
1074
- it "crops a EXIF-rotated image properly" do
1075
- file = File.new(fixture_file("rotated.jpg"))
1076
- thumb = Paperclip::Thumbnail.new(file, geometry: "50x50#")
1077
-
1078
- output_file = thumb.make
1079
-
1080
- command = Terrapin::CommandLine.new("identify", "-format %wx%h :file")
1081
- assert_equal "50x50", command.run(file: output_file.path).strip
1082
- end
1083
-
1084
- context "being thumbnailed with source file options set" do
1085
- before do
1086
- @file = File.new(fixture_file("rotated.jpg"), "rb")
1087
- @thumb = Paperclip::Thumbnail.new(@file,
1088
- geometry: "100x50#",
1089
- source_file_options: "-density 300")
1090
- end
1091
-
1092
- it "has source_file_options value set" do
1093
- assert_equal "-density 300", @thumb.source_file_options
1094
- end
1095
-
1096
- it "creates the thumbnail when sent #make" do
1097
- dst = @thumb.make
1098
- assert_match /100x50/, `identify "#{dst.path}"`
1099
- end
1100
-
1101
- it "actually applies the source file options (sets density)" do
1102
- # Verify result has the set density
1103
- dst = @thumb.make
1104
- cmd_new = %[identify -format "%x" "#{dst.path}"]
1105
- expect(`#{cmd_new}`.chomp).to start_with("300")
1106
- end
1107
- end
1108
-
1109
- context "being thumbnailed with convert options set" do
1110
- before do
1111
- @file = File.new(fixture_file("rotated.jpg"), "rb")
1112
- @thumb = Paperclip::Thumbnail.new(@file,
1113
- geometry: "100x50#",
1114
- convert_options: "-strip")
1115
- end
1116
-
1117
- it "has convert_options value set" do
1118
- assert_equal "-strip", @thumb.convert_options
1119
- end
1120
-
1121
- it "creates the thumbnail when sent #make" do
1122
- dst = @thumb.make
1123
- assert_match /100x50/, `identify "#{dst.path}"`
1124
- end
1125
-
1126
- it "actually applies the convert options (removes EXIF data)" do
1127
- # Verify original has EXIF
1128
- cmd_orig = %[identify -format "%[exif:orientation]" "#{@file.path}"]
1129
- expect(`#{cmd_orig}`.chomp).to_not be_empty
1130
-
1131
- # Verify result has no EXIF
1132
- dst = @thumb.make
1133
- cmd_new = %[identify -format "%[exif:orientation]" "#{dst.path}"]
1134
- expect(`#{cmd_new}`.chomp).to be_empty
1135
- end
1136
- end
1137
-
1138
- context "error handling" do
1139
- before do
1140
- require "image_processing"
1141
- # Use a valid image so initialization (geometry detection) succeeds
1142
- @file = File.new(fixture_file("5k.png"), "rb")
1143
- end
1144
-
1145
- context "with whiny enabled (default)" do
1146
- it "raises an error when processing fails" do
1147
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50")
1148
- allow(thumb).to receive(:build_pipeline).and_raise(Paperclip::Errors::CommandNotFoundError)
1149
-
1150
- expect { thumb.make }.to raise_error(Paperclip::Errors::CommandNotFoundError)
1151
- end
1152
-
1153
- it "raises a Paperclip::Error when an underlying processing error occurs" do
1154
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50")
1155
- # Simulate a generic processing error
1156
- allow(thumb).to receive(:build_pipeline).and_raise(ImageProcessing::Error.new("Processing failed"))
1157
-
1158
- expect { thumb.make }.to raise_error(Paperclip::Error, /Processing failed/)
1159
- end
1160
- end
1161
-
1162
- context "with whiny disabled" do
1163
- it "returns the original file when processing fails" do
1164
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", whiny: false)
1165
- allow(thumb).to receive(:build_pipeline).and_raise(ImageProcessing::Error.new("Processing failed"))
1166
-
1167
- result = thumb.make
1168
- expect(result).to eq(@file)
1169
- end
1170
-
1171
- it "logs the error" do
1172
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", whiny: false)
1173
- allow(thumb).to receive(:build_pipeline).and_raise(ImageProcessing::Error.new("Processing failed"))
1174
-
1175
- expect(Paperclip).to receive(:log).with(/Processing failed/)
1176
- thumb.make
1177
- end
1178
- end
1179
- end
1180
-
1181
- context "being thumbnailed with a blank geometry string" do
1182
- before do
1183
- @thumb = Paperclip::Thumbnail.new(@file,
1184
- geometry: "",
1185
- convert_options: "-gravity center -crop 300x300+0-0")
1186
- end
1187
-
1188
- # Verify that when geometry is blank, we don't resize, but still apply other options.
1189
- it "does not resize the image via geometry but applies convert_options" do
1190
- result = @thumb.make
1191
- cmd = %[identify -format "%wx%h" "#{result.path}"]
1192
- # Original size is 434x66. The crop option (300x300) should result in 300x66.
1193
- # This confirms that no geometry-based resize happened (which would have been to 0x0 or skipped),
1194
- # but the convert_options crop was applied.
1195
- expect(`#{cmd}`.chomp).to eq("300x66")
1196
- end
1197
- end
1198
-
1199
- context "passing a custom file geometry parser" do
1200
- after do
1201
- Object.send(:remove_const, :GeoParser) if Object.const_defined?(:GeoParser)
1202
- end
1203
-
1204
- it "uses the custom parser" do
1205
- GeoParser = Class.new do
1206
- def self.from_file(_file, _backend = nil)
1207
- new
1208
- end
1209
-
1210
- def width; 100; end
1211
-
1212
- def height; 100; end
1213
-
1214
- def modifier; nil; end
1215
-
1216
- def auto_orient; end
1217
- end
1218
-
1219
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", file_geometry_parser: ::GeoParser)
1220
- expect(thumb.current_geometry).to be_a(GeoParser)
1221
- end
1222
- end
1223
-
1224
- context "passing a custom geometry string parser" do
1225
- after do
1226
- Object.send(:remove_const, :GeoParser) if Object.const_defined?(:GeoParser)
1227
- end
1228
-
1229
- it "uses the custom parser" do
1230
- GeoParser = Class.new do
1231
- def self.parse(_s)
1232
- new
1233
- end
1234
-
1235
- def to_s
1236
- "151x167"
1237
- end
1238
-
1239
- def width; 151; end
1240
-
1241
- def height; 167; end
1242
-
1243
- def modifier; nil; end
1244
- end
1245
-
1246
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", string_geometry_parser: ::GeoParser)
1247
- expect(thumb.target_geometry).to be_a(GeoParser)
1248
- end
1249
- end
1250
- end
1251
-
1252
- context "A multipage PDF" do
1253
- before do
1254
- @file = File.new(fixture_file("twopage.pdf"), "rb")
1255
- end
1256
-
1257
- after { @file.close }
1258
-
1259
- it "starts with two pages with dimensions 612x792" do
1260
- cmd = %[identify -format "%wx%h" "#{@file.path}"]
1261
- assert_equal "612x792" * 2, `#{cmd}`.chomp
1262
- end
1263
-
1264
- context "being thumbnailed at 100x100 with cropping" do
1265
- before do
1266
- @thumb = Paperclip::Thumbnail.new(@file, geometry: "100x100#", format: :png)
1267
- end
1268
-
1269
- it "reports its correct current and target geometries" do
1270
- assert_equal "100x100#", @thumb.target_geometry.to_s
1271
- assert_equal "612x792", @thumb.current_geometry.to_s
1272
- end
1273
-
1274
- it "reports its correct format" do
1275
- assert_equal :png, @thumb.format
1276
- end
1277
-
1278
- it "creates the thumbnail when sent #make" do
1279
- dst = @thumb.make
1280
- assert_match /100x100/, `identify "#{dst.path}"`
1281
- end
1282
- end
1283
- end
1284
-
1285
- context "An animated gif" do
1286
- before do
1287
- @file = File.new(fixture_file("animated.gif"), "rb")
1288
- end
1289
-
1290
- after { @file.close }
1291
-
1292
- it "starts with 12 frames with size 100x100" do
1293
- cmd = %[identify -format "%wx%h" "#{@file.path}"]
1294
- assert_equal "100x100" * 12, `#{cmd}`.chomp
1295
- end
1296
-
1297
- context "with static output" do
1298
- before do
1299
- @thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", format: :jpg)
1300
- end
1301
-
1302
- it "creates the single frame thumbnail when sent #make" do
1303
- dst = @thumb.make
1304
- cmd = %[identify -format "%wx%h" "#{dst.path}"]
1305
- assert_equal "50x50", `#{cmd}`.chomp
1306
- end
1307
- end
1308
-
1309
- context "with animated output format" do
1310
- before do
1311
- @thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", format: :gif)
1312
- end
1313
-
1314
- it "creates the 12 frames thumbnail when sent #make" do
1315
- dst = @thumb.make
1316
- cmd = %[identify -format "%wx%h," "#{dst.path}"]
1317
- frames = `#{cmd}`.chomp.split(",")
1318
- assert_equal 12, frames.size
1319
- assert_frame_dimensions (45..50), frames
1320
- end
1321
- end
1322
-
1323
- context "with omitted output format" do
1324
- before do
1325
- @thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50")
1326
- end
1327
-
1328
- it "creates the 12 frames thumbnail when sent #make" do
1329
- dst = @thumb.make
1330
- cmd = %[identify -format "%wx%h," "#{dst.path}"]
1331
- frames = `#{cmd}`.chomp.split(",")
1332
- # image_processing might not preserve animation by default if not explicitly told to
1333
- # or if the format is not explicitly set to gif.
1334
- # But here we are testing default behavior.
1335
- # If it fails with 1 frame, it means it collapsed it.
1336
- # The original implementation preserved it.
1337
- # We might need to force loader options for animated gifs if we want to preserve layers.
1338
- # But image_processing/mini_magick should handle it if we don't flatten.
1339
-
1340
- # If this fails, we might need to adjust the test expectation or implementation.
1341
- # For now, let's assume we want to preserve behavior.
1342
- assert_equal 12, frames.size
1343
- assert_frame_dimensions (45..50), frames
1344
- end
1345
- end
1346
-
1347
- context "with unidentified source format" do
1348
- before do
1349
- @unidentified_file = File.new(fixture_file("animated.unknown"), "rb")
1350
- @thumb = Paperclip::Thumbnail.new(@file, geometry: "60x60")
1351
- end
1352
-
1353
- it "creates the 12 frames thumbnail when sent #make" do
1354
- dst = @thumb.make
1355
- cmd = %[identify -format "%wx%h," "#{dst.path}"]
1356
- frames = `#{cmd}`.chomp.split(",")
1357
- assert_equal 12, frames.size
1358
- assert_frame_dimensions (55..60), frames
1359
- end
1360
- end
1361
-
1362
- context "with no source format" do
1363
- before do
1364
- @unidentified_file = File.new(fixture_file("animated"), "rb")
1365
- @thumb = Paperclip::Thumbnail.new(@file, geometry: "70x70")
1366
- end
1367
-
1368
- it "creates the 12 frames thumbnail when sent #make" do
1369
- dst = @thumb.make
1370
- cmd = %[identify -format "%wx%h," "#{dst.path}"]
1371
- frames = `#{cmd}`.chomp.split(",")
1372
- assert_equal 12, frames.size
1373
- assert_frame_dimensions (60..70), frames
1374
- end
1375
- end
1376
-
1377
- context "with animated option set to false" do
1378
- before do
1379
- @thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", animated: false)
1380
- end
1381
-
1382
- it "outputs the gif format" do
1383
- dst = @thumb.make
1384
- cmd = %[identify "#{dst.path}"]
1385
- assert_match /GIF/, `#{cmd}`.chomp
1386
- end
1387
-
1388
- it "creates the single frame thumbnail when sent #make" do
1389
- dst = @thumb.make
1390
- cmd = %[identify -format "%wx%h" "#{dst.path}"]
1391
- # The output might be multiple frames if image_processing doesn't collapse them
1392
- # But we expect single frame here.
1393
- # If it fails, we might need to adjust the implementation to force single frame.
1394
- # For now, let's check if it starts with 50x50.
1395
- output = `#{cmd}`.chomp
1396
- # If multiple frames, it will be 50x5050x50...
1397
- # We want exactly 50x50
1398
- assert_equal "50x50", output
1399
- end
1400
- end
1401
-
1402
- context "with a specified frame_index" do
1403
- before do
1404
- @thumb = Paperclip::Thumbnail.new(
1405
- @file,
1406
- geometry: "50x50",
1407
- frame_index: 5,
1408
- format: :jpg,
1409
- )
1410
- end
1411
-
1412
- it "creates the thumbnail from the frame index when sent #make" do
1413
- @thumb.make
1414
- assert_equal 5, @thumb.frame_index
1415
- end
1416
- end
1417
-
1418
- context "with vips backend" do
1419
- before do
1420
- require "vips"
1421
- rescue LoadError
1422
- skip "libvips not installed"
1423
- end
1424
-
1425
- it "preserves animation when output is GIF" do
1426
- processor = Paperclip::Thumbnail.new(@file, { geometry: "50x50", format: :gif, backend: :vips })
1427
- dst = processor.make
1428
- cmd = %[identify -format "%wx%h," "#{dst.path}"]
1429
- frames = `#{cmd}`.chomp.split(",")
1430
- expect(frames.size).to eq(12)
1431
- end
1432
-
1433
- it "collapses animation when animated: false is set" do
1434
- processor = Paperclip::Thumbnail.new(@file,
1435
- { geometry: "50x50", format: :gif, animated: false, backend: :vips })
1436
- dst = processor.make
1437
- cmd = %[identify -format "%wx%h," "#{dst.path}"]
1438
- frames = `#{cmd}`.chomp.split(",").reject(&:empty?)
1439
- expect(frames.size).to eq(1)
1440
- expect(frames.first).to eq("50x50")
1441
- end
1442
- end
1443
- end
1444
-
1445
- context "with a really long file name" do
1446
- before do
1447
- tempfile = Tempfile.new("f")
1448
- tempfile_additional_chars = tempfile.path.split("/")[-1].length + 15
1449
- image_file = File.new(fixture_file("5k.png"), "rb")
1450
- @file = Tempfile.new("f" * (255 - tempfile_additional_chars))
1451
- @file.write(image_file.read)
1452
- @file.rewind
1453
- end
1454
-
1455
- it "does not throw Errno::ENAMETOOLONG" do
1456
- thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", format: :gif)
1457
- expect { thumb.make }.to_not raise_error
1458
- end
1459
- end
1460
- end