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.

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