mini_magick 3.7.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -3
  3. data/lib/mini_gmagick.rb +2 -1
  4. data/lib/mini_magick/configuration.rb +136 -0
  5. data/lib/mini_magick/image/info.rb +122 -0
  6. data/lib/mini_magick/image.rb +414 -312
  7. data/lib/mini_magick/logger.rb +40 -0
  8. data/lib/mini_magick/shell.rb +48 -0
  9. data/lib/mini_magick/tool/animate.rb +14 -0
  10. data/lib/mini_magick/tool/compare.rb +14 -0
  11. data/lib/mini_magick/tool/composite.rb +14 -0
  12. data/lib/mini_magick/tool/conjure.rb +14 -0
  13. data/lib/mini_magick/tool/convert.rb +14 -0
  14. data/lib/mini_magick/tool/display.rb +14 -0
  15. data/lib/mini_magick/tool/identify.rb +14 -0
  16. data/lib/mini_magick/tool/import.rb +14 -0
  17. data/lib/mini_magick/tool/mogrify.rb +14 -0
  18. data/lib/mini_magick/tool/montage.rb +14 -0
  19. data/lib/mini_magick/tool/stream.rb +14 -0
  20. data/lib/mini_magick/tool.rb +250 -0
  21. data/lib/mini_magick/utilities.rb +25 -21
  22. data/lib/mini_magick/version.rb +15 -1
  23. data/lib/mini_magick.rb +43 -70
  24. data/spec/fixtures/animation.gif +0 -0
  25. data/spec/fixtures/default.jpg +0 -0
  26. data/spec/fixtures/exif.jpg +0 -0
  27. data/spec/fixtures/image.psd +0 -0
  28. data/spec/fixtures/not_an_image.rb +1 -0
  29. data/spec/lib/mini_magick/configuration_spec.rb +66 -0
  30. data/spec/lib/mini_magick/image_spec.rb +460 -0
  31. data/spec/lib/mini_magick/shell_spec.rb +66 -0
  32. data/spec/lib/mini_magick/tool_spec.rb +107 -0
  33. data/spec/lib/mini_magick/utilities_spec.rb +17 -0
  34. data/spec/lib/mini_magick_spec.rb +39 -0
  35. data/spec/spec_helper.rb +21 -0
  36. data/spec/support/helpers.rb +37 -0
  37. metadata +62 -59
  38. data/lib/mini_magick/command_builder.rb +0 -104
  39. data/lib/mini_magick/errors.rb +0 -4
@@ -1,405 +1,507 @@
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
-
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
12
12
 
13
- def path
14
- MiniMagick::Utilities.windows? ? path_for_windows_quote_space(@path) : @path
15
- 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
16
33
 
17
- def path=(path)
18
- @path = path
34
+ create(ext) { |file| IO.copy_stream(stream, file) }
19
35
  end
20
36
 
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
46
- end
47
-
48
- create(ext) do |f|
49
- while chunk = stream.read(8192)
50
- f.write(chunk)
51
- 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
52
63
  end
53
- end
54
64
 
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) }
59
- end
60
-
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
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 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
69
+ ##
70
+ # Opens a specific image file either on the local file system or at a URI.
71
+ # Use this if you don't want to overwrite the image file.
72
+ #
73
+ # Extension is either guessed from the path or you can specify it as a
74
+ # second parameter.
75
+ #
76
+ # @param path_or_url [String] Either a local file path or a URL that
77
+ # open-uri can read
78
+ # @param ext [String] Specify the extension you want to read it as
79
+ # @return [MiniMagick::Image] The loaded image
80
+ #
81
+ def self.open(path_or_url, ext = nil)
82
+ ext ||=
83
+ if path_or_url.to_s =~ URI.regexp
84
+ File.extname(URI(path_or_url).path)
108
85
  else
109
- ext ||= File.extname(file_or_url)
110
- File.open(file_or_url, "rb") do |f|
111
- self.read(f, ext)
112
- end
86
+ File.extname(path_or_url)
113
87
  end
88
+
89
+ Kernel.open(path_or_url, "rb") do |file|
90
+ read(file, ext)
114
91
  end
92
+ end
93
+
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)
115
111
 
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)
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 = 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
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
+ @info[key, *args]
147
125
  end
148
126
  end
149
127
 
150
- # Create a new MiniMagick::Image object
128
+ ##
129
+ # @return [String] The location of the current working file
151
130
  #
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!
131
+ attr_reader :path
132
+
133
+ ##
134
+ # Create a new {MiniMagick::Image} object.
135
+ #
136
+ # _DANGER_: The file location passed in here is the *working copy*. That
137
+ # is, it gets *modified*. You can either copy it yourself or use {.open}
138
+ # which creates a temporary file for you and protects your original.
155
139
  #
156
140
  # @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)
141
+ # @yield [MiniMagick::Tool::Mogrify] If block is given, {#combine_options}
142
+ # is called.
143
+ #
144
+ def initialize(input_path, tempfile = nil, &block)
159
145
  @path = input_path
160
- @tempfile = tempfile # ensures that the tempfile will stick around until this image is garbage collected.
146
+ @tempfile = tempfile
147
+ @info = MiniMagick::Image::Info.new(@path)
148
+
149
+ combine_options(&block) if block
150
+ end
151
+
152
+ def eql?(other)
153
+ self.class.equal?(other.class) &&
154
+ signature == other.signature
161
155
  end
156
+ alias == eql?
162
157
 
158
+ def hash
159
+ signature.hash
160
+ end
161
+
162
+ ##
163
+ # Returns raw image data.
164
+ #
165
+ # @return [String] Binary string
166
+ #
167
+ def to_blob
168
+ File.binread(path)
169
+ end
170
+
171
+ ##
163
172
  # Checks to make sure that MiniMagick can read the file and understand it.
164
173
  #
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.
174
+ # This uses the 'identify' command line utility to check the file. If you
175
+ # are having issues with this, then please work directly with the
176
+ # 'identify' command and see if you can figure out what the issue is.
168
177
  #
169
178
  # @return [Boolean]
179
+ #
170
180
  def valid?
171
- run_command("identify", path)
181
+ validate!
172
182
  true
173
183
  rescue MiniMagick::Invalid
174
184
  false
175
185
  end
176
186
 
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!
187
+ ##
188
+ # Runs `identify` on the current image, and raises an error if it doesn't
189
+ # pass.
190
+ #
191
+ # @raise [MiniMagick::Invalid]
192
+ #
193
+ def validate!
194
+ identify
195
+ rescue MiniMagick::Error => error
196
+ raise MiniMagick::Invalid, error.message
197
+ end
198
+
199
+ ##
200
+ # Returns the image format (e.g. "JPEG", "GIF").
201
+ #
202
+ # @return [String]
203
+ #
204
+ attribute :type, "format"
205
+ ##
206
+ # @return [String]
207
+ #
208
+ attribute :mime_type
209
+ ##
210
+ # @return [Integer]
211
+ #
212
+ attribute :width
213
+ ##
214
+ # @return [Integer]
215
+ #
216
+ attribute :height
217
+ ##
218
+ # @return [Array<Integer>]
219
+ #
220
+ attribute :dimensions
221
+ ##
222
+ # Returns the file size of the image.
223
+ #
224
+ # @return [Integer]
225
+ #
226
+ attribute :size
227
+ ##
228
+ # @return [String]
229
+ #
230
+ attribute :colorspace
231
+ ##
232
+ # @return [Hash]
233
+ #
234
+ attribute :exif
235
+ ##
236
+ # Returns the resolution of the photo. You can optionally specify the
237
+ # units measurement.
179
238
  #
180
239
  # @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
240
+ # image.resolution("PixelsPerInch") #=> [250, 250]
241
+ # @see http://www.imagemagick.org/script/command-line-options.php#units
242
+ # @return [Array<Integer>]
243
+ #
244
+ attribute :resolution
245
+ ##
246
+ # Returns the message digest of this image as a SHA-256, hexidecimal
247
+ # encoded string. This signature uniquely identifies the image and is
248
+ # convenient for determining if an image has been modified or whether two
249
+ # images are identical.
250
+ #
251
+ # @example
252
+ # image.signature #=> "60a7848c4ca6e36b8e2c5dea632ecdc29e9637791d2c59ebf7a54c0c6a74ef7e"
253
+ # @see http://www.imagemagick.org/api/signature.php
254
+ # @return [String]
255
+ #
256
+ attribute :signature
257
+
258
+ ##
259
+ # Use this method if you want to access raw Identify's format API.
260
+ #
261
+ # @example
262
+ # image["%w %h"] #=> "250 450"
263
+ # image["%r"] #=> "DirectClass sRGB"
264
+ #
265
+ # @param value [String]
266
+ # @see http://www.imagemagick.org/script/escape.php
267
+ # @return [String]
268
+ #
193
269
  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]
220
- end
270
+ @info[value.to_s]
221
271
  end
272
+ alias info []
222
273
 
223
- # Sends raw commands to imagemagick's `mogrify` command. The image path is automatically appended to the command.
274
+ ##
275
+ # Returns layers of the image. For example, JPEGs are 1-layered, but
276
+ # formats like PSDs, GIFs and PDFs can have multiple layers/frames/pages.
224
277
  #
225
- # Remember, we are always acting on this instance of the Image when messing with this.
278
+ # @example
279
+ # image = MiniMagick::Image.new("document.pdf")
280
+ # image.pages.each_with_index do |page, idx|
281
+ # page.write("page#{idx}.pdf")
282
+ # end
283
+ # @return [Array<MiniMagick::Image>]
226
284
  #
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)
285
+ def layers
286
+ layers_count = identify.lines.count
287
+ layers_count.times.map do |idx|
288
+ MiniMagick::Image.new("#{path}[#{idx}]")
289
+ end
230
290
  end
291
+ alias pages layers
292
+ alias frames layers
231
293
 
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!
294
+ ##
295
+ # This is used to change the format of the image. That is, from "tiff to
296
+ # jpg" or something like that. Once you run it, the instance is pointing to
297
+ # a new file with a new extension!
234
298
  #
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...
299
+ # *DANGER*: This renames the file that the instance is pointing to. So, if
300
+ # you manually opened the file with Image.new(file_path)... Then that file
301
+ # is DELETED! If you used Image.open(file) then you are OK. The original
302
+ # file will still be there. But, any changes to it might not be...
238
303
  #
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.
304
+ # Formatting an animation into a non-animated type will result in
305
+ # ImageMagick creating multiple pages (starting with 0). You can choose
306
+ # which page you want to manipulate. We default to the first page.
242
307
  #
243
308
  # If you would like to convert between animated formats, pass nil as your
244
309
  # page and ImageMagick will copy all of the pages.
245
310
  #
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]
311
+ # @param format [String] The target format... Like 'jpg', 'gif', 'tiff' etc.
312
+ # @param page [Integer] If this is an animated gif, say which 'page' you
313
+ # want with an integer. Default 0 will convert only the first page; 'nil'
314
+ # will convert all pages.
315
+ # @yield [MiniMagick::Tool::Convert] It optionally yields the command,
316
+ # if you want to add something.
317
+ # @return [self]
318
+ #
251
319
  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}]"
320
+ @info.clear
321
+
322
+ if @tempfile
323
+ new_tempfile = MiniMagick::Utilities.tempfile(".#{format}")
324
+ new_path = new_tempfile.path
256
325
  else
257
- c << path
326
+ new_path = path.sub(/\.\w+$/, ".#{format}")
258
327
  end
259
- run(c)
260
328
 
261
- old_path = path
262
- self.path = path.sub(/(\.\w*)?$/, ".#{format}")
263
- File.delete(old_path) if old_path != path
329
+ MiniMagick::Tool::Convert.new do |convert|
330
+ convert << (page ? "#{path}[#{page}]" : path)
331
+ yield convert if block_given?
332
+ convert << new_path
333
+ end
264
334
 
265
- unless File.exists?(path)
266
- raise MiniMagick::Error, "Unable to format to #{format}"
335
+ if @tempfile
336
+ @tempfile.unlink
337
+ @tempfile = new_tempfile
338
+ else
339
+ File.delete(path) unless path == new_path
267
340
  end
341
+
342
+ path.replace new_path
343
+
344
+ self
268
345
  end
269
346
 
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]")
347
+ ##
348
+ # You can use multiple commands together using this method. Very easy to
349
+ # use!
350
+ #
351
+ # @example
352
+ # image.combine_options do |c|
353
+ # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
354
+ # c.thumbnail "300x500>"
355
+ # c.background "blue"
356
+ # end
357
+ #
358
+ # @yield [MiniMagick::Tool::Mogrify]
359
+ # @see http://www.imagemagick.org/script/mogrify.php
360
+ # @return [self]
361
+ #
362
+ def combine_options(&block)
363
+ mogrify(&block)
274
364
  end
275
365
 
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
366
+ ##
367
+ # If an unknown method is called then it is sent through the mogrify
368
+ # program.
278
369
  #
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
- 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)
291
- end
370
+ # @see http://www.imagemagick.org/script/mogrify.php
371
+ # @return [self]
372
+ #
373
+ def method_missing(name, *args)
374
+ mogrify do |builder|
375
+ if builder.respond_to?(name)
376
+ builder.send(name, *args)
377
+ else
378
+ super
292
379
  end
293
- output_to
294
380
  end
295
381
  end
296
382
 
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
383
+ def respond_to_missing?(method_name, include_private = false)
384
+ MiniMagick::Tool::Mogrify.new.respond_to?(method_name, include_private)
305
385
  end
306
386
 
307
- def mime_type
308
- format = self[:format]
309
- "image/" + format.to_s.downcase
387
+ ##
388
+ # Writes the temporary file out to either a file location (by passing in a
389
+ # String) or by passing in a Stream that you can #write(chunk) to
390
+ # repeatedly
391
+ #
392
+ # @param output_to [String, Pathname, #read] Some kind of stream object
393
+ # that needs to be read or a file path as a String
394
+ #
395
+ def write(output_to)
396
+ case output_to
397
+ when String, Pathname
398
+ if layer?
399
+ MiniMagick::Tool::Convert.new do |builder|
400
+ builder << path
401
+ builder << output_to
402
+ end
403
+ else
404
+ FileUtils.copy_file path, output_to
405
+ end
406
+ else
407
+ IO.copy_stream File.open(path, "rb"), output_to
408
+ end
310
409
  end
311
410
 
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)
411
+ ##
412
+ # @example
413
+ # first_image = MiniMagick::Image.open "first.jpg"
414
+ # second_image = MiniMagick::Image.open "second.jpg"
415
+ # result = first_image.composite(second_image) do |c|
416
+ # c.compose "Over" # OverCompositeOp
417
+ # c.geometry "+20+20" # copy second_image onto first_image from (20, 20)
418
+ # end
419
+ # result.write "output.jpg"
420
+ #
421
+ # @see http://www.imagemagick.org/script/composite.php
422
+ #
423
+ def composite(other_image, output_extension = 'jpg', mask = nil)
424
+ output_tempfile = MiniMagick::Utilities.tempfile(".#{output_extension}")
425
+
426
+ MiniMagick::Tool::Composite.new do |composite|
427
+ yield composite if block_given?
428
+ composite << other_image.path
429
+ composite << path
430
+ composite << mask.path if mask
431
+ composite << output_tempfile.path
317
432
  end
433
+
434
+ Image.new(output_tempfile.path, output_tempfile)
318
435
  end
319
436
 
320
- # You can use multiple commands together using this method. Very easy to use!
437
+ ##
438
+ # Collapse images with sequences to the first frame (i.e. animated gifs) and
439
+ # preserve quality.
321
440
  #
322
- # @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
441
+ # @param frame [Integer] The frame to which to collapse to, defaults to `0`.
442
+ # @return [self]
328
443
  #
329
- # @yieldparam command [CommandBuilder]
330
- def combine_options(tool = "mogrify", &block)
331
- c = CommandBuilder.new(tool)
444
+ def collapse!(frame = 0)
445
+ mogrify(frame) { |builder| builder.quality(100) }
446
+ end
332
447
 
333
- c << path if tool.to_s == "convert"
334
- block.call(c)
335
- c << path
336
- run(c)
448
+ ##
449
+ # Destroys the tempfile (created by {.open}) if it exists.
450
+ #
451
+ def destroy!
452
+ @tempfile.unlink if @tempfile
337
453
  end
338
454
 
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
455
+ ##
456
+ # Runs `identify` on itself. Accepts an optional block for adding more
457
+ # options to `identify`.
458
+ #
459
+ # @example
460
+ # image = MiniMagick::Image.open("image.jpg")
461
+ # image.identify do |b|
462
+ # b.verbose
463
+ # end # runs `identify -verbose image.jpg`
464
+ # @return [String] Output from `identify`
465
+ # @yield [MiniMagick::Tool::Identify]
466
+ #
467
+ def identify
468
+ MiniMagick::Tool::Identify.new do |builder|
469
+ yield builder if block_given?
470
+ builder << path
345
471
  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
472
  end
356
473
 
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.
474
+ # @private
475
+ def run_command(tool_name, *args)
476
+ MiniMagick::Tool.const_get(tool_name.capitalize).new do |builder|
477
+ args.each do |arg|
478
+ builder << arg
479
+ end
361
480
  end
362
-
363
- run(CommandBuilder.new(command, *args))
364
481
  end
365
482
 
366
- def run(command_builder)
367
- command = command_builder.command
368
-
369
- sub = Subexec.run(command, :timeout => MiniMagick.timeout)
483
+ private
370
484
 
371
- if sub.exitstatus != 0
372
- # Clean up after ourselves in case of an error
373
- destroy!
485
+ def mogrify(page = nil)
486
+ @info.clear
374
487
 
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}"
488
+ MiniMagick::Tool::Mogrify.new do |builder|
489
+ builder.instance_eval do
490
+ def format(*)
491
+ fail NoMethodError,
492
+ "you must call #format on a MiniMagick::Image directly"
493
+ end
382
494
  end
383
- else
384
- sub.output
495
+ yield builder if block_given?
496
+ builder << (page ? "#{path}[#{page}]" : path)
385
497
  end
498
+
499
+ self
386
500
  end
387
501
 
388
- def destroy!
389
- return if @tempfile.nil?
390
- File.unlink(path) if File.exists?(path)
391
- @tempfile = nil
502
+ def layer?
503
+ path =~ /\[\d+\]$/
392
504
  end
393
505
 
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
404
506
  end
405
507
  end