mini_magick 3.7.0 → 4.11.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,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