mini_magick 3.8.0 → 4.0.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mini_gmagick.rb +2 -1
  3. data/lib/mini_magick/configuration.rb +136 -0
  4. data/lib/mini_magick/image/info.rb +122 -0
  5. data/lib/mini_magick/image.rb +408 -343
  6. data/lib/mini_magick/logger.rb +40 -0
  7. data/lib/mini_magick/shell.rb +48 -0
  8. data/lib/mini_magick/tool/animate.rb +14 -0
  9. data/lib/mini_magick/tool/compare.rb +14 -0
  10. data/lib/mini_magick/tool/composite.rb +14 -0
  11. data/lib/mini_magick/tool/conjure.rb +14 -0
  12. data/lib/mini_magick/tool/convert.rb +14 -0
  13. data/lib/mini_magick/tool/display.rb +14 -0
  14. data/lib/mini_magick/tool/identify.rb +14 -0
  15. data/lib/mini_magick/tool/import.rb +14 -0
  16. data/lib/mini_magick/tool/mogrify.rb +14 -0
  17. data/lib/mini_magick/tool/montage.rb +14 -0
  18. data/lib/mini_magick/tool/stream.rb +14 -0
  19. data/lib/mini_magick/tool.rb +250 -0
  20. data/lib/mini_magick/utilities.rb +25 -26
  21. data/lib/mini_magick/version.rb +15 -1
  22. data/lib/mini_magick.rb +43 -79
  23. data/spec/fixtures/animation.gif +0 -0
  24. data/spec/fixtures/default.jpg +0 -0
  25. data/spec/fixtures/exif.jpg +0 -0
  26. data/spec/fixtures/image.psd +0 -0
  27. data/spec/fixtures/not_an_image.rb +1 -0
  28. data/spec/lib/mini_magick/configuration_spec.rb +66 -0
  29. data/spec/lib/mini_magick/image_spec.rb +460 -0
  30. data/spec/lib/mini_magick/shell_spec.rb +66 -0
  31. data/spec/lib/mini_magick/tool_spec.rb +107 -0
  32. data/spec/lib/mini_magick/utilities_spec.rb +17 -0
  33. data/spec/lib/mini_magick_spec.rb +39 -0
  34. data/spec/spec_helper.rb +21 -0
  35. data/spec/support/helpers.rb +37 -0
  36. metadata +50 -52
  37. data/lib/mini_magick/command_builder.rb +0 -110
  38. data/lib/mini_magick/errors.rb +0 -4
data/lib/mini_magick.rb CHANGED
@@ -1,89 +1,53 @@
1
- require 'tempfile'
2
- require 'subexec'
3
- require 'stringio'
4
- require 'pathname'
5
- require 'shellwords'
6
- require 'mini_magick/command_builder'
7
- require 'mini_magick/errors'
1
+ require 'mini_magick/configuration'
2
+ require 'mini_magick/tool'
8
3
  require 'mini_magick/image'
9
- require 'mini_magick/utilities'
10
4
 
11
5
  module MiniMagick
12
- @validate_on_create = true
13
- @validate_on_write = true
14
6
 
15
- class << self
16
- attr_accessor :processor
17
- attr_accessor :processor_path
18
- attr_accessor :timeout
19
- attr_accessor :validate_on_create
20
- attr_accessor :validate_on_write
21
-
22
- ##
23
- # Tries to detect the current processor based if any of the processors exist.
24
- # Mogrify have precedence over gm by default.
25
- #
26
- # === Returns
27
- # * [String] The detected procesor
28
- def choose_processor
29
- self.processor = if MiniMagick::Utilities.which('mogrify')
30
- :mogrify
31
- elsif MiniMagick::Utilities.which('gm')
32
- :gm
33
- else
34
- nil
35
- end
36
- end
37
-
38
- ##
39
- # Discovers the imagemagick version based on mogrify's output.
40
- #
41
- # === Returns
42
- # * The imagemagick version
43
- def image_magick_version
44
- @@version ||= Gem::Version.create(`mogrify --version`.split(' ')[2].split('-').first)
45
- end
46
-
47
- ##
48
- # The minimum allowed imagemagick version
49
- #
50
- # === Returns
51
- # * The minimum imagemagick version
52
- def minimum_image_magick_version
53
- @@minimum_version ||= Gem::Version.create('6.6.3')
54
- end
7
+ extend MiniMagick::Configuration
8
+
9
+ ##
10
+ # You might want to execute only certain blocks of processing with a
11
+ # different CLI, because for example that CLI does that particular thing
12
+ # faster. After the block CLI resets to its previous value.
13
+ #
14
+ # @example
15
+ # MiniMagick.with_cli :graphicsmagick do
16
+ # # operations that are better done with GraphicsMagick
17
+ # end
18
+ def self.with_cli(cli)
19
+ old_cli = self.cli
20
+ self.cli = cli
21
+ yield
22
+ self.cli = old_cli
23
+ end
55
24
 
56
- ##
57
- # Checks whether the imagemagick's version is valid
58
- #
59
- # === Returns
60
- # * [Boolean]
61
- def valid_version_installed?
62
- image_magick_version >= minimum_image_magick_version
63
- end
25
+ ##
26
+ # Checks whether the CLI used is ImageMagick.
27
+ #
28
+ # @return [Boolean]
29
+ def self.imagemagick?
30
+ cli == :imagemagick
31
+ end
64
32
 
65
- ##
66
- # Picks the right processor if it isn't set and returns whether it's mogrify or not.
67
- #
68
- # === Returns
69
- # * [Boolean]
70
- def mogrify?
71
- choose_processor if processor.nil?
33
+ ##
34
+ # Checks whether the CLI used is GraphicsMagick.
35
+ #
36
+ # @return [Boolean]
37
+ def self.graphicsmagick?
38
+ cli == :graphicsmagick
39
+ end
72
40
 
73
- return processor.to_s.downcase.to_sym == :mogrify unless processor.nil?
74
- false
75
- end
41
+ ##
42
+ # Returns ImageMagick's/GraphicsMagick's version.
43
+ #
44
+ # @return [String]
45
+ def self.cli_version
46
+ output = MiniMagick::Tool::Identify.new(&:version)
47
+ output[/\d+\.\d+\.\d+(-\d+)?/]
48
+ end
76
49
 
77
- ##
78
- # Picks the right processor if it isn't set and returns whether it's graphicsmagick or not.
79
- #
80
- # === Returns
81
- # * [Boolean]
82
- def gm?
83
- choose_processor if processor.nil?
50
+ class Error < RuntimeError; end
51
+ class Invalid < StandardError; end
84
52
 
85
- return processor.to_s.downcase.to_sym == :gm unless processor.nil?
86
- false
87
- end
88
- end
89
53
  end
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ expect(__FILE__).not_to be_an_image
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe MiniMagick::Configuration do
4
+ subject { Object.new.extend(MiniMagick::Configuration) }
5
+
6
+ describe "#configure" do
7
+ it "yields self" do
8
+ expect { |b| subject.configure(&b) }
9
+ .to yield_with_args(subject)
10
+ end
11
+ end
12
+
13
+ describe "#cli" do
14
+ it "can be assigned" do
15
+ subject.cli = :imagemagick
16
+ expect(subject.cli).to eq :imagemagick
17
+ end
18
+
19
+ it "returns :imagemagick if #processor is mogrify" do
20
+ allow(subject).to receive(:processor).and_return("mogrify")
21
+ expect(subject.cli).to eq :imagemagick
22
+ end
23
+
24
+ it "returns :graphicsmagick if #processor is gm" do
25
+ allow(subject).to receive(:processor).and_return("gm")
26
+ expect(subject.cli).to eq :graphicsmagick
27
+ end
28
+
29
+ it "returns nil of #processor is nil" do
30
+ allow(subject).to receive(:processor).and_return(nil)
31
+ expect(subject.cli).to eq nil
32
+ end
33
+ end
34
+
35
+ describe "#cli=" do
36
+ it "raises an error when set to an invalid value" do
37
+ expect { subject.cli = :grapicsmagick }
38
+ .to raise_error(ArgumentError)
39
+ end
40
+ end
41
+
42
+ describe "#processor" do
43
+ it "assigns :mogrify by default" do
44
+ expect(subject.processor).to eq "mogrify"
45
+ end
46
+
47
+ it "assigns :gm if ImageMagick is not available" do
48
+ allow(MiniMagick::Utilities).to receive(:which).with("mogrify").and_return(nil)
49
+ allow(MiniMagick::Utilities).to receive(:which).with("gm").and_return(true)
50
+ expect(subject.processor).to eq "gm"
51
+ end
52
+
53
+ it "returns nil if neither ImageMagick nor GraphicsMagick are available" do
54
+ allow(MiniMagick::Utilities).to receive(:which).with("mogrify").and_return(nil)
55
+ allow(MiniMagick::Utilities).to receive(:which).with("gm").and_return(nil)
56
+ expect(subject.processor).to eq nil
57
+ end
58
+ end
59
+
60
+ describe "#processor=" do
61
+ it "raises an error when set to an invalid value" do
62
+ expect { subject.processor = "mogrfy" }
63
+ .to raise_error(ArgumentError)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,460 @@
1
+ require "spec_helper"
2
+ require "pathname"
3
+ require "tempfile"
4
+ require "fileutils"
5
+ require "stringio"
6
+
7
+ ["ImageMagick", "GraphicsMagick"].each do |cli|
8
+ RSpec.context "With #{cli}", cli: cli.downcase.to_sym do
9
+ describe MiniMagick::Image do
10
+ subject { described_class.open(image_path) }
11
+
12
+ describe ".read" do
13
+ it "reads image from String" do
14
+ string = File.binread(image_path)
15
+ image = described_class.read(string)
16
+ expect(image).to be_valid
17
+ end
18
+
19
+ it "reads image from StringIO" do
20
+ stringio = StringIO.new(File.binread(image_path))
21
+ image = described_class.read(stringio)
22
+ expect(image).to be_valid
23
+ end
24
+
25
+ it "reads image from tempfile" do
26
+ tempfile = Tempfile.open('magick')
27
+ FileUtils.cp image_path, tempfile.path
28
+ image = described_class.read(tempfile)
29
+ expect(image).to be_valid
30
+ end
31
+ end
32
+
33
+ describe ".import_pixels" do
34
+ let(:dimensions) { [325, 200] }
35
+ let(:depth) { 16 } # 16 bits (2 bytes) per pixel
36
+ let(:map) { 'gray' }
37
+ let(:pixels) { Array.new(dimensions.inject(:*)) { |i| i } }
38
+ let(:blob) { pixels.pack('S*') } # unsigned short, native byte order
39
+
40
+ it "can import pixels with default format" do
41
+ image = described_class.import_pixels(blob, *dimensions, depth, map)
42
+
43
+ expect(image).to be_valid
44
+ expect(image.type).to eq 'PNG'
45
+ expect(image.dimensions).to eq dimensions
46
+ end
47
+
48
+ it "can import pixels with custom format" do
49
+ image = described_class.import_pixels(blob, *dimensions, depth, map, 'jpeg')
50
+
51
+ expect(image).to be_valid
52
+ expect(image.type).to eq 'JPEG'
53
+ expect(image.dimensions).to eq dimensions
54
+ end
55
+ end
56
+
57
+ describe ".open" do
58
+ it "makes a copy of the image" do
59
+ image = described_class.open(image_path)
60
+ expect(image.path).not_to eq image_path
61
+ expect(image).to be_valid
62
+ end
63
+
64
+ it "accepts a Pathname" do
65
+ image = described_class.open(Pathname(image_path))
66
+ expect(image).to be_valid
67
+ end
68
+
69
+ it "loads a remote image" do
70
+ begin
71
+ image = described_class.open(image_url)
72
+ expect(image).to be_valid
73
+ rescue SocketError
74
+ end
75
+ end
76
+
77
+ it "validates the image" do
78
+ expect { described_class.open(image_path(:not)) }
79
+ .to raise_error(MiniMagick::Invalid)
80
+ end
81
+ end
82
+
83
+ describe ".create" do
84
+ def create(path = image_path)
85
+ described_class.create do |f|
86
+ f.write(File.binread(path))
87
+ end
88
+ end
89
+
90
+ it "creates an image" do
91
+ image = create
92
+ expect(File.exists?(image.path)).to eq true
93
+ end
94
+
95
+ it "validates the image if validation is set" do
96
+ allow(MiniMagick).to receive(:validate_on_create).and_return(true)
97
+ expect { create(image_path(:not)) }
98
+ .to raise_error(MiniMagick::Invalid)
99
+ end
100
+
101
+ it "doesn't validate image if validation is disabled" do
102
+ allow(MiniMagick).to receive(:validate_on_create).and_return(false)
103
+ expect { create(image_path(:not)) }
104
+ .not_to raise_error
105
+ end
106
+ end
107
+
108
+ describe "#initialize" do
109
+ it "initializes a new image" do
110
+ image = described_class.new(image_path)
111
+ expect(image).to be_valid
112
+ end
113
+
114
+ it "accepts a block which it passes on to #combine_options" do
115
+ image = described_class.new(subject.path) do |b|
116
+ b.resize "100x100!"
117
+ end
118
+ expect(image.dimensions).to eq [100, 100]
119
+ end
120
+ end
121
+
122
+ describe "equivalence" do
123
+ subject(:image) { described_class.new(image_path) }
124
+ let(:same_image) { described_class.new(image_path) }
125
+ let(:other_image) { described_class.new(image_path(:exif)) }
126
+
127
+ it "is #== and #eql? to itself" do
128
+ expect(image).to eq(image)
129
+ expect(image).to eql(image)
130
+ end
131
+
132
+ it "is #== and #eql? to an instance of the same image" do
133
+ expect(image).to eq(same_image)
134
+ expect(image).to eql(same_image)
135
+ end
136
+
137
+ it "is not #== nor #eql? to an instance of a different image" do
138
+ expect(image).not_to eq(other_image)
139
+ expect(image).not_to eql(other_image)
140
+ end
141
+
142
+ it "generates the same hash code for an instance of the same image" do
143
+ expect(image.hash).to eq(same_image.hash)
144
+ end
145
+
146
+ it "generates different same hash codes for a different image" do
147
+ expect(image.hash).not_to eq(other_image.hash)
148
+ end
149
+ end
150
+
151
+ describe "#format" do
152
+ subject { described_class.open(image_path(:jpg)) }
153
+
154
+ it "changes the format of the photo" do
155
+ expect { subject.format("png") }
156
+ .to change { subject.type }
157
+ end
158
+
159
+ it "reformats an image with a given extension" do
160
+ expect { subject.format('png') }
161
+ .to change { File.extname(subject.path) }.to ".png"
162
+ end
163
+
164
+ it "creates the file with new extension" do
165
+ subject.format('png')
166
+ expect(File.exist?(subject.path)).to eq true
167
+ end
168
+
169
+ it "accepts a block of additional commands" do
170
+ expect {
171
+ subject.format("png") do |b|
172
+ b.resize("100x100!")
173
+ end
174
+ }.to change { subject.dimensions }.to [100, 100]
175
+ end
176
+
177
+ it "works without an extension" do
178
+ subject = described_class.open(image_path(:without_extension))
179
+ expect { subject.format("png") }
180
+ .to change { File.extname(subject.path) }.from("").to(".png")
181
+ end
182
+
183
+ it "deletes the previous tempfile" do
184
+ old_path = subject.path.dup
185
+ subject.format('png')
186
+ expect(File.exist?(old_path)).to eq false
187
+ end
188
+
189
+ it "doesn't delete itself when formatted to the same format" do
190
+ subject.format(subject.type.downcase)
191
+ expect(File.exists?(subject.path)).to eq true
192
+ end
193
+
194
+ it "reformats multi-image formats to multiple images" do
195
+ subject = described_class.open(image_path(:animation))
196
+ subject.format('jpg', nil)
197
+ expect(Dir[subject.path.sub('.', '*.')]).not_to be_empty
198
+ end
199
+
200
+ it "reformats multi-image formats to a single image" do
201
+ subject = described_class.open(image_path(:animation))
202
+ subject.format('jpg')
203
+ expect(subject).to be_valid
204
+ end
205
+
206
+ it "returns self" do
207
+ expect(subject.format('png')).to eq subject
208
+ end
209
+ end
210
+
211
+ describe "#write" do
212
+ it "writes the image" do
213
+ output_path = random_path("test output")
214
+ subject.write(output_path)
215
+ expect(described_class.new(output_path)).to be_valid
216
+ end
217
+
218
+ it "writes an image with stream" do
219
+ output_stream = StringIO.new
220
+ subject.write(output_stream)
221
+ expect(described_class.read(output_stream.string)).to be_valid
222
+ end
223
+
224
+ it "writes layers" do
225
+ output_path = random_path(["", ".#{subject.type.downcase}"])
226
+ subject = described_class.new(image_path(:gif))
227
+ subject.frames.first.write(output_path)
228
+ expect(described_class.new(output_path)).to be_valid
229
+ end
230
+
231
+ it "accepts a Pathname" do
232
+ output_path = Pathname(random_path)
233
+ subject.write(output_path)
234
+ expect(described_class.new(output_path.to_s)).to be_valid
235
+ end
236
+ end
237
+
238
+ describe "#valid?" do
239
+ it "returns true when image is valid" do
240
+ image = described_class.new(image_path)
241
+ expect(image).to be_valid
242
+ end
243
+
244
+ it "returns false when image is not valid" do
245
+ image = described_class.new(image_path(:not))
246
+ expect(image).not_to be_valid
247
+ end
248
+ end
249
+
250
+ describe "#[]" do
251
+ it "inspects image meta info" do
252
+ expect(subject[:width]).to be_a(Fixnum)
253
+ expect(subject[:height]).to be_a(Fixnum)
254
+ expect(subject[:dimensions]).to all(be_a(Fixnum))
255
+ expect(subject[:colorspace]).to be_a(String)
256
+ expect(subject[:format]).to match(/[A-Z]/)
257
+ expect(subject[:signature]).to match(/[[:alnum:]]{64}/)
258
+ end
259
+
260
+ it "supports string keys" do
261
+ expect(subject["width"]).to be_a(Fixnum)
262
+ expect(subject["height"]).to be_a(Fixnum)
263
+ expect(subject["dimensions"]).to all(be_a(Fixnum))
264
+ expect(subject["colorspace"]).to be_a(String)
265
+ expect(subject["format"]).to match(/[A-Z]/)
266
+ expect(subject['signature']).to match(/[[:alnum:]]{64}/)
267
+ end
268
+
269
+ it "reads exif" do
270
+ subject = described_class.new(image_path(:exif))
271
+ gps_latitude = subject.exif["GPSLatitude"].split(/\s*,\s*/)
272
+ gps_longitude = subject.exif["GPSLongitude"].split(/\s*,\s*/)
273
+
274
+ expect(subject["EXIF:ColorSpace"]).to eq "1"
275
+ expect(gps_latitude.size).to eq 3
276
+ expect(gps_longitude.size).to eq 3
277
+ end
278
+
279
+ it "passes unknown values directly to -format" do
280
+ expect(subject["%w %h"].split.map(&:to_i)).to eq [subject[:width], subject[:height]]
281
+ end
282
+ end
283
+
284
+ it "has attributes" do
285
+ expect(subject.type).to match(/^[A-Z]+$/)
286
+ expect(subject.mime_type).to match(/^image\/[a-z]+$/)
287
+ expect(subject.width).to be_a(Fixnum).and be_nonzero
288
+ expect(subject.height).to be_a(Fixnum).and be_nonzero
289
+ expect(subject.dimensions).to all(be_a(Fixnum))
290
+ expect(subject.size).to be_a(Fixnum).and be_nonzero
291
+ expect(subject.colorspace).to be_a(String)
292
+ expect(subject.resolution).to all(be_a(Fixnum))
293
+ expect(subject.signature).to match(/[[:alnum:]]{64}/)
294
+ end
295
+
296
+ describe "#exif" do
297
+ subject { described_class.new(image_path(:exif)) }
298
+
299
+ it "returns a hash of EXIF data" do
300
+ expect(subject.exif["DateTimeOriginal"]).to be_a(String)
301
+ end
302
+
303
+ it "decodes the ExifVersion" do
304
+ expect(subject.exif["ExifVersion"]).to eq("0221")
305
+ end
306
+ end
307
+
308
+ describe "#resolution" do
309
+ it "accepts units", skip_cli: :graphicsmagick do
310
+ expect(subject.resolution("PixelsPerCentimeter"))
311
+ .not_to eq subject.resolution("PixelsPerInch")
312
+ end
313
+ end
314
+
315
+ describe "#mime_type" do
316
+ it "returns the correct mime type" do
317
+ jpg = described_class.new(image_path(:jpg))
318
+ expect(jpg.mime_type).to eq 'image/jpeg'
319
+ end
320
+ end
321
+
322
+ describe "#layers" do
323
+ it "returns a list of images" do
324
+ expect(subject.layers).to all(be_a(MiniMagick::Image))
325
+ expect(subject.layers.first).to be_valid
326
+ end
327
+
328
+ it "returns multiple images for GIFs, PDFs and PSDs" do
329
+ gif = described_class.new(image_path(:gif))
330
+ psd = described_class.new(image_path(:psd))
331
+
332
+ expect(gif.frames.count).to be > 1
333
+ expect(psd.layers.count).to be > 1 unless MiniMagick.graphicsmagick?
334
+ end
335
+
336
+ it "returns one image for other formats" do
337
+ jpg = described_class.new(image_path(:jpg))
338
+
339
+ expect(jpg.layers.count).to eq 1
340
+ end
341
+ end
342
+
343
+ describe "missing methods" do
344
+ context "for a known method" do
345
+ it "is executed by #method_missing" do
346
+ expect { subject.resize '20x30!' }
347
+ .to change { subject.dimensions }.to [20, 30]
348
+ end
349
+
350
+ it "returns self" do
351
+ expect(subject.resize('20x30!')).to eq subject
352
+ end
353
+
354
+ it "can be responed to" do
355
+ expect(subject.respond_to?(:resize)).to eq true
356
+ end
357
+ end
358
+
359
+ context "for an unknown method" do
360
+ it "fails with a NoMethodError" do
361
+ expect { subject.foo }
362
+ .to raise_error(NoMethodError, /MiniMagick::Image/)
363
+ end
364
+
365
+ it "cannot be responded to" do
366
+ expect(subject.respond_to?(:foo)).to eq false
367
+ end
368
+ end
369
+ end
370
+
371
+ describe "#combine_options" do
372
+ it "chains multiple options and executes them in one command" do
373
+ expect {
374
+ subject.combine_options { |c| c.resize '20x30!' }
375
+ }.to change { subject.dimensions }.to [20, 30]
376
+ end
377
+
378
+ it "doesn't allow calling of #format" do
379
+ expect { subject.combine_options { |c| c.format("png") } }
380
+ .to raise_error(NoMethodError)
381
+ end
382
+
383
+ it "returns self" do
384
+ expect(subject.combine_options {}).to eq subject
385
+ end
386
+ end
387
+
388
+ describe "#composite" do
389
+ let(:other_image) { described_class.open(image_path) }
390
+ let(:mask) { described_class.open(image_path) }
391
+
392
+ it "creates a composite of two images" do
393
+ image = subject.composite(other_image)
394
+ expect(image).to be_valid
395
+ end
396
+
397
+ it "creates a composite of two images with mask" do
398
+ image = subject.composite(other_image, 'jpg', mask)
399
+ expect(image).to be_valid
400
+ end
401
+
402
+ it "yields an optional block" do
403
+ expect { |b| subject.composite(other_image, &b) }
404
+ .to yield_with_args(an_instance_of(MiniMagick::Tool::Composite))
405
+ end
406
+
407
+ it "makes the composited image with the provided extension" do
408
+ result = subject.composite(other_image, 'png')
409
+ expect(result.path).to end_with ".png"
410
+
411
+ result = subject.composite(other_image)
412
+ expect(result.path).to end_with ".jpg"
413
+ end
414
+ end
415
+
416
+ describe "#collapse!" do
417
+ subject { described_class.open(image_path(:animation)) }
418
+
419
+ it "collapses the image to one frame" do
420
+ subject.collapse!
421
+ expect(subject.identify.lines.count).to eq 1
422
+ end
423
+
424
+ it "keeps the extension" do
425
+ expect { subject.collapse! }
426
+ .not_to change { subject.type }
427
+ end
428
+
429
+ it "clears the info" do
430
+ expect { subject.collapse! }
431
+ .to change { subject.size }
432
+ end
433
+
434
+ it "returns self" do
435
+ expect(subject.collapse!).to eq subject
436
+ end
437
+ end
438
+
439
+ describe "#identify" do
440
+ it "returns the output of identify" do
441
+ expect(subject.identify).to match(subject.type)
442
+ end
443
+
444
+ it "yields an optional block" do
445
+ output = subject.identify do |b|
446
+ b.verbose
447
+ end
448
+ expect(output).to match("Format:")
449
+ end
450
+ end
451
+
452
+ describe "#run_command" do
453
+ it "runs the given command" do
454
+ output = subject.run_command("identify", "-format", "%w", subject.path)
455
+ expect(output).to eq subject.width.to_s
456
+ end
457
+ end
458
+ end
459
+ end
460
+ end