mini_magick 3.6.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 095e582679936f6e2e1a42ea57b449867fa6eb75
4
+ data.tar.gz: eb49ba1a91002faa50fc14d512e95668f55e398d
5
+ SHA512:
6
+ metadata.gz: 0de53204c54171eae9d42c7789b62b7e11733b1019c2994656c76ca2c43040268e47a1f6c30baa9f41675880ff36cf5e22f47a028a3a8d9a2260d0340b570c0c
7
+ data.tar.gz: c03b97bb8c6be54b917afbc80ef3d738ced59c705ed209f3001c32cfe753bb4dc1e831397a1d0fb617b02cc95b9cb2d2a1574e2adc1334b90113e42d03556e4b
data/MIT-LICENSE CHANGED
File without changes
data/Rakefile CHANGED
@@ -1,20 +1,18 @@
1
1
  require 'bundler'
2
2
  Bundler::GemHelper.install_tasks
3
3
 
4
- require 'rake/testtask'
5
-
6
4
  $:.unshift 'lib'
7
5
 
8
6
  desc 'Default: run unit tests.'
9
- task :default => [:print_version, :test]
7
+ task :default => [:print_version, :spec]
10
8
 
11
9
  task :print_version do
12
10
  puts `mogrify --version`
13
11
  end
14
12
 
15
- desc 'Test the mini_magick plugin.'
16
- Rake::TestTask.new(:test) do |t|
17
- t.libs << 'test'
18
- t.test_files = Dir.glob("test/**/*_test.rb")
19
- t.verbose = true
13
+ require 'rspec/core/rake_task'
14
+
15
+ desc "Run specs"
16
+ RSpec::Core::RakeTask.new do |t|
17
+ t.pattern = "./spec/**/*_spec.rb"
20
18
  end
data/lib/mini_magick.rb CHANGED
@@ -3,6 +3,10 @@ require 'subexec'
3
3
  require 'stringio'
4
4
  require 'pathname'
5
5
  require 'shellwords'
6
+ require 'mini_magick/command_builder'
7
+ require 'mini_magick/errors'
8
+ require 'mini_magick/image'
9
+ require 'mini_magick/utilities'
6
10
 
7
11
  module MiniMagick
8
12
  class << self
@@ -10,509 +14,67 @@ module MiniMagick
10
14
  attr_accessor :processor_path
11
15
  attr_accessor :timeout
12
16
 
13
-
14
- # Experimental method for automatically selecting a processor
15
- # such as gm. Only works on *nix.
17
+ ##
18
+ # Tries to detect the current processor based if any of the processors exist.
19
+ # Mogrify have precedence over gm by default.
16
20
  #
17
- # TODO: Write tests for this and figure out what platforms it supports
21
+ # === Returns
22
+ # * [String] The detected procesor
18
23
  def choose_processor
19
- if `type -P mogrify`.size > 0
20
- return
21
- elsif `type -P gm`.size > 0
24
+ if MiniMagick::Utilities.which('mogrify').size > 0
25
+ self.processor = 'mogrify'
26
+ elsif MiniMagick::Utilities.which('gm').size > 0
22
27
  self.processor = "gm"
23
28
  end
24
29
  end
25
-
30
+
31
+ ##
32
+ # Discovers the imagemagick version based on mogrify's output.
33
+ #
34
+ # === Returns
35
+ # * The imagemagick version
26
36
  def image_magick_version
27
37
  @@version ||= Gem::Version.create(`mogrify --version`.split(" ")[2].split("-").first)
28
38
  end
29
-
39
+
40
+ ##
41
+ # The minimum allowed imagemagick version
42
+ #
43
+ # === Returns
44
+ # * The minimum imagemagick version
30
45
  def minimum_image_magick_version
31
46
  @@minimum_version ||= Gem::Version.create("6.6.3")
32
47
  end
33
48
 
49
+ ##
50
+ # Checks whether the imagemagick's version is valid
51
+ #
52
+ # === Returns
53
+ # * [Boolean]
34
54
  def valid_version_installed?
35
55
  image_magick_version >= minimum_image_magick_version
36
56
  end
37
- end
38
-
39
- 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}
40
57
 
41
- IMAGE_CREATION_OPERATORS = %w{canvas caption gradient label logo pattern plasma radial radient rose text tile xc }
42
-
43
- class Error < RuntimeError; end
44
- class Invalid < StandardError; end
45
-
46
- class Image
47
- # @return [String] The location of the current working file
48
- attr_accessor :path
49
-
50
- # Class Methods
51
- # -------------
52
- class << self
53
- # This is the primary loading method used by all of the other class methods.
54
- #
55
- # Use this to pass in a stream object. Must respond to Object#read(size) or be a binary string object (BLOBBBB)
56
- #
57
- # As a change from the old API, please try and use IOStream objects. They are much, much better and more efficient!
58
- #
59
- # Probably easier to use the #open method if you want to open a file or a URL.
60
- #
61
- # @param stream [IOStream, String] Some kind of stream object that needs to be read or is a binary String blob!
62
- # @param ext [String] A manual extension to use for reading the file. Not required, but if you are having issues, give this a try.
63
- # @return [Image]
64
- def read(stream, ext = nil)
65
- if stream.is_a?(String)
66
- stream = StringIO.new(stream)
67
- elsif stream.is_a?(StringIO)
68
- # Do nothing, we want a StringIO-object
69
- elsif stream.respond_to? :path
70
- if File.respond_to?(:binread)
71
- stream = StringIO.new File.binread(stream.path.to_s)
72
- else
73
- stream = StringIO.new File.open(stream.path.to_s,"rb") { |f| f.read }
74
- end
75
- end
76
-
77
- create(ext) do |f|
78
- while chunk = stream.read(8192)
79
- f.write(chunk)
80
- end
81
- end
82
- end
83
-
84
- # @deprecated Please use Image.read instead!
85
- def from_blob(blob, ext = nil)
86
- warn "Warning: MiniMagick::Image.from_blob method is deprecated. Instead, please use Image.read"
87
- create(ext) { |f| f.write(blob) }
88
- end
89
-
90
- # Creates an image object from a binary string blob which contains raw pixel data (i.e. no header data).
91
- #
92
- # === Returns
93
- #
94
- # * [Image] The loaded image.
95
- #
96
- # === Parameters
97
- #
98
- # * [blob] <tt>String</tt> -- Binary string blob containing raw pixel data.
99
- # * [columns] <tt>Integer</tt> -- Number of columns.
100
- # * [rows] <tt>Integer</tt> -- Number of rows.
101
- # * [depth] <tt>Integer</tt> -- Bit depth of the encoded pixel data.
102
- # * [map] <tt>String</tt> -- A code for the mapping of the pixel data. Example: 'gray' or 'rgb'.
103
- # * [format] <tt>String</tt> -- The file extension of the image format to be used when creating the image object. Defaults to 'png'.
104
- #
105
- def import_pixels(blob, columns, rows, depth, map, format="png")
106
- # Create an image object with the raw pixel data string:
107
- image = create(".dat", validate = false) { |f| f.write(blob) }
108
- # Use ImageMagick to convert the raw data file to an image file of the desired format:
109
- converted_image_path = image.path[0..-4] + format
110
- arguments = ["-size", "#{columns}x#{rows}", "-depth", "#{depth}", "#{map}:#{image.path}", "#{converted_image_path}"]
111
- cmd = CommandBuilder.new("convert", *arguments) #Example: convert -size 256x256 -depth 16 gray:blob.dat blob.png
112
- image.run(cmd)
113
- # Update the image instance with the path of the properly formatted image, and return:
114
- image.path = converted_image_path
115
- image
116
- end
117
-
118
- # Opens a specific image file either on the local file system or at a URI.
119
- #
120
- # Use this if you don't want to overwrite the image file.
121
- #
122
- # Extension is either guessed from the path or you can specify it as a second parameter.
123
- #
124
- # If you pass in what looks like a URL, we require 'open-uri' before opening it.
125
- #
126
- # @param file_or_url [String] Either a local file path or a URL that open-uri can read
127
- # @param ext [String] Specify the extension you want to read it as
128
- # @return [Image] The loaded image
129
- def open(file_or_url, ext = nil)
130
- file_or_url = file_or_url.to_s # Force it to be a String... hell or highwater
131
- if file_or_url.include?("://")
132
- require 'open-uri'
133
- ext ||= File.extname(URI.parse(file_or_url).path)
134
- self.read(Kernel::open(file_or_url), ext)
135
- else
136
- ext ||= File.extname(file_or_url)
137
- File.open(file_or_url, "rb") do |f|
138
- self.read(f, ext)
139
- end
140
- end
141
- end
142
-
143
- # @deprecated Please use MiniMagick::Image.open(file_or_url) now
144
- def from_file(file, ext = nil)
145
- warn "Warning: MiniMagick::Image.from_file is now deprecated. Please use Image.open"
146
- open(file, ext)
147
- end
148
-
149
- # Used to create a new Image object data-copy. Not used to "paint" or that kind of thing.
150
- #
151
- # Takes an extension in a block and can be used to build a new Image object. Used
152
- # by both #open and #read to create a new object! Ensures we have a good tempfile!
153
- #
154
- # @param ext [String] Specify the extension you want to read it as
155
- # @param validate [Boolean] If false, skips validation of the created image. Defaults to true.
156
- # @yield [IOStream] You can #write bits to this object to create the new Image
157
- # @return [Image] The created image
158
- def create(ext = nil, validate = true, &block)
159
- begin
160
- tempfile = Tempfile.new(['mini_magick', ext.to_s.downcase])
161
- tempfile.binmode
162
- block.call(tempfile)
163
- tempfile.close
164
-
165
- image = self.new(tempfile.path, tempfile)
166
-
167
- if validate and !image.valid?
168
- raise MiniMagick::Invalid
169
- end
170
- return image
171
- ensure
172
- tempfile.close if tempfile
173
- end
174
- end
175
- end
176
-
177
- # Create a new MiniMagick::Image object
178
- #
179
- # _DANGER_: The file location passed in here is the *working copy*. That is, it gets *modified*.
180
- # you can either copy it yourself or use the MiniMagick::Image.open(path) method which creates a
181
- # temporary file for you and protects your original!
58
+ ##
59
+ # Picks the right processor if it isn't set and returns whether it's mogrify or not.
182
60
  #
183
- # @param input_path [String] The location of an image file
184
- # @todo Allow this to accept a block that can pass off to Image#combine_options
185
- def initialize(input_path, tempfile = nil)
186
- @path = input_path
187
- @tempfile = tempfile # ensures that the tempfile will stick around until this image is garbage collected.
188
- end
61
+ # === Returns
62
+ # * [Boolean]
63
+ def mogrify?
64
+ self.choose_processor if self.processor.nil?
189
65
 
190
- # Checks to make sure that MiniMagick can read the file and understand it.
191
- #
192
- # This uses the 'identify' command line utility to check the file. If you are having
193
- # issues with this, then please work directly with the 'identify' command and see if you
194
- # can figure out what the issue is.
195
- #
196
- # @return [Boolean]
197
- def valid?
198
- run_command("identify", path)
199
- true
200
- rescue MiniMagick::Invalid
201
- false
202
- end
203
-
204
- # A rather low-level way to interact with the "identify" command. No nice API here, just
205
- # the crazy stuff you find in ImageMagick. See the examples listed!
206
- #
207
- # @example
208
- # image["format"] #=> "TIFF"
209
- # image["height"] #=> 41 (pixels)
210
- # image["width"] #=> 50 (pixels)
211
- # image["colorspace"] #=> "DirectClassRGB"
212
- # image["dimensions"] #=> [50, 41]
213
- # image["size"] #=> 2050 (bits)
214
- # image["original_at"] #=> 2005-02-23 23:17:24 +0000 (Read from Exif data)
215
- # image["EXIF:ExifVersion"] #=> "0220" (Can read anything from Exif)
216
- #
217
- # @param format [String] A format for the "identify" command
218
- # @see For reference see http://www.imagemagick.org/script/command-line-options.php#format
219
- # @return [String, Numeric, Array, Time, Object] Depends on the method called! Defaults to String for unknown commands
220
- def [](value)
221
- # Why do I go to the trouble of putting in newlines? Because otherwise animated gifs screw everything up
222
- case value.to_s
223
- when "colorspace"
224
- run_command("identify", "-format", '%r\n', path).split("\n")[0].strip
225
- when "format"
226
- run_command("identify", "-format", '%m\n', path).split("\n")[0]
227
- when "height"
228
- run_command("identify", "-format", '%h\n', path).split("\n")[0].to_i
229
- when "width"
230
- run_command("identify", "-format", '%w\n', path).split("\n")[0].to_i
231
- when "dimensions"
232
- run_command("identify", "-format", '%w %h\n', path).split("\n")[0].split.map{|v|v.to_i}
233
- when "size"
234
- File.size(path) # Do this because calling identify -format "%b" on an animated gif fails!
235
- when "original_at"
236
- # Get the EXIF original capture as a Time object
237
- Time.local(*self["EXIF:DateTimeOriginal"].split(/:|\s+/)) rescue nil
238
- when /^EXIF\:/i
239
- result = run_command('identify', '-format', "%[#{value}]", path).chop
240
- if result.include?(",")
241
- read_character_data(result)
242
- else
243
- result
244
- end
245
- else
246
- run_command('identify', '-format', value, path).split("\n")[0]
247
- end
248
- end
249
-
250
- # Sends raw commands to imagemagick's `mogrify` command. The image path is automatically appended to the command.
251
- #
252
- # Remember, we are always acting on this instance of the Image when messing with this.
253
- #
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)
257
- end
258
-
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!
261
- #
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...
265
- #
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.
269
- #
270
- # If you would like to convert between animated formats, pass nil as your
271
- # page and ImageMagick will copy all of the pages.
272
- #
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]
278
- def format(format, page = 0)
279
- c = CommandBuilder.new('mogrify', '-format', format)
280
- yield c if block_given?
281
- if page
282
- c << "#{path}[#{page}]"
283
- else
284
- c << path
285
- end
286
- run(c)
287
-
288
- old_path = path
289
- self.path = path.sub(/(\.\w*)?$/, ".#{format}")
290
- File.delete(old_path) if old_path != path
291
-
292
- unless File.exists?(path)
293
- raise MiniMagick::Error, "Unable to format to #{format}"
294
- end
66
+ self.processor == 'mogrify'
295
67
  end
296
68
 
297
- # Collapse images with sequences to the first frame (ie. animated gifs) and
298
- # preserve quality
299
- def collapse!
300
- run_command("mogrify", "-quality", "100", "#{path}[0]")
301
- end
302
-
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
69
+ ##
70
+ # Picks the right processor if it isn't set and returns whether it's graphicsmagick or not.
305
71
  #
306
- # @param output_to [IOStream, String] Some kind of stream object that needs to be read or a file path as a String
307
- # @return [IOStream, Boolean] If you pass in a file location [String] then you get a success boolean. If its a stream, you get it back.
308
- # Writes the temporary image that we are using for processing to the output path
309
- def write(output_to)
310
- if output_to.kind_of?(String) || !output_to.respond_to?(:write)
311
- FileUtils.copy_file path, output_to
312
- run_command "identify", output_to.to_s # Verify that we have a good image
313
- else # stream
314
- File.open(path, "rb") do |f|
315
- f.binmode
316
- while chunk = f.read(8192)
317
- output_to.write(chunk)
318
- end
319
- end
320
- output_to
321
- end
322
- end
323
-
324
- # Gives you raw image data back
325
- # @return [String] binary string
326
- def to_blob
327
- f = File.new path
328
- f.binmode
329
- f.read
330
- ensure
331
- f.close if f
332
- end
333
-
334
- def mime_type
335
- format = self[:format]
336
- "image/" + format.to_s.downcase
337
- end
338
-
339
- # If an unknown method is called then it is sent through the mogrify program
340
- # Look here to find all the commands (http://www.imagemagick.org/script/mogrify.php)
341
- def method_missing(symbol, *args)
342
- combine_options do |c|
343
- c.send(symbol, *args)
344
- end
345
- end
346
-
347
- # You can use multiple commands together using this method. Very easy to use!
348
- #
349
- # @example
350
- # image.combine_options do |c|
351
- # c.draw "image Over 0,0 10,10 '#{MINUS_IMAGE_PATH}'"
352
- # c.thumbnail "300x500>"
353
- # c.background background
354
- # end
355
- #
356
- # @yieldparam command [CommandBuilder]
357
- def combine_options(tool = "mogrify", &block)
358
- c = CommandBuilder.new(tool)
359
-
360
- c << path if tool.to_s == "convert"
361
- block.call(c)
362
- c << path
363
- run(c)
364
- end
365
-
366
- def composite(other_image, output_extension = 'jpg', &block)
367
- begin
368
- second_tempfile = Tempfile.new(output_extension)
369
- second_tempfile.binmode
370
- ensure
371
- second_tempfile.close
372
- end
373
-
374
- command = CommandBuilder.new("composite")
375
- block.call(command) if block
376
- command.push(other_image.path)
377
- command.push(self.path)
378
- command.push(second_tempfile.path)
379
-
380
- run(command)
381
- return Image.new(second_tempfile.path, second_tempfile)
382
- end
383
-
384
- def run_command(command, *args)
385
- # -ping "efficiently determine image characteristics."
386
- if command == 'identify'
387
- args.unshift '-ping'
388
- args.unshift '-quiet' unless MiniMagick.processor.to_s == 'gm'
389
- end
390
-
391
- run(CommandBuilder.new(command, *args))
392
- end
393
-
394
- def run(command_builder)
395
- command = command_builder.command
396
-
397
- sub = Subexec.run(command, :timeout => MiniMagick.timeout)
398
-
399
- if sub.exitstatus != 0
400
- # Clean up after ourselves in case of an error
401
- destroy!
402
-
403
- # Raise the appropriate error
404
- if sub.output =~ /no decode delegate/i || sub.output =~ /did not return an image/i
405
- raise Invalid, sub.output
406
- else
407
- # TODO: should we do something different if the command times out ...?
408
- # its definitely better for logging.. otherwise we dont really know
409
- raise Error, "Command (#{command.inspect.gsub("\\", "")}) failed: #{{:status_code => sub.exitstatus, :output => sub.output}.inspect}"
410
- end
411
- else
412
- sub.output
413
- end
414
- end
415
-
416
- def destroy!
417
- return if @tempfile.nil?
418
- File.unlink(@tempfile.path) if File.exists?(@tempfile.path)
419
- @tempfile = nil
420
- end
421
-
422
- private
423
- # Sometimes we get back a list of character values
424
- def read_character_data(list_of_characters)
425
- chars = list_of_characters.gsub(" ", "").split(",")
426
- result = ""
427
- chars.each do |val|
428
- result << ("%c" % val.to_i)
429
- end
430
- result
431
- end
432
- end
433
-
434
- class CommandBuilder
435
- def initialize(tool, *options)
436
- @tool = tool
437
- @args = []
438
- options.each { |arg| push(arg) }
439
- end
440
-
441
- def command
442
- com = "#{@tool} #{args.join(' ')}".strip
443
- com = "#{MiniMagick.processor} #{com}" unless MiniMagick.processor.nil?
444
-
445
- com = File.join MiniMagick.processor_path, com unless MiniMagick.processor_path.nil?
446
- com.strip
447
- end
448
-
449
- def args
450
- @args.map(&:shellescape)
451
- end
452
-
453
- # Add each mogrify command in both underscore and dash format
454
- MOGRIFY_COMMANDS.each do |mogrify_command|
455
-
456
- # Example of what is generated here:
457
- #
458
- # def auto_orient(*options)
459
- # add_command("auto-orient", *options)
460
- # self
461
- # end
462
- # alias_method :"auto-orient", :auto_orient
463
-
464
- dashed_command = mogrify_command.to_s.gsub("_","-")
465
- underscored_command = mogrify_command.to_s.gsub("-","_")
466
-
467
- define_method(underscored_command) do |*options|
468
- add_command(__method__.to_s.gsub("_","-"), *options)
469
- self
470
- end
471
- alias_method dashed_command, underscored_command
472
- end
473
-
474
- def format(*options)
475
- raise Error, "You must call 'format' on the image object directly!"
476
- end
477
-
478
- IMAGE_CREATION_OPERATORS.each do |operator|
479
- define_method operator do |*options|
480
- add_creation_operator(__method__.to_s, *options)
481
- self
482
- end
483
- end
484
-
485
- def +(*options)
486
- push(@args.pop.gsub(/^-/, '+'))
487
- if options.any?
488
- options.each do |o|
489
- push o
490
- end
491
- end
492
- end
493
-
494
- def add_command(command, *options)
495
- push "-#{command}"
496
- if options.any?
497
- options.each do |o|
498
- push o
499
- end
500
- end
501
- end
502
-
503
- def add_creation_operator(command, *options)
504
- creation_command = command
505
- if options.any?
506
- options.each do |option|
507
- creation_command << ":#{option}"
508
- end
509
- end
510
- push creation_command
511
- end
72
+ # === Returns
73
+ # * [Boolean]
74
+ def gm?
75
+ self.choose_processor if self.processor.nil?
512
76
 
513
- def push(arg)
514
- @args << arg.to_s.strip
77
+ self.processor == 'gm'
515
78
  end
516
- alias :<< :push
517
79
  end
518
80
  end