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