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