mini_magick 3.8.0 → 4.0.0.rc

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