mini_magick 3.6.0 → 3.7.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.

Potentially problematic release.


This version of mini_magick might be problematic. Click here for more details.

@@ -0,0 +1,104 @@
1
+ module MiniMagick
2
+ class CommandBuilder
3
+ MOGRIFY_COMMANDS = %w{adaptive-blur adaptive-resize adaptive-sharpen adjoin affine alpha annotate antialias append attenuate authenticate auto-gamma auto-level auto-orient backdrop background bench bias black-point-compensation black-threshold blend blue-primary blue-shift blur border bordercolor borderwidth brightness-contrast cache caption cdl channel charcoal chop clamp clip clip-mask clip-path clone clut coalesce colorize colormap color-matrix colors colorspace combine comment compose composite compress contrast contrast-stretch convolve crop cycle debug decipher deconstruct define delay delete density depth descend deskew despeckle direction displace display dispose dissimilarity-threshold dissolve distort dither draw duplicate edge emboss encipher encoding endian enhance equalize evaluate evaluate-sequence extent extract family features fft fill filter flatten flip floodfill flop font foreground format frame function fuzz fx gamma gaussian-blur geometry gravity green-primary hald-clut help highlight-color iconGeometry iconic identify ift immutable implode insert intent interlace interpolate interline-spacing interword-spacing kerning label lat layers level level-colors limit linear-stretch linewidth liquid-rescale list log loop lowlight-color magnify map mask mattecolor median metric mode modulate monitor monochrome morph morphology mosaic motion-blur name negate noise normalize opaque ordered-dither orient page paint path pause pen perceptible ping pointsize polaroid poly posterize precision preview print process profile quality quantize quiet radial-blur raise random-threshold red-primary regard-warnings region remap remote render repage resample resize respect-parentheses reverse roll rotate sample sampling-factor scale scene screen seed segment selective-blur separate sepia-tone set shade shadow shared-memory sharpen shave shear sigmoidal-contrast silent size sketch smush snaps solarize sparse-color splice spread statistic stegano stereo stretch strip stroke strokewidth style subimage-search swap swirl synchronize taint text-font texture threshold thumbnail tile tile-offset tint title transform transparent transparent-color transpose transverse treedepth trim type undercolor unique-colors units unsharp update verbose version view vignette virtual-pixel visual watermark wave weight white-point white-threshold window window-group write}
4
+ IMAGE_CREATION_OPERATORS = %w{canvas caption gradient label logo pattern plasma radial radient rose text tile xc }
5
+
6
+ def initialize(tool, *options)
7
+ @tool = tool
8
+ @args = []
9
+ options.each { |arg| push(arg) }
10
+ end
11
+
12
+ def command
13
+ com = "#{@tool} #{args.join(' ')}".strip
14
+ com = "#{MiniMagick.processor} #{com}" unless MiniMagick.mogrify?
15
+
16
+ com = File.join MiniMagick.processor_path, com unless MiniMagick.processor_path.nil?
17
+ com.strip
18
+ end
19
+
20
+ def escape_string_windows(value)
21
+ # For Windows, ^ is the escape char, equivalent to \ in Unix.
22
+ escaped = value.gsub(/\^/, '^^').gsub(/>/, '^>')
23
+ if escaped !~ /^".+"$/ && escaped.include?("'")
24
+ escaped.inspect
25
+ else
26
+ escaped
27
+ end
28
+
29
+ end
30
+
31
+ def args
32
+ if !MiniMagick::Utilities.windows?
33
+ @args.map(&:shellescape)
34
+ else
35
+ @args.map { |arg| escape_string_windows(arg) }
36
+ end
37
+ end
38
+
39
+ # Add each mogrify command in both underscore and dash format
40
+ MOGRIFY_COMMANDS.each do |mogrify_command|
41
+
42
+ # Example of what is generated here:
43
+ #
44
+ # def auto_orient(*options)
45
+ # add_command("auto-orient", *options)
46
+ # self
47
+ # end
48
+ # alias_method :"auto-orient", :auto_orient
49
+
50
+ dashed_command = mogrify_command.to_s.gsub("_","-")
51
+ underscored_command = mogrify_command.to_s.gsub("-","_")
52
+
53
+ define_method(underscored_command) do |*options|
54
+ add_command(__method__.to_s.gsub("_","-"), *options)
55
+ self
56
+ end
57
+ alias_method dashed_command, underscored_command
58
+ end
59
+
60
+ def format(*options)
61
+ raise Error, "You must call 'format' on the image object directly!"
62
+ end
63
+
64
+ IMAGE_CREATION_OPERATORS.each do |operator|
65
+ define_method operator do |*options|
66
+ add_creation_operator(__method__.to_s, *options)
67
+ self
68
+ end
69
+ end
70
+
71
+ def +(*options)
72
+ push(@args.pop.gsub(/^-/, '+'))
73
+ if options.any?
74
+ options.each do |o|
75
+ push o
76
+ end
77
+ end
78
+ end
79
+
80
+ def add_command(command, *options)
81
+ push "-#{command}"
82
+ if options.any?
83
+ options.each do |o|
84
+ push o
85
+ end
86
+ end
87
+ end
88
+
89
+ def add_creation_operator(command, *options)
90
+ creation_command = command
91
+ if options.any?
92
+ options.each do |option|
93
+ creation_command << ":#{option}"
94
+ end
95
+ end
96
+ push creation_command
97
+ end
98
+
99
+ def push(arg)
100
+ @args << arg.to_s.strip
101
+ end
102
+ alias :<< :push
103
+ end
104
+ end
@@ -0,0 +1,4 @@
1
+ module MiniMagick
2
+ class Error < RuntimeError; end
3
+ class Invalid < StandardError; end
4
+ end
@@ -0,0 +1,405 @@
1
+ module MiniMagick
2
+ class Image
3
+ # @return [String] The location of the current working file
4
+ attr_accessor :path
5
+
6
+ def path_for_windows_quote_space(path)
7
+ path = Pathname.new(@path).to_s
8
+ # For Windows, if a path contains space char, you need to quote it, otherwise you SHOULD NOT quote it.
9
+ # If you quote a path that does not contains space, it will not work.
10
+ @path.include?(' ') ? path.inspect : path
11
+ end
12
+
13
+ def path
14
+ MiniMagick::Utilities.windows? ? path_for_windows_quote_space(@path) : @path
15
+ end
16
+
17
+ def path=(path)
18
+ @path = path
19
+ end
20
+
21
+ # Class Methods
22
+ # -------------
23
+ class << self
24
+ # This is the primary loading method used by all of the other class methods.
25
+ #
26
+ # Use this to pass in a stream object. Must respond to Object#read(size) or be a binary string object (BLOBBBB)
27
+ #
28
+ # As a change from the old API, please try and use IOStream objects. They are much, much better and more efficient!
29
+ #
30
+ # Probably easier to use the #open method if you want to open a file or a URL.
31
+ #
32
+ # @param stream [IOStream, String] Some kind of stream object that needs to be read or is a binary String blob!
33
+ # @param ext [String] A manual extension to use for reading the file. Not required, but if you are having issues, give this a try.
34
+ # @return [Image]
35
+ def read(stream, ext = nil)
36
+ if stream.is_a?(String)
37
+ stream = StringIO.new(stream)
38
+ elsif stream.is_a?(StringIO)
39
+ # Do nothing, we want a StringIO-object
40
+ elsif stream.respond_to? :path
41
+ if File.respond_to?(:binread)
42
+ stream = StringIO.new File.binread(stream.path.to_s)
43
+ else
44
+ stream = StringIO.new File.open(stream.path.to_s,"rb") { |f| f.read }
45
+ end
46
+ end
47
+
48
+ create(ext) do |f|
49
+ while chunk = stream.read(8192)
50
+ f.write(chunk)
51
+ end
52
+ end
53
+ end
54
+
55
+ # @deprecated Please use Image.read instead!
56
+ def from_blob(blob, ext = nil)
57
+ warn "Warning: MiniMagick::Image.from_blob method is deprecated. Instead, please use Image.read"
58
+ create(ext) { |f| f.write(blob) }
59
+ end
60
+
61
+ # Creates an image object from a binary string blob which contains raw pixel data (i.e. no header data).
62
+ #
63
+ # === Returns
64
+ #
65
+ # * [Image] The loaded image.
66
+ #
67
+ # === Parameters
68
+ #
69
+ # * [blob] <tt>String</tt> -- Binary string blob containing raw pixel data.
70
+ # * [columns] <tt>Integer</tt> -- Number of columns.
71
+ # * [rows] <tt>Integer</tt> -- Number of rows.
72
+ # * [depth] <tt>Integer</tt> -- Bit depth of the encoded pixel data.
73
+ # * [map] <tt>String</tt> -- A code for the mapping of the pixel data. Example: 'gray' or 'rgb'.
74
+ # * [format] <tt>String</tt> -- The file extension of the image format to be used when creating the image object. Defaults to 'png'.
75
+ #
76
+ def import_pixels(blob, columns, rows, depth, map, format="png")
77
+ # Create an image object with the raw pixel data string:
78
+ image = create(".dat", validate = false) { |f| f.write(blob) }
79
+ # Use ImageMagick to convert the raw data file to an image file of the desired format:
80
+ converted_image_path = image.path[0..-4] + format
81
+ arguments = ["-size", "#{columns}x#{rows}", "-depth", "#{depth}", "#{map}:#{image.path}", "#{converted_image_path}"]
82
+ cmd = CommandBuilder.new("convert", *arguments) #Example: convert -size 256x256 -depth 16 gray:blob.dat blob.png
83
+ image.run(cmd)
84
+ # Update the image instance with the path of the properly formatted image, and return:
85
+ image.path = converted_image_path
86
+ image
87
+ end
88
+
89
+ # Opens a specific image file either on the local file system or at a URI.
90
+ #
91
+ # Use this if you don't want to overwrite the image file.
92
+ #
93
+ # Extension is either guessed from the path or you can specify it as a second parameter.
94
+ #
95
+ # If you pass in what looks like a URL, we require 'open-uri' before opening it.
96
+ #
97
+ # @param file_or_url [String] Either a local file path or a URL that open-uri can read
98
+ # @param ext [String] Specify the extension you want to read it as
99
+ # @return [Image] The loaded image
100
+ def open(file_or_url, ext = nil)
101
+ file_or_url = file_or_url.to_s # Force it to be a String... hell or highwater
102
+ if file_or_url.include?("://")
103
+ require 'open-uri'
104
+ ext ||= File.extname(URI.parse(file_or_url).path)
105
+ Kernel::open(file_or_url) do |f|
106
+ self.read(f, ext)
107
+ end
108
+ else
109
+ ext ||= File.extname(file_or_url)
110
+ File.open(file_or_url, "rb") do |f|
111
+ self.read(f, ext)
112
+ end
113
+ end
114
+ end
115
+
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)
120
+ end
121
+
122
+ # Used to create a new Image object data-copy. Not used to "paint" or that kind of thing.
123
+ #
124
+ # Takes an extension in a block and can be used to build a new Image object. Used
125
+ # by both #open and #read to create a new object! Ensures we have a good tempfile!
126
+ #
127
+ # @param ext [String] Specify the extension you want to read it as
128
+ # @param validate [Boolean] If false, skips validation of the created image. Defaults to true.
129
+ # @yield [IOStream] You can #write bits to this object to create the new Image
130
+ # @return [Image] The created image
131
+ def create(ext = nil, validate = true, &block)
132
+ begin
133
+ tempfile = Tempfile.new(['mini_magick', ext.to_s.downcase])
134
+ tempfile.binmode
135
+ block.call(tempfile)
136
+ tempfile.close
137
+
138
+ image = self.new(tempfile.path, tempfile)
139
+
140
+ if validate and !image.valid?
141
+ raise MiniMagick::Invalid
142
+ end
143
+ return image
144
+ ensure
145
+ tempfile.close if tempfile
146
+ end
147
+ end
148
+ end
149
+
150
+ # Create a new MiniMagick::Image object
151
+ #
152
+ # _DANGER_: The file location passed in here is the *working copy*. That is, it gets *modified*.
153
+ # you can either copy it yourself or use the MiniMagick::Image.open(path) method which creates a
154
+ # temporary file for you and protects your original!
155
+ #
156
+ # @param input_path [String] The location of an image file
157
+ # @todo Allow this to accept a block that can pass off to Image#combine_options
158
+ def initialize(input_path, tempfile = nil)
159
+ @path = input_path
160
+ @tempfile = tempfile # ensures that the tempfile will stick around until this image is garbage collected.
161
+ end
162
+
163
+ # Checks to make sure that MiniMagick can read the file and understand it.
164
+ #
165
+ # This uses the 'identify' command line utility to check the file. If you are having
166
+ # issues with this, then please work directly with the 'identify' command and see if you
167
+ # can figure out what the issue is.
168
+ #
169
+ # @return [Boolean]
170
+ def valid?
171
+ run_command("identify", path)
172
+ true
173
+ rescue MiniMagick::Invalid
174
+ false
175
+ end
176
+
177
+ # A rather low-level way to interact with the "identify" command. No nice API here, just
178
+ # the crazy stuff you find in ImageMagick. See the examples listed!
179
+ #
180
+ # @example
181
+ # image["format"] #=> "TIFF"
182
+ # image["height"] #=> 41 (pixels)
183
+ # image["width"] #=> 50 (pixels)
184
+ # image["colorspace"] #=> "DirectClassRGB"
185
+ # image["dimensions"] #=> [50, 41]
186
+ # image["size"] #=> 2050 (bits)
187
+ # image["original_at"] #=> 2005-02-23 23:17:24 +0000 (Read from Exif data)
188
+ # image["EXIF:ExifVersion"] #=> "0220" (Can read anything from Exif)
189
+ #
190
+ # @param format [String] A format for the "identify" command
191
+ # @see For reference see http://www.imagemagick.org/script/command-line-options.php#format
192
+ # @return [String, Numeric, Array, Time, Object] Depends on the method called! Defaults to String for unknown commands
193
+ def [](value)
194
+ # Why do I go to the trouble of putting in newlines? Because otherwise animated gifs screw everything up
195
+ case value.to_s
196
+ when "colorspace"
197
+ run_command("identify", "-format", '%r\n', path).split("\n")[0].strip
198
+ when "format"
199
+ run_command("identify", "-format", '%m\n', path).split("\n")[0]
200
+ when "height"
201
+ run_command("identify", "-format", '%h\n', path).split("\n")[0].to_i
202
+ when "width"
203
+ run_command("identify", "-format", '%w\n', path).split("\n")[0].to_i
204
+ when "dimensions"
205
+ run_command("identify", "-format", MiniMagick::Utilities.windows? ? '"%w %h\n"' : '%w %h\n', path).split("\n")[0].split.map{|v|v.to_i}
206
+ when "size"
207
+ File.size(path) # Do this because calling identify -format "%b" on an animated gif fails!
208
+ when "original_at"
209
+ # Get the EXIF original capture as a Time object
210
+ Time.local(*self["EXIF:DateTimeOriginal"].split(/:|\s+/)) rescue nil
211
+ when /^EXIF\:/i
212
+ result = run_command('identify', '-format', "%[#{value}]", path).chomp
213
+ if result.include?(",")
214
+ read_character_data(result)
215
+ else
216
+ result
217
+ end
218
+ else
219
+ run_command('identify', '-format', value, path).split("\n")[0]
220
+ end
221
+ end
222
+
223
+ # Sends raw commands to imagemagick's `mogrify` command. The image path is automatically appended to the command.
224
+ #
225
+ # Remember, we are always acting on this instance of the Image when messing with this.
226
+ #
227
+ # @return [String] Whatever the result from the command line is. May not be terribly useful.
228
+ def <<(*args)
229
+ run_command("mogrify", *args << path)
230
+ end
231
+
232
+ # This is used to change the format of the image. That is, from "tiff to jpg" or something like that.
233
+ # Once you run it, the instance is pointing to a new file with a new extension!
234
+ #
235
+ # *DANGER*: This renames the file that the instance is pointing to. So, if you manually opened the
236
+ # file with Image.new(file_path)... then that file is DELETED! If you used Image.open(file) then
237
+ # you are ok. The original file will still be there. But, any changes to it might not be...
238
+ #
239
+ # Formatting an animation into a non-animated type will result in ImageMagick creating multiple
240
+ # pages (starting with 0). You can choose which page you want to manipulate. We default to the
241
+ # first page.
242
+ #
243
+ # If you would like to convert between animated formats, pass nil as your
244
+ # page and ImageMagick will copy all of the pages.
245
+ #
246
+ # @param format [String] The target format... like 'jpg', 'gif', 'tiff', etc.
247
+ # @param page [Integer] If this is an animated gif, say which 'page' you want
248
+ # with an integer. Default 0 will convert only the first page; 'nil' will
249
+ # convert all pages.
250
+ # @return [nil]
251
+ def format(format, page = 0)
252
+ c = CommandBuilder.new('mogrify', '-format', format)
253
+ yield c if block_given?
254
+ if page
255
+ c << "#{path}[#{page}]"
256
+ else
257
+ c << path
258
+ end
259
+ run(c)
260
+
261
+ old_path = path
262
+ self.path = path.sub(/(\.\w*)?$/, ".#{format}")
263
+ File.delete(old_path) if old_path != path
264
+
265
+ unless File.exists?(path)
266
+ raise MiniMagick::Error, "Unable to format to #{format}"
267
+ end
268
+ end
269
+
270
+ # Collapse images with sequences to the first frame (ie. animated gifs) and
271
+ # preserve quality
272
+ def collapse!
273
+ run_command("mogrify", "-quality", "100", "#{path}[0]")
274
+ end
275
+
276
+ # Writes the temporary file out to either a file location (by passing in a String) or by
277
+ # passing in a Stream that you can #write(chunk) to repeatedly
278
+ #
279
+ # @param output_to [IOStream, String] Some kind of stream object that needs to be read or a file path as a String
280
+ # @return [IOStream, Boolean] If you pass in a file location [String] then you get a success boolean. If its a stream, you get it back.
281
+ # Writes the temporary image that we are using for processing to the output path
282
+ def write(output_to)
283
+ if output_to.kind_of?(String) || !output_to.respond_to?(:write)
284
+ FileUtils.copy_file path, output_to
285
+ run_command "identify", MiniMagick::Utilities.windows? ? path_for_windows_quote_space(output_to.to_s) : output_to.to_s # Verify that we have a good image
286
+ else # stream
287
+ File.open(path, "rb") do |f|
288
+ f.binmode
289
+ while chunk = f.read(8192)
290
+ output_to.write(chunk)
291
+ end
292
+ end
293
+ output_to
294
+ end
295
+ end
296
+
297
+ # Gives you raw image data back
298
+ # @return [String] binary string
299
+ def to_blob
300
+ f = File.new path
301
+ f.binmode
302
+ f.read
303
+ ensure
304
+ f.close if f
305
+ end
306
+
307
+ def mime_type
308
+ format = self[:format]
309
+ "image/" + format.to_s.downcase
310
+ end
311
+
312
+ # If an unknown method is called then it is sent through the mogrify program
313
+ # Look here to find all the commands (http://www.imagemagick.org/script/mogrify.php)
314
+ def method_missing(symbol, *args)
315
+ combine_options do |c|
316
+ c.send(symbol, *args)
317
+ end
318
+ end
319
+
320
+ # You can use multiple commands together using this method. Very easy to use!
321
+ #
322
+ # @example
323
+ # image.combine_options do |c|
324
+ # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
325
+ # c.thumbnail "300x500>"
326
+ # c.background background
327
+ # end
328
+ #
329
+ # @yieldparam command [CommandBuilder]
330
+ def combine_options(tool = "mogrify", &block)
331
+ c = CommandBuilder.new(tool)
332
+
333
+ c << path if tool.to_s == "convert"
334
+ block.call(c)
335
+ c << path
336
+ run(c)
337
+ end
338
+
339
+ def composite(other_image, output_extension = 'jpg', &block)
340
+ begin
341
+ second_tempfile = Tempfile.new(output_extension)
342
+ second_tempfile.binmode
343
+ ensure
344
+ second_tempfile.close
345
+ end
346
+
347
+ command = CommandBuilder.new("composite")
348
+ block.call(command) if block
349
+ command.push(other_image.path)
350
+ command.push(self.path)
351
+ command.push(second_tempfile.path)
352
+
353
+ run(command)
354
+ return Image.new(second_tempfile.path, second_tempfile)
355
+ end
356
+
357
+ def run_command(command, *args)
358
+ if command == 'identify'
359
+ args.unshift '-ping' # -ping "efficiently determine image characteristics."
360
+ args.unshift '-quiet' if MiniMagick.mogrify? # graphicsmagick has no -quiet option.
361
+ end
362
+
363
+ run(CommandBuilder.new(command, *args))
364
+ end
365
+
366
+ def run(command_builder)
367
+ command = command_builder.command
368
+
369
+ sub = Subexec.run(command, :timeout => MiniMagick.timeout)
370
+
371
+ if sub.exitstatus != 0
372
+ # Clean up after ourselves in case of an error
373
+ destroy!
374
+
375
+ # Raise the appropriate error
376
+ if sub.output =~ /no decode delegate/i || sub.output =~ /did not return an image/i
377
+ raise Invalid, sub.output
378
+ else
379
+ # TODO: should we do something different if the command times out ...?
380
+ # its definitely better for logging.. otherwise we dont really know
381
+ raise Error, "Command (#{command.inspect.gsub("\\", "")}) failed: #{{:status_code => sub.exitstatus, :output => sub.output}.inspect}"
382
+ end
383
+ else
384
+ sub.output
385
+ end
386
+ end
387
+
388
+ def destroy!
389
+ return if @tempfile.nil?
390
+ File.unlink(path) if File.exists?(path)
391
+ @tempfile = nil
392
+ end
393
+
394
+ private
395
+ # Sometimes we get back a list of character values
396
+ def read_character_data(list_of_characters)
397
+ chars = list_of_characters.gsub(" ", "").split(",")
398
+ result = ""
399
+ chars.each do |val|
400
+ result << ("%c" % val.to_i)
401
+ end
402
+ result
403
+ end
404
+ end
405
+ end