mini_magick 3.7.0 → 4.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,405 +1,634 @@
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_accessor :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 (BLOB)
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
- MiniMagick::Utilities.windows? ? path_for_windows_quote_space(@path) : @path
34
+ create(ext) { |file| IO.copy_stream(stream, file) }
15
35
  end
16
36
 
17
- def path=(path)
18
- @path = path
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
63
+ end
64
+
65
+ image.path.replace output_path
66
+ end
19
67
  end
20
68
 
21
- # Class Methods
22
- # -------------
23
- class << self
24
- # This is the primary loading method used by all of the other class methods.
25
- #
26
- # Use this to pass in a stream object. Must respond to Object#read(size) or be a binary string object (BLOBBBB)
27
- #
28
- # As a change from the old API, please try and use IOStream objects. They are much, much better and more efficient!
29
- #
30
- # Probably easier to use the #open method if you want to open a file or a URL.
31
- #
32
- # @param stream [IOStream, String] Some kind of stream object that needs to be read or is a binary String blob!
33
- # @param ext [String] A manual extension to use for reading the file. Not required, but if you are having issues, give this a try.
34
- # @return [Image]
35
- def read(stream, ext = nil)
36
- if stream.is_a?(String)
37
- stream = StringIO.new(stream)
38
- elsif stream.is_a?(StringIO)
39
- # Do nothing, we want a StringIO-object
40
- elsif stream.respond_to? :path
41
- if File.respond_to?(:binread)
42
- stream = StringIO.new File.binread(stream.path.to_s)
43
- else
44
- stream = StringIO.new File.open(stream.path.to_s,"rb") { |f| f.read }
45
- 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
+ # @param options [Hash] Specify options for the open method
80
+ # @return [MiniMagick::Image] The loaded image
81
+ #
82
+ def self.open(path_or_url, ext = nil, options = {})
83
+ options, ext = ext, nil if ext.is_a?(Hash)
84
+
85
+ # Don't use Kernel#open, but reuse its logic
86
+ openable =
87
+ if path_or_url.respond_to?(:open)
88
+ path_or_url
89
+ elsif path_or_url.respond_to?(:to_str) &&
90
+ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ path_or_url &&
91
+ (uri = URI.parse(path_or_url)).respond_to?(:open)
92
+ uri
93
+ else
94
+ options = { binmode: true }.merge(options)
95
+ Pathname(path_or_url)
46
96
  end
47
97
 
48
- create(ext) do |f|
49
- while chunk = stream.read(8192)
50
- f.write(chunk)
51
- end
52
- end
98
+ if openable.is_a?(URI::Generic)
99
+ ext ||= File.extname(openable.path)
100
+ else
101
+ ext ||= File.extname(openable.to_s)
53
102
  end
103
+ ext.sub!(/:.*/, '') # hack for filenames or URLs that include a colon
54
104
 
55
- # @deprecated Please use Image.read instead!
56
- def from_blob(blob, ext = nil)
57
- warn "Warning: MiniMagick::Image.from_blob method is deprecated. Instead, please use Image.read"
58
- create(ext) { |f| f.write(blob) }
105
+ if openable.is_a?(URI::Generic)
106
+ openable.open(options) { |file| read(file, ext) }
107
+ else
108
+ openable.open(**options) { |file| read(file, ext) }
59
109
  end
110
+ end
111
+
112
+ ##
113
+ # Used to create a new Image object data-copy. Not used to "paint" or
114
+ # that kind of thing.
115
+ #
116
+ # Takes an extension in a block and can be used to build a new Image
117
+ # object. Used by both {.open} and {.read} to create a new object. Ensures
118
+ # we have a good tempfile.
119
+ #
120
+ # @param ext [String] Specify the extension you want to read it as
121
+ # @param validate [Boolean] If false, skips validation of the created
122
+ # image. Defaults to true.
123
+ # @yield [Tempfile] You can #write bits to this object to create the new
124
+ # Image
125
+ # @return [MiniMagick::Image] The created image
126
+ #
127
+ def self.create(ext = nil, validate = MiniMagick.validate_on_create, &block)
128
+ tempfile = MiniMagick::Utilities.tempfile(ext.to_s.downcase, &block)
60
129
 
61
- # Creates an image object from a binary string blob which contains raw pixel data (i.e. no header data).
62
- #
63
- # === Returns
64
- #
65
- # * [Image] The loaded image.
66
- #
67
- # === Parameters
68
- #
69
- # * [blob] <tt>String</tt> -- Binary string blob containing raw pixel data.
70
- # * [columns] <tt>Integer</tt> -- Number of columns.
71
- # * [rows] <tt>Integer</tt> -- Number of rows.
72
- # * [depth] <tt>Integer</tt> -- Bit depth of the encoded pixel data.
73
- # * [map] <tt>String</tt> -- A code for the mapping of the pixel data. Example: 'gray' or 'rgb'.
74
- # * [format] <tt>String</tt> -- The file extension of the image format to be used when creating the image object. 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", validate = 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
130
+ new(tempfile.path, tempfile).tap do |image|
131
+ image.validate! if validate
87
132
  end
133
+ end
88
134
 
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 highwater
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
- self.read(f, ext)
107
- end
135
+ ##
136
+ # @private
137
+ # @!macro [attach] attribute
138
+ # @!attribute [r] $1
139
+ #
140
+ def self.attribute(name, key = name.to_s)
141
+ define_method(name) do |*args|
142
+ if args.any? && name != :resolution
143
+ mogrify { |b| b.send(name, *args) }
108
144
  else
109
- ext ||= File.extname(file_or_url)
110
- File.open(file_or_url, "rb") do |f|
111
- self.read(f, ext)
112
- end
145
+ @info[key, *args]
113
146
  end
114
147
  end
148
+ end
115
149
 
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)
120
- end
150
+ ##
151
+ # @return [String] The location of the current working file
152
+ #
153
+ attr_reader :path
154
+ ##
155
+ # @return [Tempfile] The underlying temporary file
156
+ #
157
+ attr_reader :tempfile
121
158
 
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 = true, &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 = self.new(tempfile.path, tempfile)
139
-
140
- if validate and !image.valid?
141
- raise MiniMagick::Invalid
142
- end
143
- return image
144
- ensure
145
- tempfile.close if tempfile
146
- end
147
- end
159
+ ##
160
+ # Create a new {MiniMagick::Image} object.
161
+ #
162
+ # _DANGER_: The file location passed in here is the *working copy*. That
163
+ # is, it gets *modified*. You can either copy it yourself or use {.open}
164
+ # which creates a temporary file for you and protects your original.
165
+ #
166
+ # @param input_path [String, Pathname] The location of an image file
167
+ # @yield [MiniMagick::Tool::Mogrify] If block is given, {#combine_options}
168
+ # is called.
169
+ #
170
+ def initialize(input_path, tempfile = nil, &block)
171
+ @path = input_path.to_s
172
+ @tempfile = tempfile
173
+ @info = MiniMagick::Image::Info.new(@path)
174
+
175
+ combine_options(&block) if block
176
+ end
177
+
178
+ def ==(other)
179
+ self.class == other.class && signature == other.signature
180
+ end
181
+ alias eql? ==
182
+
183
+ def hash
184
+ signature.hash
148
185
  end
149
186
 
150
- # Create a new MiniMagick::Image object
187
+ ##
188
+ # Returns raw image data.
151
189
  #
152
- # _DANGER_: The file location passed in here is the *working copy*. That is, it gets *modified*.
153
- # you can either copy it yourself or use the MiniMagick::Image.open(path) method which creates a
154
- # temporary file for you and protects your original!
190
+ # @return [String] Binary string
155
191
  #
156
- # @param input_path [String] The location of an image file
157
- # @todo Allow this to accept a block that can pass off to Image#combine_options
158
- def initialize(input_path, tempfile = nil)
159
- @path = input_path
160
- @tempfile = tempfile # ensures that the tempfile will stick around until this image is garbage collected.
192
+ def to_blob
193
+ File.binread(path)
161
194
  end
162
195
 
196
+ ##
163
197
  # Checks to make sure that MiniMagick can read the file and understand it.
164
198
  #
165
- # This uses the 'identify' command line utility to check the file. If you are having
166
- # issues with this, then please work directly with the 'identify' command and see if you
167
- # can figure out what the issue is.
199
+ # This uses the 'identify' command line utility to check the file. If you
200
+ # are having issues with this, then please work directly with the
201
+ # 'identify' command and see if you can figure out what the issue is.
168
202
  #
169
203
  # @return [Boolean]
204
+ #
170
205
  def valid?
171
- run_command("identify", path)
206
+ validate!
172
207
  true
173
208
  rescue MiniMagick::Invalid
174
209
  false
175
210
  end
176
211
 
177
- # A rather low-level way to interact with the "identify" command. No nice API here, just
178
- # the crazy stuff you find in ImageMagick. See the examples listed!
212
+ ##
213
+ # Runs `identify` on the current image, and raises an error if it doesn't
214
+ # pass.
215
+ #
216
+ # @raise [MiniMagick::Invalid]
217
+ #
218
+ def validate!
219
+ identify
220
+ rescue MiniMagick::Error => error
221
+ raise MiniMagick::Invalid, error.message
222
+ end
223
+
224
+ ##
225
+ # Returns the image format (e.g. "JPEG", "GIF").
226
+ #
227
+ # @return [String]
228
+ #
229
+ attribute :type, "format"
230
+ ##
231
+ # @return [String]
232
+ #
233
+ attribute :mime_type
234
+ ##
235
+ # @return [Integer]
236
+ #
237
+ attribute :width
238
+ ##
239
+ # @return [Integer]
240
+ #
241
+ attribute :height
242
+ ##
243
+ # @return [Array<Integer>]
244
+ #
245
+ attribute :dimensions
246
+ ##
247
+ # Returns the file size of the image (in bytes).
248
+ #
249
+ # @return [Integer]
250
+ #
251
+ attribute :size
252
+ ##
253
+ # Returns the file size in a human readable format.
254
+ #
255
+ # @return [String]
256
+ #
257
+ attribute :human_size
258
+ ##
259
+ # @return [String]
260
+ #
261
+ attribute :colorspace
262
+ ##
263
+ # @return [Hash]
264
+ #
265
+ attribute :exif
266
+ ##
267
+ # Returns the resolution of the photo. You can optionally specify the
268
+ # units measurement.
269
+ #
270
+ # @example
271
+ # image.resolution("PixelsPerInch") #=> [250, 250]
272
+ # @see http://www.imagemagick.org/script/command-line-options.php#units
273
+ # @return [Array<Integer>]
274
+ #
275
+ attribute :resolution
276
+ ##
277
+ # Returns the message digest of this image as a SHA-256, hexidecimal
278
+ # encoded string. This signature uniquely identifies the image and is
279
+ # convenient for determining if an image has been modified or whether two
280
+ # images are identical.
281
+ #
282
+ # @example
283
+ # image.signature #=> "60a7848c4ca6e36b8e2c5dea632ecdc29e9637791d2c59ebf7a54c0c6a74ef7e"
284
+ # @see http://www.imagemagick.org/api/signature.php
285
+ # @return [String]
286
+ #
287
+ attribute :signature
288
+ ##
289
+ # Returns the information from `identify -verbose` in a Hash format, for
290
+ # ImageMagick.
291
+ #
292
+ # @return [Hash]
293
+ attribute :data
294
+ ##
295
+ # Returns the information from `identify -verbose` in a Hash format, for
296
+ # GraphicsMagick.
297
+ #
298
+ # @return [Hash]
299
+ attribute :details
300
+
301
+ ##
302
+ # Use this method if you want to access raw Identify's format API.
179
303
  #
180
304
  # @example
181
- # image["format"] #=> "TIFF"
182
- # image["height"] #=> 41 (pixels)
183
- # image["width"] #=> 50 (pixels)
184
- # image["colorspace"] #=> "DirectClassRGB"
185
- # image["dimensions"] #=> [50, 41]
186
- # image["size"] #=> 2050 (bits)
187
- # image["original_at"] #=> 2005-02-23 23:17:24 +0000 (Read from Exif data)
188
- # image["EXIF:ExifVersion"] #=> "0220" (Can read anything from Exif)
189
- #
190
- # @param format [String] A format for the "identify" command
191
- # @see For reference see http://www.imagemagick.org/script/command-line-options.php#format
192
- # @return [String, Numeric, Array, Time, Object] Depends on the method called! Defaults to String for unknown commands
305
+ # image["%w %h"] #=> "250 450"
306
+ # image["%r"] #=> "DirectClass sRGB"
307
+ #
308
+ # @param value [String]
309
+ # @see http://www.imagemagick.org/script/escape.php
310
+ # @return [String]
311
+ #
193
312
  def [](value)
194
- # Why do I go to the trouble of putting in newlines? Because otherwise animated gifs screw everything up
195
- case value.to_s
196
- when "colorspace"
197
- run_command("identify", "-format", '%r\n', path).split("\n")[0].strip
198
- when "format"
199
- run_command("identify", "-format", '%m\n', path).split("\n")[0]
200
- when "height"
201
- run_command("identify", "-format", '%h\n', path).split("\n")[0].to_i
202
- when "width"
203
- run_command("identify", "-format", '%w\n', path).split("\n")[0].to_i
204
- when "dimensions"
205
- run_command("identify", "-format", MiniMagick::Utilities.windows? ? '"%w %h\n"' : '%w %h\n', path).split("\n")[0].split.map{|v|v.to_i}
206
- when "size"
207
- File.size(path) # Do this because calling identify -format "%b" on an animated gif fails!
208
- when "original_at"
209
- # Get the EXIF original capture as a Time object
210
- Time.local(*self["EXIF:DateTimeOriginal"].split(/:|\s+/)) rescue nil
211
- when /^EXIF\:/i
212
- result = run_command('identify', '-format', "%[#{value}]", path).chomp
213
- if result.include?(",")
214
- read_character_data(result)
215
- else
216
- result
217
- end
218
- else
219
- run_command('identify', '-format', value, path).split("\n")[0]
313
+ @info[value.to_s]
314
+ end
315
+ alias info []
316
+
317
+ ##
318
+ # Returns layers of the image. For example, JPEGs are 1-layered, but
319
+ # formats like PSDs, GIFs and PDFs can have multiple layers/frames/pages.
320
+ #
321
+ # @example
322
+ # image = MiniMagick::Image.new("document.pdf")
323
+ # image.pages.each_with_index do |page, idx|
324
+ # page.write("page#{idx}.pdf")
325
+ # end
326
+ # @return [Array<MiniMagick::Image>]
327
+ #
328
+ def layers
329
+ layers_count = identify.lines.count
330
+ layers_count.times.map do |idx|
331
+ MiniMagick::Image.new("#{path}[#{idx}]")
220
332
  end
221
333
  end
334
+ alias pages layers
335
+ alias frames layers
222
336
 
223
- # Sends raw commands to imagemagick's `mogrify` command. The image path is automatically appended to the command.
337
+ ##
338
+ # Returns a matrix of pixels from the image. The matrix is constructed as
339
+ # an array (1) of arrays (2) of arrays (3) of unsigned integers:
340
+ #
341
+ # 1) one for each row of pixels
342
+ # 2) one for each column of pixels
343
+ # 3) three elements in the range 0-255, one for each of the RGB color channels
344
+ #
345
+ # @example
346
+ # img = MiniMagick::Image.open 'image.jpg'
347
+ # pixels = img.get_pixels
348
+ # pixels[3][2][1] # the green channel value from the 4th-row, 3rd-column pixel
349
+ #
350
+ # It can also be called after applying transformations:
224
351
  #
225
- # Remember, we are always acting on this instance of the Image when messing with this.
352
+ # @example
353
+ # img = MiniMagick::Image.open 'image.jpg'
354
+ # img.crop '20x30+10+5'
355
+ # img.colorspace 'Gray'
356
+ # pixels = img.get_pixels
357
+ #
358
+ # In this example, all pixels in pix should now have equal R, G, and B values.
359
+ #
360
+ # @return [Array] Matrix of each color of each pixel
361
+ def get_pixels
362
+ convert = MiniMagick::Tool::Convert.new
363
+ convert << path
364
+ convert.depth(8)
365
+ convert << "RGB:-"
366
+
367
+ # Do not use `convert.call` here. We need the whole binary (unstripped) output here.
368
+ shell = MiniMagick::Shell.new
369
+ output, * = shell.run(convert.command)
370
+
371
+ pixels_array = output.unpack("C*")
372
+ pixels = pixels_array.each_slice(3).each_slice(width).to_a
373
+
374
+ # deallocate large intermediary objects
375
+ output.clear
376
+ pixels_array.clear
377
+
378
+ pixels
379
+ end
380
+
381
+ ##
382
+ # This is used to create image from pixels. This might be required if you
383
+ # create pixels for some image processing reasons and you want to form
384
+ # image from those pixels.
226
385
  #
227
- # @return [String] Whatever the result from the command line is. May not be terribly useful.
228
- def <<(*args)
229
- run_command("mogrify", *args << path)
386
+ # *DANGER*: This operation can be very expensive. Please try to use with
387
+ # caution.
388
+ #
389
+ # @example
390
+ # # It is given in readme.md file
391
+ ##
392
+ def self.get_image_from_pixels(pixels, dimension, map, depth, mime_type)
393
+ pixels = pixels.flatten
394
+ blob = pixels.pack('C*')
395
+ import_pixels(blob, *dimension, depth, map, mime_type)
230
396
  end
231
397
 
232
- # This is used to change the format of the image. That is, from "tiff to jpg" or something like that.
233
- # Once you run it, the instance is pointing to a new file with a new extension!
398
+ ##
399
+ # This is used to change the format of the image. That is, from "tiff to
400
+ # jpg" or something like that. Once you run it, the instance is pointing to
401
+ # a new file with a new extension!
234
402
  #
235
- # *DANGER*: This renames the file that the instance is pointing to. So, if you manually opened the
236
- # file with Image.new(file_path)... then that file is DELETED! If you used Image.open(file) then
237
- # you are ok. The original file will still be there. But, any changes to it might not be...
403
+ # *DANGER*: This renames the file that the instance is pointing to. So, if
404
+ # you manually opened the file with Image.new(file_path)... Then that file
405
+ # is DELETED! If you used Image.open(file) then you are OK. The original
406
+ # file will still be there. But, any changes to it might not be...
238
407
  #
239
- # Formatting an animation into a non-animated type will result in ImageMagick creating multiple
240
- # pages (starting with 0). You can choose which page you want to manipulate. We default to the
241
- # first page.
408
+ # Formatting an animation into a non-animated type will result in
409
+ # ImageMagick creating multiple pages (starting with 0). You can choose
410
+ # which page you want to manipulate. We default to the first page.
242
411
  #
243
412
  # If you would like to convert between animated formats, pass nil as your
244
413
  # page and ImageMagick will copy all of the pages.
245
414
  #
246
- # @param format [String] The target format... like 'jpg', 'gif', 'tiff', etc.
247
- # @param page [Integer] If this is an animated gif, say which 'page' you want
248
- # with an integer. Default 0 will convert only the first page; 'nil' will
249
- # convert all pages.
250
- # @return [nil]
251
- def format(format, page = 0)
252
- c = CommandBuilder.new('mogrify', '-format', format)
253
- yield c if block_given?
254
- if page
255
- c << "#{path}[#{page}]"
415
+ # @param format [String] The target format... Like 'jpg', 'gif', 'tiff' etc.
416
+ # @param page [Integer] If this is an animated gif, say which 'page' you
417
+ # want with an integer. Default 0 will convert only the first page; 'nil'
418
+ # will convert all pages.
419
+ # @param read_opts [Hash] Any read options to be passed to ImageMagick
420
+ # for example: image.format('jpg', page, {density: '300'})
421
+ # @yield [MiniMagick::Tool::Convert] It optionally yields the command,
422
+ # if you want to add something.
423
+ # @return [self]
424
+ #
425
+ def format(format, page = 0, read_opts={})
426
+ if @tempfile
427
+ new_tempfile = MiniMagick::Utilities.tempfile(".#{format}")
428
+ new_path = new_tempfile.path
429
+ else
430
+ new_path = Pathname(path).sub_ext(".#{format}").to_s
431
+ end
432
+
433
+ input_path = path.dup
434
+ input_path << "[#{page}]" if page && !layer?
435
+
436
+ MiniMagick::Tool::Convert.new do |convert|
437
+ read_opts.each do |opt, val|
438
+ convert.send(opt.to_s, val)
439
+ end
440
+ convert << input_path
441
+ yield convert if block_given?
442
+ convert << new_path
443
+ end
444
+
445
+ if @tempfile
446
+ destroy!
447
+ @tempfile = new_tempfile
256
448
  else
257
- c << path
449
+ File.delete(path) unless path == new_path || layer?
258
450
  end
259
- run(c)
260
451
 
261
- old_path = path
262
- self.path = path.sub(/(\.\w*)?$/, ".#{format}")
263
- File.delete(old_path) if old_path != path
452
+ path.replace new_path
453
+ @info.clear
454
+
455
+ self
456
+ end
457
+
458
+ ##
459
+ # You can use multiple commands together using this method. Very easy to
460
+ # use!
461
+ #
462
+ # @example
463
+ # image.combine_options do |c|
464
+ # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
465
+ # c.thumbnail "300x500>"
466
+ # c.background "blue"
467
+ # end
468
+ #
469
+ # @yield [MiniMagick::Tool::Mogrify]
470
+ # @see http://www.imagemagick.org/script/mogrify.php
471
+ # @return [self]
472
+ #
473
+ def combine_options(&block)
474
+ mogrify(&block)
475
+ end
264
476
 
265
- unless File.exists?(path)
266
- raise MiniMagick::Error, "Unable to format to #{format}"
477
+ ##
478
+ # If an unknown method is called then it is sent through the mogrify
479
+ # program.
480
+ #
481
+ # @see http://www.imagemagick.org/script/mogrify.php
482
+ # @return [self]
483
+ #
484
+ def method_missing(name, *args)
485
+ mogrify do |builder|
486
+ builder.send(name, *args)
267
487
  end
268
488
  end
269
489
 
270
- # Collapse images with sequences to the first frame (ie. animated gifs) and
271
- # preserve quality
272
- def collapse!
273
- run_command("mogrify", "-quality", "100", "#{path}[0]")
490
+ def respond_to_missing?(method_name, include_private = false)
491
+ MiniMagick::Tool::Mogrify.option_methods.include?(method_name.to_s)
274
492
  end
275
493
 
276
- # Writes the temporary file out to either a file location (by passing in a String) or by
277
- # passing in a Stream that you can #write(chunk) to repeatedly
494
+ ##
495
+ # Writes the temporary file out to either a file location (by passing in a
496
+ # String) or by passing in a Stream that you can #write(chunk) to
497
+ # repeatedly
498
+ #
499
+ # @param output_to [String, Pathname, #read] Some kind of stream object
500
+ # that needs to be read or a file path as a String
278
501
  #
279
- # @param output_to [IOStream, String] Some kind of stream object that needs to be read or a file path as a String
280
- # @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.
281
- # Writes the temporary image that we are using for processing to the output path
282
502
  def write(output_to)
283
- if output_to.kind_of?(String) || !output_to.respond_to?(:write)
284
- FileUtils.copy_file path, output_to
285
- run_command "identify", MiniMagick::Utilities.windows? ? path_for_windows_quote_space(output_to.to_s) : output_to.to_s # Verify that we have a good image
286
- else # stream
287
- File.open(path, "rb") do |f|
288
- f.binmode
289
- while chunk = f.read(8192)
290
- output_to.write(chunk)
503
+ case output_to
504
+ when String, Pathname
505
+ if layer?
506
+ MiniMagick::Tool::Convert.new do |builder|
507
+ builder << path
508
+ builder << output_to
291
509
  end
510
+ else
511
+ FileUtils.copy_file path, output_to unless path == output_to.to_s
292
512
  end
293
- output_to
513
+ else
514
+ IO.copy_stream File.open(path, "rb"), output_to
294
515
  end
295
516
  end
296
517
 
297
- # Gives you raw image data back
298
- # @return [String] binary string
299
- def to_blob
300
- f = File.new path
301
- f.binmode
302
- f.read
303
- ensure
304
- f.close if f
518
+ ##
519
+ # @example
520
+ # first_image = MiniMagick::Image.open "first.jpg"
521
+ # second_image = MiniMagick::Image.open "second.jpg"
522
+ # result = first_image.composite(second_image) do |c|
523
+ # c.compose "Over" # OverCompositeOp
524
+ # c.geometry "+20+20" # copy second_image onto first_image from (20, 20)
525
+ # end
526
+ # result.write "output.jpg"
527
+ #
528
+ # @see http://www.imagemagick.org/script/composite.php
529
+ #
530
+ def composite(other_image, output_extension = type.downcase, mask = nil)
531
+ output_tempfile = MiniMagick::Utilities.tempfile(".#{output_extension}")
532
+
533
+ MiniMagick::Tool::Composite.new do |composite|
534
+ yield composite if block_given?
535
+ composite << other_image.path
536
+ composite << path
537
+ composite << mask.path if mask
538
+ composite << output_tempfile.path
539
+ end
540
+
541
+ Image.new(output_tempfile.path, output_tempfile)
305
542
  end
306
543
 
307
- def mime_type
308
- format = self[:format]
309
- "image/" + format.to_s.downcase
544
+ ##
545
+ # Collapse images with sequences to the first frame (i.e. animated gifs) and
546
+ # preserve quality.
547
+ #
548
+ # @param frame [Integer] The frame to which to collapse to, defaults to `0`.
549
+ # @return [self]
550
+ #
551
+ def collapse!(frame = 0)
552
+ mogrify(frame) { |builder| builder.quality(100) }
310
553
  end
311
554
 
312
- # If an unknown method is called then it is sent through the mogrify program
313
- # Look here to find all the commands (http://www.imagemagick.org/script/mogrify.php)
314
- def method_missing(symbol, *args)
315
- combine_options do |c|
316
- c.send(symbol, *args)
555
+ ##
556
+ # Destroys the tempfile (created by {.open}) if it exists.
557
+ #
558
+ def destroy!
559
+ if @tempfile
560
+ FileUtils.rm_f @tempfile.path.sub(/mpc$/, "cache") if @tempfile.path.end_with?(".mpc")
561
+ @tempfile.unlink
317
562
  end
318
563
  end
319
564
 
320
- # You can use multiple commands together using this method. Very easy to use!
565
+ ##
566
+ # Runs `identify` on itself. Accepts an optional block for adding more
567
+ # options to `identify`.
321
568
  #
322
569
  # @example
323
- # image.combine_options do |c|
324
- # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
325
- # c.thumbnail "300x500>"
326
- # c.background background
327
- # end
570
+ # image = MiniMagick::Image.open("image.jpg")
571
+ # image.identify do |b|
572
+ # b.verbose
573
+ # end # runs `identify -verbose image.jpg`
574
+ # @return [String] Output from `identify`
575
+ # @yield [MiniMagick::Tool::Identify]
328
576
  #
329
- # @yieldparam command [CommandBuilder]
330
- def combine_options(tool = "mogrify", &block)
331
- c = CommandBuilder.new(tool)
332
-
333
- c << path if tool.to_s == "convert"
334
- block.call(c)
335
- c << path
336
- run(c)
337
- end
338
-
339
- def composite(other_image, output_extension = 'jpg', &block)
340
- begin
341
- second_tempfile = Tempfile.new(output_extension)
342
- second_tempfile.binmode
343
- ensure
344
- second_tempfile.close
577
+ def identify
578
+ MiniMagick::Tool::Identify.new do |builder|
579
+ yield builder if block_given?
580
+ builder << path
345
581
  end
346
-
347
- command = CommandBuilder.new("composite")
348
- block.call(command) if block
349
- command.push(other_image.path)
350
- command.push(self.path)
351
- command.push(second_tempfile.path)
352
-
353
- run(command)
354
- return Image.new(second_tempfile.path, second_tempfile)
355
582
  end
356
583
 
357
- def run_command(command, *args)
358
- if command == 'identify'
359
- args.unshift '-ping' # -ping "efficiently determine image characteristics."
360
- args.unshift '-quiet' if MiniMagick.mogrify? # graphicsmagick has no -quiet option.
584
+ # @private
585
+ def run_command(tool_name, *args)
586
+ MiniMagick::Tool.const_get(tool_name.capitalize).new do |builder|
587
+ args.each do |arg|
588
+ builder << arg
589
+ end
361
590
  end
362
-
363
- run(CommandBuilder.new(command, *args))
364
591
  end
365
592
 
366
- def run(command_builder)
367
- command = command_builder.command
593
+ def mogrify(page = nil)
594
+ MiniMagick::Tool::MogrifyRestricted.new do |builder|
595
+ yield builder if block_given?
596
+ builder << (page ? "#{path}[#{page}]" : path)
597
+ end
368
598
 
369
- sub = Subexec.run(command, :timeout => MiniMagick.timeout)
599
+ @info.clear
370
600
 
371
- if sub.exitstatus != 0
372
- # Clean up after ourselves in case of an error
373
- destroy!
601
+ self
602
+ end
374
603
 
375
- # Raise the appropriate error
376
- if sub.output =~ /no decode delegate/i || sub.output =~ /did not return an image/i
377
- raise Invalid, sub.output
378
- else
379
- # TODO: should we do something different if the command times out ...?
380
- # its definitely better for logging.. otherwise we dont really know
381
- raise Error, "Command (#{command.inspect.gsub("\\", "")}) failed: #{{:status_code => sub.exitstatus, :output => sub.output}.inspect}"
382
- end
383
- else
384
- sub.output
385
- end
604
+ def layer?
605
+ path =~ /\[\d+\]$/
386
606
  end
387
607
 
388
- def destroy!
389
- return if @tempfile.nil?
390
- File.unlink(path) if File.exists?(path)
391
- @tempfile = nil
608
+ ##
609
+ # Compares if image width
610
+ # is greater than height
611
+ # ============
612
+ # | |
613
+ # | |
614
+ # ============
615
+ # @return [Boolean]
616
+ def landscape?
617
+ width > height
392
618
  end
393
619
 
394
- private
395
- # Sometimes we get back a list of character values
396
- def read_character_data(list_of_characters)
397
- chars = list_of_characters.gsub(" ", "").split(",")
398
- result = ""
399
- chars.each do |val|
400
- result << ("%c" % val.to_i)
401
- end
402
- result
403
- end
620
+ ##
621
+ # Compares if image height
622
+ # is greater than width
623
+ # ======
624
+ # | |
625
+ # | |
626
+ # | |
627
+ # | |
628
+ # ======
629
+ # @return [Boolean]
630
+ def portrait?
631
+ height > width
632
+ end
404
633
  end
405
634
  end