mini_magick 3.8.1 → 4.9.4

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 (42) hide show
  1. checksums.yaml +5 -5
  2. data/lib/mini_gmagick.rb +1 -0
  3. data/lib/mini_magick/configuration.rb +198 -0
  4. data/lib/mini_magick/image/info.rb +192 -0
  5. data/lib/mini_magick/image.rb +456 -341
  6. data/lib/mini_magick/shell.rb +81 -0
  7. data/lib/mini_magick/tool/animate.rb +14 -0
  8. data/lib/mini_magick/tool/compare.rb +14 -0
  9. data/lib/mini_magick/tool/composite.rb +14 -0
  10. data/lib/mini_magick/tool/conjure.rb +14 -0
  11. data/lib/mini_magick/tool/convert.rb +14 -0
  12. data/lib/mini_magick/tool/display.rb +14 -0
  13. data/lib/mini_magick/tool/identify.rb +14 -0
  14. data/lib/mini_magick/tool/import.rb +14 -0
  15. data/lib/mini_magick/tool/magick.rb +14 -0
  16. data/lib/mini_magick/tool/mogrify.rb +14 -0
  17. data/lib/mini_magick/tool/mogrify_restricted.rb +15 -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 +297 -0
  21. data/lib/mini_magick/utilities.rb +23 -50
  22. data/lib/mini_magick/version.rb +5 -5
  23. data/lib/mini_magick.rb +54 -65
  24. metadata +49 -51
  25. data/lib/mini_magick/command_builder.rb +0 -94
  26. data/lib/mini_magick/errors.rb +0 -4
  27. data/spec/files/actually_a_gif.jpg +0 -0
  28. data/spec/files/animation.gif +0 -0
  29. data/spec/files/composited.jpg +0 -0
  30. data/spec/files/erroneous.jpg +0 -0
  31. data/spec/files/layers.psd +0 -0
  32. data/spec/files/leaves (spaced).tiff +0 -0
  33. data/spec/files/not_an_image.php +0 -1
  34. data/spec/files/png.png +0 -0
  35. data/spec/files/simple-minus.gif +0 -0
  36. data/spec/files/simple.gif +0 -0
  37. data/spec/files/trogdor.jpg +0 -0
  38. data/spec/files/trogdor_capitalized.JPG +0 -0
  39. data/spec/lib/mini_magick/command_builder_spec.rb +0 -153
  40. data/spec/lib/mini_magick/image_spec.rb +0 -499
  41. data/spec/lib/mini_magick_spec.rb +0 -63
  42. data/spec/spec_helper.rb +0 -29
@@ -1,188 +1,184 @@
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)
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
33
+
34
+ create(ext) { |file| IO.copy_stream(stream, file) }
14
35
  end
15
36
 
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
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
47
63
  end
48
64
 
49
- create(ext) do |f|
50
- while chunk = stream.read(8192)
51
- f.write(chunk)
52
- end
53
- end
65
+ image.path.replace output_path
54
66
  end
67
+ end
55
68
 
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
69
+ ##
70
+ # Opens a specific image file either on the local file system or at a URI.
71
+ # Use this if you don't want to overwrite the image file.
72
+ #
73
+ # Extension is either guessed from the path or you can specify it as a
74
+ # second parameter.
75
+ #
76
+ # @param path_or_url [String] Either a local file path or a URL that
77
+ # open-uri can read
78
+ # @param ext [String] Specify the extension you want to read it as
79
+ # @param options [Hash] Specify options for the open method
80
+ # @return [MiniMagick::Image] The loaded image
81
+ #
82
+ def self.open(path_or_url, ext = nil, options = {})
83
+ options, ext = ext, nil if ext.is_a?(Hash)
61
84
 
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) }
79
- # Use ImageMagick to convert the raw data file to an image file of the
80
- # 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
90
- end
85
+ uri = URI(path_or_url.to_s)
91
86
 
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
114
- else
115
- ext ||= File.extname(file_or_url)
116
- File.open(file_or_url, 'rb') do |f|
117
- read(f, ext)
118
- end
119
- end
87
+ ext ||= File.extname(uri.path)
88
+ ext.sub!(/:.*/, '') # hack for filenames or URLs that include a colon
89
+
90
+ if uri.is_a?(URI::HTTP) || uri.is_a?(URI::FTP)
91
+ uri.open(options) { |file| read(file, ext) }
92
+ else
93
+ File.open(uri.to_s, "rb", options) { |file| read(file, ext) }
120
94
  end
95
+ end
121
96
 
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)
97
+ ##
98
+ # Used to create a new Image object data-copy. Not used to "paint" or
99
+ # that kind of thing.
100
+ #
101
+ # Takes an extension in a block and can be used to build a new Image
102
+ # object. Used by both {.open} and {.read} to create a new object. Ensures
103
+ # we have a good tempfile.
104
+ #
105
+ # @param ext [String] Specify the extension you want to read it as
106
+ # @param validate [Boolean] If false, skips validation of the created
107
+ # image. Defaults to true.
108
+ # @yield [Tempfile] You can #write bits to this object to create the new
109
+ # Image
110
+ # @return [MiniMagick::Image] The created image
111
+ #
112
+ def self.create(ext = nil, validate = MiniMagick.validate_on_create, &block)
113
+ tempfile = MiniMagick::Utilities.tempfile(ext.to_s.downcase, &block)
114
+
115
+ new(tempfile.path, tempfile).tap do |image|
116
+ image.validate! if validate
126
117
  end
118
+ end
127
119
 
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
120
+ ##
121
+ # @private
122
+ # @!macro [attach] attribute
123
+ # @!attribute [r] $1
124
+ #
125
+ def self.attribute(name, key = name.to_s)
126
+ define_method(name) do |*args|
127
+ if args.any? && name != :resolution
128
+ mogrify { |b| b.send(name, *args) }
129
+ else
130
+ @info[key, *args]
131
+ end
153
132
  end
154
133
  end
155
134
 
156
- # Create a new MiniMagick::Image object
135
+ ##
136
+ # @return [String] The location of the current working file
137
+ #
138
+ attr_reader :path
139
+ ##
140
+ # @return [Tempfile] The underlying temporary file
141
+ #
142
+ attr_reader :tempfile
143
+
144
+ ##
145
+ # Create a new {MiniMagick::Image} object.
157
146
  #
158
147
  # _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!
162
- #
163
- # @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)
167
- @path = input_path
148
+ # is, it gets *modified*. You can either copy it yourself or use {.open}
149
+ # which creates a temporary file for you and protects your original.
150
+ #
151
+ # @param input_path [String, Pathname] The location of an image file
152
+ # @yield [MiniMagick::Tool::Mogrify] If block is given, {#combine_options}
153
+ # is called.
154
+ #
155
+ def initialize(input_path, tempfile = nil, &block)
156
+ @path = input_path.to_s
168
157
  @tempfile = tempfile
169
- @info = {}
170
- reset_queue
158
+ @info = MiniMagick::Image::Info.new(@path)
159
+
160
+ combine_options(&block) if block
171
161
  end
172
162
 
173
- def reset_queue
174
- @command_queued = false
175
- @queue = MiniMagick::CommandBuilder.new('mogrify')
176
- @info.clear
163
+ def ==(other)
164
+ self.class == other.class && signature == other.signature
177
165
  end
166
+ alias eql? ==
178
167
 
179
- def run_queue
180
- return nil unless @command_queued
181
- @queue << MiniMagick::Utilities.path(@path)
182
- run(@queue)
183
- reset_queue
168
+ def hash
169
+ signature.hash
184
170
  end
185
171
 
172
+ ##
173
+ # Returns raw image data.
174
+ #
175
+ # @return [String] Binary string
176
+ #
177
+ def to_blob
178
+ File.binread(path)
179
+ end
180
+
181
+ ##
186
182
  # Checks to make sure that MiniMagick can read the file and understand it.
187
183
  #
188
184
  # This uses the 'identify' command line utility to check the file. If you
@@ -190,89 +186,184 @@ module MiniMagick
190
186
  # 'identify' command and see if you can figure out what the issue is.
191
187
  #
192
188
  # @return [Boolean]
189
+ #
193
190
  def valid?
194
- run_command('identify', path)
191
+ validate!
195
192
  true
196
193
  rescue MiniMagick::Invalid
197
194
  false
198
195
  end
199
196
 
200
- def info(key)
201
- run_queue if @command_queued
202
-
203
- @info[key]
197
+ ##
198
+ # Runs `identify` on the current image, and raises an error if it doesn't
199
+ # pass.
200
+ #
201
+ # @raise [MiniMagick::Invalid]
202
+ #
203
+ def validate!
204
+ identify
205
+ rescue MiniMagick::Error => error
206
+ raise MiniMagick::Invalid, error.message
204
207
  end
205
208
 
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!
209
+ ##
210
+ # Returns the image format (e.g. "JPEG", "GIF").
211
+ #
212
+ # @return [String]
213
+ #
214
+ attribute :type, "format"
215
+ ##
216
+ # @return [String]
217
+ #
218
+ attribute :mime_type
219
+ ##
220
+ # @return [Integer]
221
+ #
222
+ attribute :width
223
+ ##
224
+ # @return [Integer]
225
+ #
226
+ attribute :height
227
+ ##
228
+ # @return [Array<Integer>]
229
+ #
230
+ attribute :dimensions
231
+ ##
232
+ # Returns the file size of the image (in bytes).
233
+ #
234
+ # @return [Integer]
235
+ #
236
+ attribute :size
237
+ ##
238
+ # Returns the file size in a human readable format.
239
+ #
240
+ # @return [String]
241
+ #
242
+ attribute :human_size
243
+ ##
244
+ # @return [String]
245
+ #
246
+ attribute :colorspace
247
+ ##
248
+ # @return [Hash]
249
+ #
250
+ attribute :exif
251
+ ##
252
+ # Returns the resolution of the photo. You can optionally specify the
253
+ # units measurement.
254
+ #
255
+ # @example
256
+ # image.resolution("PixelsPerInch") #=> [250, 250]
257
+ # @see http://www.imagemagick.org/script/command-line-options.php#units
258
+ # @return [Array<Integer>]
259
+ #
260
+ attribute :resolution
261
+ ##
262
+ # Returns the message digest of this image as a SHA-256, hexidecimal
263
+ # encoded string. This signature uniquely identifies the image and is
264
+ # convenient for determining if an image has been modified or whether two
265
+ # images are identical.
266
+ #
267
+ # @example
268
+ # image.signature #=> "60a7848c4ca6e36b8e2c5dea632ecdc29e9637791d2c59ebf7a54c0c6a74ef7e"
269
+ # @see http://www.imagemagick.org/api/signature.php
270
+ # @return [String]
271
+ #
272
+ attribute :signature
273
+ ##
274
+ # Returns the information from `identify -verbose` in a Hash format, for
275
+ # ImageMagick.
276
+ #
277
+ # @return [Hash]
278
+ attribute :data
279
+ ##
280
+ # Returns the information from `identify -verbose` in a Hash format, for
281
+ # GraphicsMagick.
282
+ #
283
+ # @return [Hash]
284
+ attribute :details
285
+
286
+ ##
287
+ # Use this method if you want to access raw Identify's format API.
209
288
  #
210
289
  # @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
290
+ # image["%w %h"] #=> "250 450"
291
+ # image["%r"] #=> "DirectClass sRGB"
292
+ #
293
+ # @param value [String]
294
+ # @see http://www.imagemagick.org/script/escape.php
295
+ # @return [String]
296
+ #
224
297
  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]
298
+ @info[value.to_s]
299
+ end
300
+ alias info []
301
+
302
+ ##
303
+ # Returns layers of the image. For example, JPEGs are 1-layered, but
304
+ # formats like PSDs, GIFs and PDFs can have multiple layers/frames/pages.
305
+ #
306
+ # @example
307
+ # image = MiniMagick::Image.new("document.pdf")
308
+ # image.pages.each_with_index do |page, idx|
309
+ # page.write("page#{idx}.pdf")
310
+ # end
311
+ # @return [Array<MiniMagick::Image>]
312
+ #
313
+ def layers
314
+ layers_count = identify.lines.count
315
+ layers_count.times.map do |idx|
316
+ MiniMagick::Image.new("#{path}[#{idx}]")
317
+ end
262
318
  end
319
+ alias pages layers
320
+ alias frames layers
263
321
 
264
- # Sends raw commands to imagemagick's `mogrify` command. The image path is
265
- # automatically appended to the command.
322
+ ##
323
+ # Returns a matrix of pixels from the image. The matrix is constructed as
324
+ # an array (1) of arrays (2) of arrays (3) of unsigned integers:
325
+ #
326
+ # 1) one for each row of pixels
327
+ # 2) one for each column of pixels
328
+ # 3) three elements in the range 0-255, one for each of the RGB color channels
266
329
  #
267
- # Remember, we are always acting on this instance of the Image when messing
268
- # with this.
330
+ # @example
331
+ # img = MiniMagick::Image.open 'image.jpg'
332
+ # pixels = img.get_pixels
333
+ # pixels[3][2][1] # the green channel value from the 4th-row, 3rd-column pixel
334
+ #
335
+ # It can also be called after applying transformations:
269
336
  #
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)
337
+ # @example
338
+ # img = MiniMagick::Image.open 'image.jpg'
339
+ # img.crop '20x30+10+5'
340
+ # img.colorspace 'Gray'
341
+ # pixels = img.get_pixels
342
+ #
343
+ # In this example, all pixels in pix should now have equal R, G, and B values.
344
+ #
345
+ # @return [Array] Matrix of each color of each pixel
346
+ def get_pixels
347
+ convert = MiniMagick::Tool::Convert.new
348
+ convert << path
349
+ convert.depth(8)
350
+ convert << "RGB:-"
351
+
352
+ # Do not use `convert.call` here. We need the whole binary (unstripped) output here.
353
+ shell = MiniMagick::Shell.new
354
+ output, * = shell.run(convert.command)
355
+
356
+ pixels_array = output.unpack("C*")
357
+ pixels = pixels_array.each_slice(3).each_slice(width).to_a
358
+
359
+ # deallocate large intermediary objects
360
+ output.clear
361
+ pixels_array.clear
362
+
363
+ pixels
274
364
  end
275
365
 
366
+ ##
276
367
  # This is used to change the format of the image. That is, from "tiff to
277
368
  # jpg" or something like that. Once you run it, the instance is pointing to
278
369
  # a new file with a new extension!
@@ -293,169 +384,193 @@ module MiniMagick
293
384
  # @param page [Integer] If this is an animated gif, say which 'page' you
294
385
  # want with an integer. Default 0 will convert only the first page; 'nil'
295
386
  # will convert all pages.
296
- # @return [nil]
297
- def format(format, page = 0)
298
- run_queue if @command_queued
387
+ # @param read_opts [Hash] Any read options to be passed to ImageMagick
388
+ # for example: image.format('jpg', page, {density: '300'})
389
+ # @yield [MiniMagick::Tool::Convert] It optionally yields the command,
390
+ # if you want to add something.
391
+ # @return [self]
392
+ #
393
+ def format(format, page = 0, read_opts={})
394
+ if @tempfile
395
+ new_tempfile = MiniMagick::Utilities.tempfile(".#{format}")
396
+ new_path = new_tempfile.path
397
+ else
398
+ new_path = Pathname(path).sub_ext(".#{format}").to_s
399
+ end
400
+
401
+ input_path = path.dup
402
+ input_path << "[#{page}]" if page && !layer?
299
403
 
300
- c = CommandBuilder.new('mogrify', '-format', format)
301
- yield c if block_given?
302
- c << (page ? "#{path}[#{page}]" : path)
303
- run(c)
404
+ MiniMagick::Tool::Convert.new do |convert|
405
+ read_opts.each do |opt, val|
406
+ convert.send(opt.to_s, val)
407
+ end
408
+ convert << input_path
409
+ yield convert if block_given?
410
+ convert << new_path
411
+ end
412
+
413
+ if @tempfile
414
+ destroy!
415
+ @tempfile = new_tempfile
416
+ else
417
+ File.delete(path) unless path == new_path || layer?
418
+ end
304
419
 
305
- old_path = path
420
+ path.replace new_path
421
+ @info.clear
306
422
 
307
- self.path = path.sub(/(\.\w*)?$/, (page ? ".#{format}" : "-0.#{format}"))
423
+ self
424
+ end
308
425
 
309
- File.delete(old_path) if old_path != path
426
+ ##
427
+ # You can use multiple commands together using this method. Very easy to
428
+ # use!
429
+ #
430
+ # @example
431
+ # image.combine_options do |c|
432
+ # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
433
+ # c.thumbnail "300x500>"
434
+ # c.background "blue"
435
+ # end
436
+ #
437
+ # @yield [MiniMagick::Tool::Mogrify]
438
+ # @see http://www.imagemagick.org/script/mogrify.php
439
+ # @return [self]
440
+ #
441
+ def combine_options(&block)
442
+ mogrify(&block)
443
+ end
310
444
 
311
- unless File.exist?(path)
312
- fail MiniMagick::Error, "Unable to format to #{format}"
445
+ ##
446
+ # If an unknown method is called then it is sent through the mogrify
447
+ # program.
448
+ #
449
+ # @see http://www.imagemagick.org/script/mogrify.php
450
+ # @return [self]
451
+ #
452
+ def method_missing(name, *args)
453
+ mogrify do |builder|
454
+ builder.send(name, *args)
313
455
  end
314
456
  end
315
457
 
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]")
458
+ def respond_to_missing?(method_name, include_private = false)
459
+ MiniMagick::Tool::Mogrify.option_methods.include?(method_name.to_s)
320
460
  end
321
461
 
462
+ ##
322
463
  # Writes the temporary file out to either a file location (by passing in a
323
464
  # String) or by passing in a Stream that you can #write(chunk) to
324
465
  # repeatedly
325
466
  #
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.
467
+ # @param output_to [String, Pathname, #read] Some kind of stream object
468
+ # that needs to be read or a file path as a String
469
+ #
330
470
  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)
471
+ case output_to
472
+ when String, Pathname
473
+ if layer?
474
+ MiniMagick::Tool::Convert.new do |builder|
475
+ builder << path
476
+ builder << output_to
345
477
  end
478
+ else
479
+ FileUtils.copy_file path, output_to unless path == output_to.to_s
346
480
  end
347
- output_to
481
+ else
482
+ IO.copy_stream File.open(path, "rb"), output_to
348
483
  end
349
484
  end
350
485
 
351
- # Gives you raw image data back
352
- # @return [String] binary string
353
- def to_blob
354
- run_queue if @command_queued
486
+ ##
487
+ # @example
488
+ # first_image = MiniMagick::Image.open "first.jpg"
489
+ # second_image = MiniMagick::Image.open "second.jpg"
490
+ # result = first_image.composite(second_image) do |c|
491
+ # c.compose "Over" # OverCompositeOp
492
+ # c.geometry "+20+20" # copy second_image onto first_image from (20, 20)
493
+ # end
494
+ # result.write "output.jpg"
495
+ #
496
+ # @see http://www.imagemagick.org/script/composite.php
497
+ #
498
+ def composite(other_image, output_extension = type.downcase, mask = nil)
499
+ output_tempfile = MiniMagick::Utilities.tempfile(".#{output_extension}")
500
+
501
+ MiniMagick::Tool::Composite.new do |composite|
502
+ yield composite if block_given?
503
+ composite << other_image.path
504
+ composite << path
505
+ composite << mask.path if mask
506
+ composite << output_tempfile.path
507
+ end
355
508
 
356
- f = File.new path
357
- f.binmode
358
- f.read
359
- ensure
360
- f.close if f
509
+ Image.new(output_tempfile.path, output_tempfile)
361
510
  end
362
511
 
363
- def mime_type
364
- format = self[:format]
365
- 'image/' + format.to_s.downcase
512
+ ##
513
+ # Collapse images with sequences to the first frame (i.e. animated gifs) and
514
+ # preserve quality.
515
+ #
516
+ # @param frame [Integer] The frame to which to collapse to, defaults to `0`.
517
+ # @return [self]
518
+ #
519
+ def collapse!(frame = 0)
520
+ mogrify(frame) { |builder| builder.quality(100) }
366
521
  end
367
522
 
368
- # If an unknown method is called then it is sent through the mogrify
369
- # program.
523
+ ##
524
+ # Destroys the tempfile (created by {.open}) if it exists.
370
525
  #
371
- # @see http://www.imagemagick.org/script/mogrify.php
372
- def method_missing(symbol, *args)
373
- @queue.send(symbol, *args)
374
- @command_queued = true
526
+ def destroy!
527
+ if @tempfile
528
+ FileUtils.rm_f @tempfile.path.sub(/mpc$/, "cache") if @tempfile.path.end_with?(".mpc")
529
+ @tempfile.unlink
530
+ end
375
531
  end
376
532
 
377
- # You can use multiple commands together using this method. Very easy to
378
- # use!
533
+ ##
534
+ # Runs `identify` on itself. Accepts an optional block for adding more
535
+ # options to `identify`.
379
536
  #
380
537
  # @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
538
+ # image = MiniMagick::Image.open("image.jpg")
539
+ # image.identify do |b|
540
+ # b.verbose
541
+ # end # runs `identify -verbose image.jpg`
542
+ # @return [String] Output from `identify`
543
+ # @yield [MiniMagick::Tool::Identify]
386
544
  #
387
- # @yieldparam command [CommandBuilder]
388
- def combine_options
389
- if block_given?
390
- yield @queue
391
- @command_queued = true
545
+ def identify
546
+ MiniMagick::Tool::Identify.new do |builder|
547
+ yield builder if block_given?
548
+ builder << path
392
549
  end
393
550
  end
394
551
 
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
552
+ # @private
553
+ def run_command(tool_name, *args)
554
+ MiniMagick::Tool.const_get(tool_name.capitalize).new do |builder|
555
+ args.each do |arg|
556
+ builder << arg
557
+ end
402
558
  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
559
  end
414
560
 
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.
561
+ def mogrify(page = nil)
562
+ MiniMagick::Tool::MogrifyRestricted.new do |builder|
563
+ yield builder if block_given?
564
+ builder << (page ? "#{path}[#{page}]" : path)
421
565
  end
422
566
 
423
- run(CommandBuilder.new(command, *args))
424
- end
425
-
426
- def run(command_builder)
427
- command = command_builder.command
428
-
429
- sub = Subexec.run(command, :timeout => MiniMagick.timeout)
430
-
431
- if sub.exitstatus != 0
432
- # Clean up after ourselves in case of an error
433
- destroy!
434
-
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}"
442
- end
443
- else
444
- sub.output
445
- end
446
- end
567
+ @info.clear
447
568
 
448
- def destroy!
449
- return if @tempfile.nil?
450
- File.unlink(@path) if File.exist?(@path)
451
- @tempfile = nil
569
+ self
452
570
  end
453
571
 
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
572
+ def layer?
573
+ path =~ /\[\d+\]$/
459
574
  end
460
575
  end
461
576
  end