mini_magick 3.8.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +122 -0
  5. data/lib/mini_magick/image.rb +408 -343
  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 +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 +460 -0
  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 +39 -0
  34. data/spec/spec_helper.rb +21 -0
  35. data/spec/support/helpers.rb +37 -0
  36. metadata +50 -52
  37. data/lib/mini_magick/command_builder.rb +0 -110
  38. data/lib/mini_magick/errors.rb +0 -4
@@ -1,442 +1,507 @@
1
+ require 'tempfile'
2
+ require 'stringio'
3
+ require 'pathname'
4
+ require 'uri'
5
+ require 'open-uri'
6
+
7
+ require 'mini_magick/image/info'
8
+ require 'mini_magick/utilities'
9
+
1
10
  module MiniMagick
2
11
  class Image
3
- # @return [String] The location of the current working file
4
- attr_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
-
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
64
 
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
115
93
 
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)
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
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
149
130
  #
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!
131
+ attr_reader :path
132
+
133
+ ##
134
+ # Create a new {MiniMagick::Image} object.
135
+ #
136
+ # _DANGER_: The file location passed in here is the *working copy*. That
137
+ # is, it gets *modified*. You can either copy it yourself or use {.open}
138
+ # which creates a temporary file for you and protects your original.
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
146
+ @tempfile = tempfile
147
+ @info = MiniMagick::Image::Info.new(@path)
148
+
149
+ combine_options(&block) if block
161
150
  end
162
151
 
163
- def reset_queue
164
- @command_queued = false
165
- @queue = MiniMagick::CommandBuilder.new('mogrify')
166
- @info.clear
152
+ def eql?(other)
153
+ self.class.equal?(other.class) &&
154
+ signature == other.signature
167
155
  end
156
+ alias == eql?
168
157
 
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
158
+ def hash
159
+ signature.hash
174
160
  end
175
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
+ ##
176
172
  # Checks to make sure that MiniMagick can read the file and understand it.
177
173
  #
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.
174
+ # This uses the 'identify' command line utility to check the file. If you
175
+ # are having issues with this, then please work directly with the
176
+ # 'identify' command and see if you can figure out what the issue is.
181
177
  #
182
178
  # @return [Boolean]
179
+ #
183
180
  def valid?
184
- run_command('identify', path)
181
+ validate!
185
182
  true
186
183
  rescue MiniMagick::Invalid
187
184
  false
188
185
  end
189
186
 
190
- def info(key)
191
- run_queue if @command_queued
192
-
193
- @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
194
197
  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!
198
+
199
+ ##
200
+ # Returns the image format (e.g. "JPEG", "GIF").
201
+ #
202
+ # @return [String]
203
+ #
204
+ attribute :type, "format"
205
+ ##
206
+ # @return [String]
207
+ #
208
+ attribute :mime_type
209
+ ##
210
+ # @return [Integer]
211
+ #
212
+ attribute :width
213
+ ##
214
+ # @return [Integer]
215
+ #
216
+ attribute :height
217
+ ##
218
+ # @return [Array<Integer>]
219
+ #
220
+ attribute :dimensions
221
+ ##
222
+ # Returns the file size of the image.
223
+ #
224
+ # @return [Integer]
225
+ #
226
+ attribute :size
227
+ ##
228
+ # @return [String]
229
+ #
230
+ attribute :colorspace
231
+ ##
232
+ # @return [Hash]
233
+ #
234
+ attribute :exif
235
+ ##
236
+ # Returns the resolution of the photo. You can optionally specify the
237
+ # units measurement.
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.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
+ #
211
269
  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]
270
+ @info[value.to_s]
248
271
  end
272
+ alias info []
249
273
 
250
- # Sends raw commands to imagemagick's `mogrify` command. The image path is automatically appended to the command.
274
+ ##
275
+ # Returns layers of the image. For example, JPEGs are 1-layered, but
276
+ # formats like PSDs, GIFs and PDFs can have multiple layers/frames/pages.
251
277
  #
252
- # Remember, we are always acting on this instance of the Image when messing with this.
278
+ # @example
279
+ # image = MiniMagick::Image.new("document.pdf")
280
+ # image.pages.each_with_index do |page, idx|
281
+ # page.write("page#{idx}.pdf")
282
+ # end
283
+ # @return [Array<MiniMagick::Image>]
253
284
  #
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)
285
+ def layers
286
+ layers_count = identify.lines.count
287
+ layers_count.times.map do |idx|
288
+ MiniMagick::Image.new("#{path}[#{idx}]")
289
+ end
257
290
  end
291
+ alias pages layers
292
+ alias frames layers
258
293
 
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!
294
+ ##
295
+ # This is used to change the format of the image. That is, from "tiff to
296
+ # jpg" or something like that. Once you run it, the instance is pointing to
297
+ # a new file with a new extension!
261
298
  #
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...
299
+ # *DANGER*: This renames the file that the instance is pointing to. So, if
300
+ # you manually opened the file with Image.new(file_path)... Then that file
301
+ # is DELETED! If you used Image.open(file) then you are OK. The original
302
+ # file will still be there. But, any changes to it might not be...
265
303
  #
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.
304
+ # Formatting an animation into a non-animated type will result in
305
+ # ImageMagick creating multiple pages (starting with 0). You can choose
306
+ # which page you want to manipulate. We default to the first page.
269
307
  #
270
308
  # If you would like to convert between animated formats, pass nil as your
271
309
  # page and ImageMagick will copy all of the pages.
272
310
  #
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]
311
+ # @param format [String] The target format... Like 'jpg', 'gif', 'tiff' etc.
312
+ # @param page [Integer] If this is an animated gif, say which 'page' you
313
+ # want with an integer. Default 0 will convert only the first page; 'nil'
314
+ # will convert all pages.
315
+ # @yield [MiniMagick::Tool::Convert] It optionally yields the command,
316
+ # if you want to add something.
317
+ # @return [self]
318
+ #
278
319
  def format(format, page = 0)
279
- run_queue if @command_queued
320
+ @info.clear
280
321
 
281
- c = CommandBuilder.new('mogrify', '-format', format)
282
- yield c if block_given?
283
- c << (page ? "#{path}[#{page}]" : path)
284
- 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
285
328
 
286
- 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
287
334
 
288
- 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
289
341
 
290
- File.delete(old_path) if old_path != path
342
+ path.replace new_path
343
+
344
+ self
345
+ end
291
346
 
292
- unless File.exist?(path)
293
- 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
294
380
  end
295
381
  end
296
382
 
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]")
383
+ def respond_to_missing?(method_name, include_private = false)
384
+ MiniMagick::Tool::Mogrify.new.respond_to?(method_name, include_private)
301
385
  end
302
386
 
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
387
+ ##
388
+ # Writes the temporary file out to either a file location (by passing in a
389
+ # String) or by passing in a Stream that you can #write(chunk) to
390
+ # repeatedly
391
+ #
392
+ # @param output_to [String, Pathname, #read] Some kind of stream object
393
+ # that needs to be read or a file path as a String
305
394
  #
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
395
  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
318
- 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)
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
324
402
  end
403
+ else
404
+ FileUtils.copy_file path, output_to
325
405
  end
326
- output_to
406
+ else
407
+ IO.copy_stream File.open(path, "rb"), output_to
327
408
  end
328
409
  end
329
410
 
330
- # Gives you raw image data back
331
- # @return [String] binary string
332
- def to_blob
333
- 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
334
433
 
335
- f = File.new path
336
- f.binmode
337
- f.read
338
- ensure
339
- f.close if f
434
+ Image.new(output_tempfile.path, output_tempfile)
340
435
  end
341
436
 
342
- def mime_type
343
- format = self[:format]
344
- '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) }
345
446
  end
346
447
 
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
448
+ ##
449
+ # Destroys the tempfile (created by {.open}) if it exists.
450
+ #
451
+ def destroy!
452
+ @tempfile.unlink if @tempfile
352
453
  end
353
454
 
354
- # You can use multiple commands together using this method. Very easy to use!
455
+ ##
456
+ # Runs `identify` on itself. Accepts an optional block for adding more
457
+ # options to `identify`.
355
458
  #
356
459
  # @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
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]
362
466
  #
363
- # @yieldparam command [CommandBuilder]
364
- def combine_options
365
- if block_given?
366
- yield @queue
367
- @command_queued = true
467
+ def identify
468
+ MiniMagick::Tool::Identify.new do |builder|
469
+ yield builder if block_given?
470
+ builder << path
368
471
  end
369
472
  end
370
473
 
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.
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
397
480
  end
398
-
399
- run(CommandBuilder.new(command, *args))
400
481
  end
401
482
 
402
- def run(command_builder)
403
- command = command_builder.command
404
-
405
- sub = Subexec.run(command, :timeout => MiniMagick.timeout)
483
+ private
406
484
 
407
- if sub.exitstatus != 0
408
- # Clean up after ourselves in case of an error
409
- destroy!
485
+ def mogrify(page = nil)
486
+ @info.clear
410
487
 
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}"
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
418
494
  end
419
- else
420
- sub.output
495
+ yield builder if block_given?
496
+ builder << (page ? "#{path}[#{page}]" : path)
421
497
  end
422
- end
423
498
 
424
- def destroy!
425
- return if @tempfile.nil?
426
- File.unlink(path) if File.exist?(path)
427
- @tempfile = nil
499
+ self
428
500
  end
429
501
 
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
502
+ def layer?
503
+ path =~ /\[\d+\]$/
440
504
  end
505
+
441
506
  end
442
507
  end