mini_magick 3.8.1 → 4.0.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 (41) 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 +384 -334
  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 +23 -50
  21. data/lib/mini_magick/version.rb +4 -4
  22. data/lib/mini_magick.rb +43 -65
  23. metadata +21 -81
  24. data/lib/mini_magick/command_builder.rb +0 -94
  25. data/lib/mini_magick/errors.rb +0 -4
  26. data/spec/files/actually_a_gif.jpg +0 -0
  27. data/spec/files/animation.gif +0 -0
  28. data/spec/files/composited.jpg +0 -0
  29. data/spec/files/erroneous.jpg +0 -0
  30. data/spec/files/layers.psd +0 -0
  31. data/spec/files/leaves (spaced).tiff +0 -0
  32. data/spec/files/not_an_image.php +0 -1
  33. data/spec/files/png.png +0 -0
  34. data/spec/files/simple-minus.gif +0 -0
  35. data/spec/files/simple.gif +0 -0
  36. data/spec/files/trogdor.jpg +0 -0
  37. data/spec/files/trogdor_capitalized.JPG +0 -0
  38. data/spec/lib/mini_magick/command_builder_spec.rb +0 -153
  39. data/spec/lib/mini_magick/image_spec.rb +0 -499
  40. data/spec/lib/mini_magick_spec.rb +0 -63
  41. data/spec/spec_helper.rb +0 -29
@@ -1,188 +1,178 @@
1
1
  require 'tempfile'
2
- require 'subexec'
3
2
  require 'stringio'
4
3
  require 'pathname'
4
+ require 'uri'
5
+ require 'open-uri'
6
+
7
+ require 'mini_magick/image/info'
8
+ require 'mini_magick/utilities'
5
9
 
6
10
  module MiniMagick
7
11
  class Image
8
- # @return [String] The location of the current working file
9
- attr_writer :path
10
12
 
11
- def path
12
- run_queue if @command_queued
13
- MiniMagick::Utilities.path(@path)
14
- end
15
-
16
- # Class Methods
17
- # -------------
18
- class << self
19
- # This is the primary loading method used by all of the other class
20
- # methods.
21
- #
22
- # Use this to pass in a stream object. Must respond to Object#read(size)
23
- # or be a binary string object (BLOBBBB)
24
- #
25
- # As a change from the old API, please try and use IOStream objects. They
26
- # are much, much better and more efficient!
27
- #
28
- # Probably easier to use the #open method if you want to open a file or a
29
- # URL.
30
- #
31
- # @param stream [IOStream, String] Some kind of stream object that needs
32
- # to be read or is a binary String blob!
33
- # @param ext [String] A manual extension to use for reading the file. Not
34
- # required, but if you are having issues, give this a try.
35
- # @return [Image]
36
- def read(stream, ext = nil)
37
- if stream.is_a?(String)
38
- stream = StringIO.new(stream)
39
- elsif stream.is_a?(StringIO)
40
- # Do nothing, we want a StringIO-object
41
- elsif stream.respond_to? :path
42
- if File.respond_to?(:binread)
43
- stream = StringIO.new File.binread(stream.path.to_s)
44
- else
45
- stream = StringIO.new File.open(stream.path.to_s, 'rb') { |f| f.read }
46
- end
47
- end
48
-
49
- create(ext) do |f|
50
- while chunk = stream.read(8192)
51
- f.write(chunk)
52
- end
53
- end
13
+ ##
14
+ # This is the primary loading method used by all of the other class
15
+ # methods.
16
+ #
17
+ # Use this to pass in a stream object. Must respond to #read(size) or be a
18
+ # binary string object (BLOBBBB)
19
+ #
20
+ # Probably easier to use the {.open} method if you want to open a file or a
21
+ # URL.
22
+ #
23
+ # @param stream [#read, String] Some kind of stream object that needs
24
+ # to be read or is a binary String blob
25
+ # @param ext [String] A manual extension to use for reading the file. Not
26
+ # required, but if you are having issues, give this a try.
27
+ # @return [MiniMagick::Image]
28
+ #
29
+ def self.read(stream, ext = nil)
30
+ if stream.is_a?(String)
31
+ stream = StringIO.new(stream)
54
32
  end
55
33
 
56
- # @deprecated Please use Image.read instead!
57
- def from_blob(blob, ext = nil)
58
- warn 'Warning: MiniMagick::Image.from_blob method is deprecated. Instead, please use Image.read'
59
- create(ext) { |f| f.write(blob) }
60
- end
34
+ create(ext) { |file| IO.copy_stream(stream, file) }
35
+ end
61
36
 
62
- # Creates an image object from a binary string blob which contains raw
63
- # pixel data (i.e. no header data).
64
- #
65
- # @param blob [String] Binary string blob containing raw pixel data.
66
- # @param columns [Integer] Number of columns.
67
- # @param rows [Integer] Number of rows.
68
- # @param depth [Integer] Bit depth of the encoded pixel data.
69
- # @param map [String] A code for the mapping of the pixel data. Example:
70
- # 'gray' or 'rgb'.
71
- # @param format [String] The file extension of the image format to be
72
- # used when creating the image object.
73
- # Defaults to 'png'.
74
- # @return [Image] The loaded image.
75
- #
76
- def import_pixels(blob, columns, rows, depth, map, format = 'png')
77
- # Create an image object with the raw pixel data string:
78
- image = create('.dat', false) { |f| f.write(blob) }
37
+ ##
38
+ # Creates an image object from a binary string blob which contains raw
39
+ # pixel data (i.e. no header data).
40
+ #
41
+ # @param blob [String] Binary string blob containing raw pixel data.
42
+ # @param columns [Integer] Number of columns.
43
+ # @param rows [Integer] Number of rows.
44
+ # @param depth [Integer] Bit depth of the encoded pixel data.
45
+ # @param map [String] A code for the mapping of the pixel data. Example:
46
+ # 'gray' or 'rgb'.
47
+ # @param format [String] The file extension of the image format to be
48
+ # used when creating the image object.
49
+ # Defaults to 'png'.
50
+ # @return [MiniMagick::Image] The loaded image.
51
+ #
52
+ def self.import_pixels(blob, columns, rows, depth, map, format = 'png')
53
+ # Create an image object with the raw pixel data string:
54
+ create(".dat", false) { |f| f.write(blob) }.tap do |image|
55
+ output_path = image.path.sub(/\.\w+$/, ".#{format}")
79
56
  # Use ImageMagick to convert the raw data file to an image file of the
80
57
  # desired format:
81
- converted_image_path = image.path[0..-4] + format
82
- arguments = ['-size', "#{columns}x#{rows}", '-depth', "#{depth}", "#{map}:#{image.path}", "#{converted_image_path}"]
83
- # Example: convert -size 256x256 -depth 16 gray:blob.dat blob.png
84
- cmd = CommandBuilder.new('convert', *arguments)
85
- image.run(cmd)
86
- # Update the image instance with the path of the properly formatted
87
- # image, and return:
88
- image.path = converted_image_path
89
- image
58
+ MiniMagick::Tool::Convert.new do |convert|
59
+ convert.size "#{columns}x#{rows}"
60
+ convert.depth depth
61
+ convert << "#{map}:#{image.path}"
62
+ convert << output_path
63
+ end
64
+
65
+ image.path.replace output_path
90
66
  end
67
+ end
91
68
 
92
- # Opens a specific image file either on the local file system or at a URI.
93
- #
94
- # Use this if you don't want to overwrite the image file.
95
- #
96
- # Extension is either guessed from the path or you can specify it as a
97
- # second parameter.
98
- #
99
- # If you pass in what looks like a URL, we require 'open-uri' before
100
- # opening it.
101
- #
102
- # @param file_or_url [String] Either a local file path or a URL that
103
- # open-uri can read
104
- # @param ext [String] Specify the extension you want to read it as
105
- # @return [Image] The loaded image
106
- def open(file_or_url, ext = nil)
107
- file_or_url = file_or_url.to_s # Force String... Hell or high water
108
- if file_or_url.include?('://')
109
- require 'open-uri'
110
- ext ||= File.extname(URI.parse(file_or_url).path)
111
- Kernel.open(file_or_url) do |f|
112
- read(f, ext)
113
- end
69
+ ##
70
+ # Opens a specific image file either on the local file system or at a URI.
71
+ # Use this if you don't want to overwrite the image file.
72
+ #
73
+ # Extension is either guessed from the path or you can specify it as a
74
+ # second parameter.
75
+ #
76
+ # @param path_or_url [String] Either a local file path or a URL that
77
+ # open-uri can read
78
+ # @param ext [String] Specify the extension you want to read it as
79
+ # @return [MiniMagick::Image] The loaded image
80
+ #
81
+ def self.open(path_or_url, ext = nil)
82
+ ext ||=
83
+ if path_or_url.to_s =~ URI.regexp
84
+ File.extname(URI(path_or_url).path)
114
85
  else
115
- ext ||= File.extname(file_or_url)
116
- File.open(file_or_url, 'rb') do |f|
117
- read(f, ext)
118
- end
86
+ File.extname(path_or_url)
119
87
  end
88
+
89
+ Kernel.open(path_or_url, "rb") do |file|
90
+ read(file, ext)
120
91
  end
92
+ end
121
93
 
122
- # @deprecated Please use MiniMagick::Image.open(file_or_url) now
123
- def from_file(file, ext = nil)
124
- warn 'Warning: MiniMagick::Image.from_file is now deprecated. Please use Image.open'
125
- open(file, ext)
94
+ ##
95
+ # Used to create a new Image object data-copy. Not used to "paint" or
96
+ # that kind of thing.
97
+ #
98
+ # Takes an extension in a block and can be used to build a new Image
99
+ # object. Used by both {.open} and {.read} to create a new object. Ensures
100
+ # we have a good tempfile.
101
+ #
102
+ # @param ext [String] Specify the extension you want to read it as
103
+ # @param validate [Boolean] If false, skips validation of the created
104
+ # image. Defaults to true.
105
+ # @yield [Tempfile] You can #write bits to this object to create the new
106
+ # Image
107
+ # @return [MiniMagick::Image] The created image
108
+ #
109
+ def self.create(ext = nil, validate = MiniMagick.validate_on_create, &block)
110
+ tempfile = MiniMagick::Utilities.tempfile(ext.to_s.downcase, &block)
111
+
112
+ new(tempfile.path, tempfile).tap do |image|
113
+ image.validate! if validate
126
114
  end
115
+ end
127
116
 
128
- # Used to create a new Image object data-copy. Not used to "paint" or
129
- # that kind of thing.
130
- #
131
- # Takes an extension in a block and can be used to build a new Image
132
- # object. Used by both #open and #read to create a new object! Ensures we
133
- # have a good tempfile!
134
- #
135
- # @param ext [String] Specify the extension you want to read it as
136
- # @param validate [Boolean] If false, skips validation of the created
137
- # image. Defaults to true.
138
- # @yield [IOStream] You can #write bits to this object to create the new
139
- # Image
140
- # @return [Image] The created image
141
- def create(ext = nil, validate = MiniMagick.validate_on_create, &block)
142
- tempfile = Tempfile.new(['mini_magick', ext.to_s.downcase])
143
- tempfile.binmode
144
- block.call(tempfile)
145
- tempfile.close
146
-
147
- image = new(tempfile.path, tempfile)
148
-
149
- fail MiniMagick::Invalid if validate && !image.valid?
150
- return image
151
- ensure
152
- tempfile.close if tempfile
117
+ ##
118
+ # @private
119
+ # @!macro [attach] attribute
120
+ # @!attribute [r] $1
121
+ #
122
+ def self.attribute(name, key = name.to_s)
123
+ define_method(name) do |*args|
124
+ if args.any? && MiniMagick::Tool::Mogrify.instance_methods.include?(name)
125
+ mogrify { |b| b.send(name, *args) }
126
+ else
127
+ @info[key, *args]
128
+ end
153
129
  end
154
130
  end
155
131
 
156
- # Create a new MiniMagick::Image object
132
+ ##
133
+ # @return [String] The location of the current working file
134
+ #
135
+ attr_reader :path
136
+
137
+ ##
138
+ # Create a new {MiniMagick::Image} object.
157
139
  #
158
140
  # _DANGER_: The file location passed in here is the *working copy*. That
159
- # is, it gets *modified*. you can either copy it yourself or use the
160
- # MiniMagick::Image.open(path) method which creates a temporary file for
161
- # you and protects your original!
141
+ # is, it gets *modified*. You can either copy it yourself or use {.open}
142
+ # which creates a temporary file for you and protects your original.
162
143
  #
163
144
  # @param input_path [String] The location of an image file
164
- # @todo Allow this to accept a block that can pass off to
165
- # Image#combine_options
166
- def initialize(input_path, tempfile = nil)
145
+ # @yield [MiniMagick::Tool::Mogrify] If block is given, {#combine_options}
146
+ # is called.
147
+ #
148
+ def initialize(input_path, tempfile = nil, &block)
167
149
  @path = input_path
168
150
  @tempfile = tempfile
169
- @info = {}
170
- reset_queue
151
+ @info = MiniMagick::Image::Info.new(@path)
152
+
153
+ combine_options(&block) if block
171
154
  end
172
155
 
173
- def reset_queue
174
- @command_queued = false
175
- @queue = MiniMagick::CommandBuilder.new('mogrify')
176
- @info.clear
156
+ def eql?(other)
157
+ self.class.equal?(other.class) &&
158
+ signature == other.signature
177
159
  end
160
+ alias == eql?
178
161
 
179
- def run_queue
180
- return nil unless @command_queued
181
- @queue << MiniMagick::Utilities.path(@path)
182
- run(@queue)
183
- reset_queue
162
+ def hash
163
+ signature.hash
164
+ end
165
+
166
+ ##
167
+ # Returns raw image data.
168
+ #
169
+ # @return [String] Binary string
170
+ #
171
+ def to_blob
172
+ File.binread(path)
184
173
  end
185
174
 
175
+ ##
186
176
  # Checks to make sure that MiniMagick can read the file and understand it.
187
177
  #
188
178
  # This uses the 'identify' command line utility to check the file. If you
@@ -190,89 +180,122 @@ module MiniMagick
190
180
  # 'identify' command and see if you can figure out what the issue is.
191
181
  #
192
182
  # @return [Boolean]
183
+ #
193
184
  def valid?
194
- run_command('identify', path)
185
+ validate!
195
186
  true
196
187
  rescue MiniMagick::Invalid
197
188
  false
198
189
  end
199
190
 
200
- def info(key)
201
- run_queue if @command_queued
202
-
203
- @info[key]
191
+ ##
192
+ # Runs `identify` on the current image, and raises an error if it doesn't
193
+ # pass.
194
+ #
195
+ # @raise [MiniMagick::Invalid]
196
+ #
197
+ def validate!
198
+ identify
199
+ rescue MiniMagick::Error => error
200
+ raise MiniMagick::Invalid, error.message
204
201
  end
205
202
 
206
- # A rather low-level way to interact with the "identify" command. No nice
207
- # API here, just the crazy stuff you find in ImageMagick. See the examples
208
- # listed!
203
+ ##
204
+ # Returns the image format (e.g. "JPEG", "GIF").
205
+ #
206
+ # @return [String]
207
+ #
208
+ attribute :type, "format"
209
+ ##
210
+ # @return [String]
211
+ #
212
+ attribute :mime_type
213
+ ##
214
+ # @return [Integer]
215
+ #
216
+ attribute :width
217
+ ##
218
+ # @return [Integer]
219
+ #
220
+ attribute :height
221
+ ##
222
+ # @return [Array<Integer>]
223
+ #
224
+ attribute :dimensions
225
+ ##
226
+ # Returns the file size of the image.
227
+ #
228
+ # @return [Integer]
229
+ #
230
+ attribute :size
231
+ ##
232
+ # @return [String]
233
+ #
234
+ attribute :colorspace
235
+ ##
236
+ # @return [Hash]
237
+ #
238
+ attribute :exif
239
+ ##
240
+ # Returns the resolution of the photo. You can optionally specify the
241
+ # units measurement.
242
+ #
243
+ # @example
244
+ # image.resolution("PixelsPerInch") #=> [250, 250]
245
+ # @see http://www.imagemagick.org/script/command-line-options.php#units
246
+ # @return [Array<Integer>]
247
+ #
248
+ attribute :resolution
249
+ ##
250
+ # Returns the message digest of this image as a SHA-256, hexidecimal
251
+ # encoded string. This signature uniquely identifies the image and is
252
+ # convenient for determining if an image has been modified or whether two
253
+ # images are identical.
254
+ #
255
+ # @example
256
+ # image.signature #=> "60a7848c4ca6e36b8e2c5dea632ecdc29e9637791d2c59ebf7a54c0c6a74ef7e"
257
+ # @see http://www.imagemagick.org/api/signature.php
258
+ # @return [String]
259
+ #
260
+ attribute :signature
261
+
262
+ ##
263
+ # Use this method if you want to access raw Identify's format API.
209
264
  #
210
265
  # @example
211
- # image["format"] #=> "TIFF"
212
- # image["height"] #=> 41 (pixels)
213
- # image["width"] #=> 50 (pixels)
214
- # image["colorspace"] #=> "DirectClassRGB"
215
- # image["dimensions"] #=> [50, 41]
216
- # image["size"] #=> 2050 (bits)
217
- # image["original_at"] #=> 2005-02-23 23:17:24 +0000 (Read from Exif data)
218
- # image["EXIF:ExifVersion"] #=> "0220" (Can read anything from Exif)
219
- #
220
- # @param format [String] A format for the "identify" command
221
- # @see http://www.imagemagick.org/script/command-line-options.php#format
222
- # @return [String, Numeric, Array, Time, Object] Depends on the method
223
- # called! Defaults to String for unknown commands
266
+ # image["%w %h"] #=> "250 450"
267
+ # image["%r"] #=> "DirectClass sRGB"
268
+ #
269
+ # @param value [String]
270
+ # @see http://www.imagemagick.org/script/escape.php
271
+ # @return [String]
272
+ #
224
273
  def [](value)
225
- retrieved = info(value)
226
- return retrieved unless retrieved.nil?
227
-
228
- # Why do I go to the trouble of putting in newlines? Because otherwise
229
- # animated gifs screw everything up
230
- retrieved = case value.to_s
231
- when 'colorspace'
232
- run_command('identify', '-format', '%r\n', path).split("\n")[0].strip
233
- when 'format'
234
- run_command('identify', '-format', '%m\n', path).split("\n")[0]
235
- when 'dimensions', 'width', 'height'
236
- width_height = run_command(
237
- 'identify', '-format', MiniMagick::Utilities.windows? ? '"%w %h\n"' : '%w %h\n', path
238
- ).split("\n")[0].split.map { |v| v.to_i }
239
-
240
- @info[:width] = width_height[0]
241
- @info[:height] = width_height[1]
242
- @info[:dimensions] = width_height
243
- @info[value.to_sym]
244
- when 'size'
245
- File.size(path) # Do this because calling identify -format "%b" on an animated gif fails!
246
- when 'original_at'
247
- # Get the EXIF original capture as a Time object
248
- Time.local(*self['EXIF:DateTimeOriginal'].split(/:|\s+/)) rescue nil
249
- when /^EXIF\:/i
250
- result = run_command('identify', '-format', "%[#{value}]", path).chomp
251
- if result.include?(',')
252
- read_character_data(result)
253
- else
254
- result
255
- end
256
- else
257
- run_command('identify', '-format', value, path).split("\n")[0]
258
- end
259
-
260
- @info[value] = retrieved unless retrieved.nil?
261
- @info[value]
274
+ @info[value.to_s]
262
275
  end
276
+ alias info []
263
277
 
264
- # Sends raw commands to imagemagick's `mogrify` command. The image path is
265
- # automatically appended to the command.
278
+ ##
279
+ # Returns layers of the image. For example, JPEGs are 1-layered, but
280
+ # formats like PSDs, GIFs and PDFs can have multiple layers/frames/pages.
266
281
  #
267
- # Remember, we are always acting on this instance of the Image when messing
268
- # with this.
282
+ # @example
283
+ # image = MiniMagick::Image.new("document.pdf")
284
+ # image.pages.each_with_index do |page, idx|
285
+ # page.write("page#{idx}.pdf")
286
+ # end
287
+ # @return [Array<MiniMagick::Image>]
269
288
  #
270
- # @return [String] Whatever the result from the command line is. May not be
271
- # terribly useful.
272
- def <<(*args)
273
- run_command('mogrify', *args << path)
289
+ def layers
290
+ layers_count = identify.lines.count
291
+ layers_count.times.map do |idx|
292
+ MiniMagick::Image.new("#{path}[#{idx}]")
293
+ end
274
294
  end
295
+ alias pages layers
296
+ alias frames layers
275
297
 
298
+ ##
276
299
  # This is used to change the format of the image. That is, from "tiff to
277
300
  # jpg" or something like that. Once you run it, the instance is pointing to
278
301
  # a new file with a new extension!
@@ -293,169 +316,196 @@ module MiniMagick
293
316
  # @param page [Integer] If this is an animated gif, say which 'page' you
294
317
  # want with an integer. Default 0 will convert only the first page; 'nil'
295
318
  # will convert all pages.
296
- # @return [nil]
319
+ # @yield [MiniMagick::Tool::Convert] It optionally yields the command,
320
+ # if you want to add something.
321
+ # @return [self]
322
+ #
297
323
  def format(format, page = 0)
298
- run_queue if @command_queued
324
+ @info.clear
325
+
326
+ if @tempfile
327
+ new_tempfile = MiniMagick::Utilities.tempfile(".#{format}")
328
+ new_path = new_tempfile.path
329
+ else
330
+ new_path = path.sub(/\.\w+$/, ".#{format}")
331
+ end
299
332
 
300
- c = CommandBuilder.new('mogrify', '-format', format)
301
- yield c if block_given?
302
- c << (page ? "#{path}[#{page}]" : path)
303
- run(c)
333
+ MiniMagick::Tool::Convert.new do |convert|
334
+ convert << (page ? "#{path}[#{page}]" : path)
335
+ yield convert if block_given?
336
+ convert << new_path
337
+ end
338
+
339
+ if @tempfile
340
+ @tempfile.unlink
341
+ @tempfile = new_tempfile
342
+ else
343
+ File.delete(path) unless path == new_path
344
+ end
304
345
 
305
- old_path = path
346
+ path.replace new_path
306
347
 
307
- self.path = path.sub(/(\.\w*)?$/, (page ? ".#{format}" : "-0.#{format}"))
348
+ self
349
+ end
308
350
 
309
- File.delete(old_path) if old_path != path
351
+ ##
352
+ # You can use multiple commands together using this method. Very easy to
353
+ # use!
354
+ #
355
+ # @example
356
+ # image.combine_options do |c|
357
+ # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
358
+ # c.thumbnail "300x500>"
359
+ # c.background "blue"
360
+ # end
361
+ #
362
+ # @yield [MiniMagick::Tool::Mogrify]
363
+ # @see http://www.imagemagick.org/script/mogrify.php
364
+ # @return [self]
365
+ #
366
+ def combine_options(&block)
367
+ mogrify(&block)
368
+ end
310
369
 
311
- unless File.exist?(path)
312
- fail MiniMagick::Error, "Unable to format to #{format}"
370
+ ##
371
+ # If an unknown method is called then it is sent through the mogrify
372
+ # program.
373
+ #
374
+ # @see http://www.imagemagick.org/script/mogrify.php
375
+ # @return [self]
376
+ #
377
+ def method_missing(name, *args)
378
+ mogrify do |builder|
379
+ if builder.respond_to?(name)
380
+ builder.send(name, *args)
381
+ else
382
+ super
383
+ end
313
384
  end
314
385
  end
315
386
 
316
- # Collapse images with sequences to the first frame (i.e. animated gifs) and
317
- # preserve quality
318
- def collapse!
319
- run_command('mogrify', '-quality', '100', "#{path}[0]")
387
+ def respond_to_missing?(method_name, include_private = false)
388
+ MiniMagick::Tool::Mogrify.new.respond_to?(method_name, include_private)
320
389
  end
321
390
 
391
+ ##
322
392
  # Writes the temporary file out to either a file location (by passing in a
323
393
  # String) or by passing in a Stream that you can #write(chunk) to
324
394
  # repeatedly
325
395
  #
326
- # @param output_to [IOStream, String] Some kind of stream object that needs
327
- # to be read or a file path as a String
328
- # @return [IOStream, Boolean] If you pass in a file location [String] then
329
- # you get a success boolean. If its a stream, you get it back.
396
+ # @param output_to [String, Pathname, #read] Some kind of stream object
397
+ # that needs to be read or a file path as a String
398
+ #
330
399
  def write(output_to)
331
- run_queue if @command_queued
332
-
333
- if output_to.kind_of?(String) || output_to.kind_of?(Pathname) || !output_to.respond_to?(:write)
334
- FileUtils.copy_file path, output_to
335
- if MiniMagick.validate_on_write
336
- run_command(
337
- 'identify', MiniMagick::Utilities.path(output_to.to_s)
338
- ) # Verify that we have a good image
339
- end
340
- else # stream
341
- File.open(path, 'rb') do |f|
342
- f.binmode
343
- while chunk = f.read(8192)
344
- output_to.write(chunk)
400
+ case output_to
401
+ when String, Pathname
402
+ if layer?
403
+ MiniMagick::Tool::Convert.new do |builder|
404
+ builder << path
405
+ builder << output_to
345
406
  end
407
+ else
408
+ FileUtils.copy_file path, output_to
346
409
  end
347
- output_to
410
+ else
411
+ IO.copy_stream File.open(path, "rb"), output_to
348
412
  end
349
413
  end
350
414
 
351
- # Gives you raw image data back
352
- # @return [String] binary string
353
- def to_blob
354
- run_queue if @command_queued
415
+ ##
416
+ # @example
417
+ # first_image = MiniMagick::Image.open "first.jpg"
418
+ # second_image = MiniMagick::Image.open "second.jpg"
419
+ # result = first_image.composite(second_image) do |c|
420
+ # c.compose "Over" # OverCompositeOp
421
+ # c.geometry "+20+20" # copy second_image onto first_image from (20, 20)
422
+ # end
423
+ # result.write "output.jpg"
424
+ #
425
+ # @see http://www.imagemagick.org/script/composite.php
426
+ #
427
+ def composite(other_image, output_extension = 'jpg', mask = nil)
428
+ output_tempfile = MiniMagick::Utilities.tempfile(".#{output_extension}")
429
+
430
+ MiniMagick::Tool::Composite.new do |composite|
431
+ yield composite if block_given?
432
+ composite << other_image.path
433
+ composite << path
434
+ composite << mask.path if mask
435
+ composite << output_tempfile.path
436
+ end
355
437
 
356
- f = File.new path
357
- f.binmode
358
- f.read
359
- ensure
360
- f.close if f
438
+ Image.new(output_tempfile.path, output_tempfile)
361
439
  end
362
440
 
363
- def mime_type
364
- format = self[:format]
365
- 'image/' + format.to_s.downcase
441
+ ##
442
+ # Collapse images with sequences to the first frame (i.e. animated gifs) and
443
+ # preserve quality.
444
+ #
445
+ # @param frame [Integer] The frame to which to collapse to, defaults to `0`.
446
+ # @return [self]
447
+ #
448
+ def collapse!(frame = 0)
449
+ mogrify(frame) { |builder| builder.quality(100) }
366
450
  end
367
451
 
368
- # If an unknown method is called then it is sent through the mogrify
369
- # program.
452
+ ##
453
+ # Destroys the tempfile (created by {.open}) if it exists.
370
454
  #
371
- # @see http://www.imagemagick.org/script/mogrify.php
372
- def method_missing(symbol, *args)
373
- @queue.send(symbol, *args)
374
- @command_queued = true
455
+ def destroy!
456
+ @tempfile.unlink if @tempfile
375
457
  end
376
458
 
377
- # You can use multiple commands together using this method. Very easy to
378
- # use!
459
+ ##
460
+ # Runs `identify` on itself. Accepts an optional block for adding more
461
+ # options to `identify`.
379
462
  #
380
463
  # @example
381
- # image.combine_options do |c|
382
- # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
383
- # c.thumbnail "300x500>"
384
- # c.background background
385
- # end
464
+ # image = MiniMagick::Image.open("image.jpg")
465
+ # image.identify do |b|
466
+ # b.verbose
467
+ # end # runs `identify -verbose image.jpg`
468
+ # @return [String] Output from `identify`
469
+ # @yield [MiniMagick::Tool::Identify]
386
470
  #
387
- # @yieldparam command [CommandBuilder]
388
- def combine_options
389
- if block_given?
390
- yield @queue
391
- @command_queued = true
471
+ def identify
472
+ MiniMagick::Tool::Identify.new do |builder|
473
+ yield builder if block_given?
474
+ builder << path
392
475
  end
393
476
  end
394
477
 
395
- def composite(other_image, output_extension = 'jpg', mask = nil, &block)
396
- run_queue if @command_queued
397
- begin
398
- second_tempfile = Tempfile.new(output_extension)
399
- second_tempfile.binmode
400
- ensure
401
- second_tempfile.close
402
- end
403
-
404
- command = CommandBuilder.new('composite')
405
- block.call(command) if block
406
- command.push(other_image.path)
407
- command.push(path)
408
- command.push(mask.path) unless mask.nil?
409
- command.push(second_tempfile.path)
410
-
411
- run(command)
412
- Image.new(second_tempfile.path, second_tempfile)
413
- end
414
-
415
- def run_command(command, *args)
416
- run_queue if @command_queued
417
-
418
- if command == 'identify'
419
- args.unshift '-ping' # -ping "efficiently determine image characteristics."
420
- args.unshift '-quiet' if MiniMagick.mogrify? && !MiniMagick.debug # graphicsmagick has no -quiet option.
478
+ # @private
479
+ def run_command(tool_name, *args)
480
+ MiniMagick::Tool.const_get(tool_name.capitalize).new do |builder|
481
+ args.each do |arg|
482
+ builder << arg
483
+ end
421
484
  end
422
-
423
- run(CommandBuilder.new(command, *args))
424
485
  end
425
486
 
426
- def run(command_builder)
427
- command = command_builder.command
428
-
429
- sub = Subexec.run(command, :timeout => MiniMagick.timeout)
487
+ private
430
488
 
431
- if sub.exitstatus != 0
432
- # Clean up after ourselves in case of an error
433
- destroy!
489
+ def mogrify(page = nil)
490
+ @info.clear
434
491
 
435
- # Raise the appropriate error
436
- if sub.output =~ /no decode delegate/i || sub.output =~ /did not return an image/i
437
- fail Invalid, sub.output
438
- else
439
- # TODO: should we do something different if the command times out ...?
440
- # its definitely better for logging.. Otherwise we don't really know
441
- fail Error, "Command (#{command.inspect.gsub("\\", "")}) failed: #{{ :status_code => sub.exitstatus, :output => sub.output }.inspect}"
492
+ MiniMagick::Tool::Mogrify.new do |builder|
493
+ builder.instance_eval do
494
+ def format(*)
495
+ fail NoMethodError,
496
+ "you must call #format on a MiniMagick::Image directly"
497
+ end
442
498
  end
443
- else
444
- sub.output
499
+ yield builder if block_given?
500
+ builder << (page ? "#{path}[#{page}]" : path)
445
501
  end
446
- end
447
502
 
448
- def destroy!
449
- return if @tempfile.nil?
450
- File.unlink(@path) if File.exist?(@path)
451
- @tempfile = nil
503
+ self
452
504
  end
453
505
 
454
- private
455
-
456
- # Sometimes we get back a list of character values
457
- def read_character_data(string)
458
- string.scan(/\d+/).map(&:to_i).map(&:chr).join
506
+ def layer?
507
+ path =~ /\[\d+\]$/
459
508
  end
509
+
460
510
  end
461
511
  end