mini_magick 4.7.2 → 4.13.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,24 +5,23 @@ module MiniMagick
5
5
  module Configuration
6
6
 
7
7
  ##
8
- # Set whether you want to use [ImageMagick](http://www.imagemagick.org) or
9
- # [GraphicsMagick](http://www.graphicsmagick.org).
10
- #
11
- # @return [Symbol] `:imagemagick` or `:graphicsmagick`
8
+ # If you don't have the CLI tools in your PATH, you can set the path to the
9
+ # executables.
12
10
  #
13
- attr_accessor :cli
11
+ attr_writer :cli_path
14
12
  # @private (for backwards compatibility)
15
- attr_accessor :processor
13
+ attr_accessor :processor_path
16
14
 
17
15
  ##
18
- # If you don't have the CLI tools in your PATH, you can set the path to the
19
- # executables.
16
+ # Adds a prefix to the CLI command.
17
+ # For example, you could use `firejail` to run all commands in a sandbox.
18
+ # Can be a string, or an array of strings.
19
+ # e.g. 'firejail', or ['firejail', '--force']
20
20
  #
21
21
  # @return [String]
22
+ # @return [Array<String>]
22
23
  #
23
- attr_accessor :cli_path
24
- # @private (for backwards compatibility)
25
- attr_accessor :processor_path
24
+ attr_accessor :cli_prefix
26
25
 
27
26
  ##
28
27
  # If you don't want commands to take too long, you can set a timeout (in
@@ -32,12 +31,12 @@ module MiniMagick
32
31
  #
33
32
  attr_accessor :timeout
34
33
  ##
35
- # When set to `true`, it outputs each command to STDOUT in their shell
34
+ # When get to `true`, it outputs each command to STDOUT in their shell
36
35
  # version.
37
36
  #
38
37
  # @return [Boolean]
39
38
  #
40
- attr_accessor :debug
39
+ attr_reader :debug
41
40
  ##
42
41
  # Logger for {#debug}, default is `MiniMagick::Logger.new(STDOUT)`, but
43
42
  # you can override it, for example if you want the logs to be written to
@@ -46,6 +45,13 @@ module MiniMagick
46
45
  # @return [Logger]
47
46
  #
48
47
  attr_accessor :logger
48
+ ##
49
+ # Temporary directory used by MiniMagick, default is `Dir.tmpdir`, but
50
+ # you can override it.
51
+ #
52
+ # @return [String]
53
+ #
54
+ attr_accessor :tmpdir
49
55
 
50
56
  ##
51
57
  # If set to `true`, it will `identify` every newly created image, and raise
@@ -58,7 +64,7 @@ module MiniMagick
58
64
  ##
59
65
  # If set to `true`, it will `identify` every image that gets written (with
60
66
  # {MiniMagick::Image#write}), and raise `MiniMagick::Invalid` if the image
61
- # is not valid. Useful for validating that processing was sucessful,
67
+ # is not valid. Useful for validating that processing was successful,
62
68
  # although it adds a bit of overhead. Defaults to `true`.
63
69
  #
64
70
  # @return [Boolean]
@@ -74,20 +80,17 @@ module MiniMagick
74
80
  attr_accessor :whiny
75
81
 
76
82
  ##
77
- # Instructs MiniMagick how to execute the shell commands. Available
78
- # APIs are "open3" (default) and "posix-spawn" (requires the "posix-spawn"
79
- # gem).
80
- #
81
- # @return [String]
82
- #
83
- attr_accessor :shell_api
83
+ # If set to `false`, it will not forward warnings from ImageMagick to
84
+ # standard error.
85
+ attr_accessor :warnings
84
86
 
85
87
  def self.extended(base)
88
+ base.tmpdir = Dir.tmpdir
86
89
  base.validate_on_create = true
87
90
  base.validate_on_write = true
88
91
  base.whiny = true
89
- base.shell_api = "open3"
90
92
  base.logger = Logger.new($stdout).tap { |l| l.level = Logger::INFO }
93
+ base.warnings = true
91
94
  end
92
95
 
93
96
  ##
@@ -102,51 +105,95 @@ module MiniMagick
102
105
  yield self
103
106
  end
104
107
 
108
+ CLI_DETECTION = {
109
+ imagemagick7: "magick",
110
+ imagemagick: "mogrify",
111
+ graphicsmagick: "gm",
112
+ }
113
+
114
+ # @private (for backwards compatibility)
105
115
  def processor
106
- @processor ||= ["mogrify", "gm"].detect do |processor|
116
+ @processor ||= CLI_DETECTION.values.detect do |processor|
107
117
  MiniMagick::Utilities.which(processor)
108
118
  end
109
119
  end
110
120
 
121
+ # @private (for backwards compatibility)
111
122
  def processor=(processor)
112
123
  @processor = processor.to_s
113
124
 
114
- unless ["mogrify", "gm"].include?(@processor)
125
+ unless CLI_DETECTION.value?(@processor)
115
126
  raise ArgumentError,
116
- "processor has to be set to either \"mogrify\" or \"gm\"" \
127
+ "processor has to be set to either \"magick\", \"mogrify\" or \"gm\"" \
117
128
  ", was set to #{@processor.inspect}"
118
129
  end
119
130
  end
120
131
 
132
+ ##
133
+ # Get [ImageMagick](http://www.imagemagick.org) or
134
+ # [GraphicsMagick](http://www.graphicsmagick.org).
135
+ #
136
+ # @return [Symbol] `:imagemagick` or `:graphicsmagick`
137
+ #
121
138
  def cli
122
- @cli ||
123
- case processor.to_s
124
- when "mogrify" then :imagemagick
125
- when "gm" then :graphicsmagick
126
- else
127
- raise MiniMagick::Error, "ImageMagick/GraphicsMagick is not installed"
128
- end
139
+ if instance_variable_defined?("@cli")
140
+ instance_variable_get("@cli")
141
+ else
142
+ cli = CLI_DETECTION.key(processor) or
143
+ fail MiniMagick::Error, "You must have ImageMagick or GraphicsMagick installed"
144
+
145
+ instance_variable_set("@cli", cli)
146
+ end
129
147
  end
130
148
 
149
+ ##
150
+ # Set whether you want to use [ImageMagick](http://www.imagemagick.org) or
151
+ # [GraphicsMagick](http://www.graphicsmagick.org).
152
+ #
131
153
  def cli=(value)
132
154
  @cli = value
133
155
 
134
- if not [:imagemagick, :graphicsmagick].include?(@cli)
156
+ if not CLI_DETECTION.key?(@cli)
135
157
  raise ArgumentError,
136
- "CLI has to be set to either :imagemagick or :graphicsmagick" \
158
+ "CLI has to be set to either :imagemagick, :imagemagick7 or :graphicsmagick" \
137
159
  ", was set to #{@cli.inspect}"
138
160
  end
139
161
  end
140
162
 
163
+ ##
164
+ # If you set the path of CLI tools, you can get the path of the
165
+ # executables.
166
+ #
167
+ # @return [String]
168
+ #
141
169
  def cli_path
142
- @cli_path || @processor_path
170
+ if instance_variable_defined?("@cli_path")
171
+ instance_variable_get("@cli_path")
172
+ else
173
+ processor_path = instance_variable_get("@processor_path") if instance_variable_defined?("@processor_path")
174
+
175
+ instance_variable_set("@cli_path", processor_path)
176
+ end
143
177
  end
144
178
 
179
+ ##
180
+ # When set to `true`, it outputs each command to STDOUT in their shell
181
+ # version.
182
+ #
145
183
  def debug=(value)
146
184
  warn "MiniMagick.debug is deprecated and will be removed in MiniMagick 5. Use `MiniMagick.logger.level = Logger::DEBUG` instead."
147
185
  logger.level = value ? Logger::DEBUG : Logger::INFO
148
186
  end
149
187
 
188
+ def shell_api=(value)
189
+ warn "MiniMagick.shell_api is deprecated and will be removed in MiniMagick 5. The posix-spawn gem doesn't improve performance recent Ruby versions anymore, so support for it will be removed."
190
+ @shell_api = value
191
+ end
192
+
193
+ def shell_api
194
+ @shell_api || "open3"
195
+ end
196
+
150
197
  # Backwards compatibility
151
198
  def reload_tools
152
199
  warn "MiniMagick.reload_tools is deprecated because it is no longer necessary"
@@ -42,7 +42,7 @@ module MiniMagick
42
42
 
43
43
  def cheap_info(value)
44
44
  @info.fetch(value) do
45
- format, width, height, size = self["%m %w %h %b"].split(" ")
45
+ format, width, height, size = parse_warnings(self["%m %w %h %b"]).split(" ")
46
46
 
47
47
  path = @path
48
48
  path = path.match(/\[\d+\]$/).pre_match if path =~ /\[\d+\]$/
@@ -62,11 +62,23 @@ module MiniMagick
62
62
  raise MiniMagick::Invalid, "image data can't be read"
63
63
  end
64
64
 
65
+ def parse_warnings(raw_info)
66
+ return raw_info unless raw_info.split("\n").size > 1
67
+
68
+ raw_info.split("\n").each do |line|
69
+ # must match "%m %w %h %b"
70
+ return line if line.match?(/^[A-Z]+ \d+ \d+ \d+(|\.\d+)([KMGTPEZY]{0,1})B$/)
71
+ end
72
+ raise TypeError
73
+ end
74
+
65
75
  def colorspace
66
76
  @info["colorspace"] ||= self["%r"]
67
77
  end
68
78
 
69
79
  def mime_type
80
+ warn "[MiniMagick] MiniMagick::Image#mime_type has been deprecated, because it wasn't returning correct result for all formats ImageMagick supports. Unfortunately, returning the correct MIME type would be very slow, because it would require ImageMagick to read the whole file. It's better to use Marcel and MimeMagic gems, which are able to determine the MIME type just from the image header."
81
+
70
82
  "image/#{self["format"].downcase}"
71
83
  end
72
84
 
@@ -91,7 +103,7 @@ module MiniMagick
91
103
  line = line.chomp("\n")
92
104
 
93
105
  case MiniMagick.cli
94
- when :imagemagick
106
+ when :imagemagick, :imagemagick7
95
107
  if match = line.match(/^exif:/)
96
108
  key, value = match.post_match.split("=", 2)
97
109
  value = decode_comma_separated_ascii_characters(value) if ASCII_ENCODED_EXIF_KEYS.include?(key)
@@ -100,6 +112,7 @@ module MiniMagick
100
112
  hash[hash.keys.last] << "\n#{line}"
101
113
  end
102
114
  when :graphicsmagick
115
+ next if line == "unknown"
103
116
  key, value = line.split("=", 2)
104
117
  value.gsub!("\\012", "\n") # convert "\012" characters to newlines
105
118
  hash[key] = value
@@ -119,7 +132,7 @@ module MiniMagick
119
132
  end
120
133
 
121
134
  def details
122
- warn "[MiniMagick] MiniMagick::Image#details has been deprecated, as it was causing too many parsing errors. You should use MiniMagick::Image#data instead, which differs in a way that the keys are in camelcase." if MiniMagick.imagemagick?
135
+ warn "[MiniMagick] MiniMagick::Image#details has been deprecated, as it was causing too many parsing errors. You should use MiniMagick::Image#data instead, which differs in a way that the keys are in camelcase." if MiniMagick.imagemagick? || MiniMagick.imagemagick7?
123
136
 
124
137
  @info["details"] ||= (
125
138
  details_string = identify(&:verbose)
@@ -139,8 +152,8 @@ module MiniMagick
139
152
  next
140
153
  end
141
154
 
142
- key, _, value = line.partition(/:[\s\n]/).map(&:strip)
143
- hash = key_stack.inject(details_hash) { |hash, key| hash.fetch(key) }
155
+ key, _, value = line.partition(/:[\s]/).map(&:strip)
156
+ hash = key_stack.inject(details_hash) { |_hash, _key| _hash.fetch(_key) }
144
157
  if value.empty?
145
158
  hash[key] = {}
146
159
  key_stack.push key
@@ -15,7 +15,7 @@ module MiniMagick
15
15
  # methods.
16
16
  #
17
17
  # Use this to pass in a stream object. Must respond to #read(size) or be a
18
- # binary string object (BLOBBBB)
18
+ # binary string object (BLOB)
19
19
  #
20
20
  # Probably easier to use the {.open} method if you want to open a file or a
21
21
  # URL.
@@ -76,20 +76,36 @@ module MiniMagick
76
76
  # @param path_or_url [String] Either a local file path or a URL that
77
77
  # open-uri can read
78
78
  # @param ext [String] Specify the extension you want to read it as
79
+ # @param options [Hash] Specify options for the open method
79
80
  # @return [MiniMagick::Image] The loaded image
80
81
  #
81
- def self.open(path_or_url, ext = nil)
82
- ext ||=
83
- if File.exist?(path_or_url)
84
- File.extname(path_or_url)
82
+ def self.open(path_or_url, ext = nil, options = {})
83
+ options, ext = ext, nil if ext.is_a?(Hash)
84
+
85
+ # Don't use Kernel#open, but reuse its logic
86
+ openable =
87
+ if path_or_url.respond_to?(:open)
88
+ path_or_url
89
+ elsif path_or_url.respond_to?(:to_str) &&
90
+ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ path_or_url &&
91
+ (uri = URI.parse(path_or_url)).respond_to?(:open)
92
+ uri
85
93
  else
86
- File.extname(URI(path_or_url).path)
94
+ options = { binmode: true }.merge(options)
95
+ Pathname(path_or_url)
87
96
  end
88
97
 
98
+ if openable.is_a?(URI::Generic)
99
+ ext ||= File.extname(openable.path)
100
+ else
101
+ ext ||= File.extname(openable.to_s)
102
+ end
89
103
  ext.sub!(/:.*/, '') # hack for filenames or URLs that include a colon
90
104
 
91
- Kernel.open(path_or_url, "rb") do |file|
92
- read(file, ext)
105
+ if openable.is_a?(URI::Generic)
106
+ openable.open(options) { |file| read(file, ext) }
107
+ else
108
+ openable.open(**options) { |file| read(file, ext) }
93
109
  end
94
110
  end
95
111
 
@@ -258,7 +274,7 @@ module MiniMagick
258
274
  #
259
275
  attribute :resolution
260
276
  ##
261
- # Returns the message digest of this image as a SHA-256, hexidecimal
277
+ # Returns the message digest of this image as a SHA-256, hexadecimal
262
278
  # encoded string. This signature uniquely identifies the image and is
263
279
  # convenient for determining if an image has been modified or whether two
264
280
  # images are identical.
@@ -324,13 +340,18 @@ module MiniMagick
324
340
  #
325
341
  # 1) one for each row of pixels
326
342
  # 2) one for each column of pixels
327
- # 3) three elements in the range 0-255, one for each of the RGB color channels
343
+ # 3) three or four elements in the range 0-255, one for each of the RGB(A) color channels
328
344
  #
329
345
  # @example
330
346
  # img = MiniMagick::Image.open 'image.jpg'
331
347
  # pixels = img.get_pixels
332
348
  # pixels[3][2][1] # the green channel value from the 4th-row, 3rd-column pixel
333
349
  #
350
+ # @example
351
+ # img = MiniMagick::Image.open 'image.jpg'
352
+ # pixels = img.get_pixels("RGBA")
353
+ # pixels[3][2][3] # the alpha channel value from the 4th-row, 3rd-column pixel
354
+ #
334
355
  # It can also be called after applying transformations:
335
356
  #
336
357
  # @example
@@ -341,16 +362,22 @@ module MiniMagick
341
362
  #
342
363
  # In this example, all pixels in pix should now have equal R, G, and B values.
343
364
  #
365
+ # @param map [String] A code for the mapping of the pixel data. Must be either
366
+ # 'RGB' or 'RGBA'. Default to 'RGB'
344
367
  # @return [Array] Matrix of each color of each pixel
345
- def get_pixels
346
- output = MiniMagick::Tool::Convert.new do |convert|
347
- convert << path
348
- convert.depth(8)
349
- convert << "RGB:-"
350
- end
368
+ def get_pixels(map="RGB")
369
+ raise ArgumentError, "Invalid map value" unless ["RGB", "RGBA"].include?(map)
370
+ convert = MiniMagick::Tool::Convert.new
371
+ convert << path
372
+ convert.depth(8)
373
+ convert << "#{map}:-"
374
+
375
+ # Do not use `convert.call` here. We need the whole binary (unstripped) output here.
376
+ shell = MiniMagick::Shell.new
377
+ output, * = shell.run(convert.command)
351
378
 
352
379
  pixels_array = output.unpack("C*")
353
- pixels = pixels_array.each_slice(3).each_slice(width).to_a
380
+ pixels = pixels_array.each_slice(map.length).each_slice(width).to_a
354
381
 
355
382
  # deallocate large intermediary objects
356
383
  output.clear
@@ -359,6 +386,23 @@ module MiniMagick
359
386
  pixels
360
387
  end
361
388
 
389
+ ##
390
+ # This is used to create image from pixels. This might be required if you
391
+ # create pixels for some image processing reasons and you want to form
392
+ # image from those pixels.
393
+ #
394
+ # *DANGER*: This operation can be very expensive. Please try to use with
395
+ # caution.
396
+ #
397
+ # @example
398
+ # # It is given in readme.md file
399
+ ##
400
+ def self.get_image_from_pixels(pixels, dimension, map, depth, mime_type)
401
+ pixels = pixels.flatten
402
+ blob = pixels.pack('C*')
403
+ import_pixels(blob, *dimension, depth, map, mime_type)
404
+ end
405
+
362
406
  ##
363
407
  # This is used to change the format of the image. That is, from "tiff to
364
408
  # jpg" or something like that. Once you run it, the instance is pointing to
@@ -417,6 +461,9 @@ module MiniMagick
417
461
  @info.clear
418
462
 
419
463
  self
464
+ rescue MiniMagick::Invalid, MiniMagick::Error => e
465
+ new_tempfile.unlink if new_tempfile && @tempfile != new_tempfile
466
+ raise e
420
467
  end
421
468
 
422
469
  ##
@@ -568,5 +615,31 @@ module MiniMagick
568
615
  def layer?
569
616
  path =~ /\[\d+\]$/
570
617
  end
618
+
619
+ ##
620
+ # Compares if image width
621
+ # is greater than height
622
+ # ============
623
+ # | |
624
+ # | |
625
+ # ============
626
+ # @return [Boolean]
627
+ def landscape?
628
+ width > height
629
+ end
630
+
631
+ ##
632
+ # Compares if image height
633
+ # is greater than width
634
+ # ======
635
+ # | |
636
+ # | |
637
+ # | |
638
+ # | |
639
+ # ======
640
+ # @return [Boolean]
641
+ def portrait?
642
+ height > width
643
+ end
571
644
  end
572
645
  end
@@ -14,9 +14,10 @@ module MiniMagick
14
14
  stdout, stderr, status = execute(command, stdin: options[:stdin])
15
15
 
16
16
  if status != 0 && options.fetch(:whiny, MiniMagick.whiny)
17
- fail MiniMagick::Error, "`#{command.join(" ")}` failed with error:\n#{stderr}"
17
+ fail MiniMagick::Error, "`#{command.join(" ")}` failed with status: #{status.inspect} and error:\n#{stderr}"
18
18
  end
19
19
 
20
+ stderr = stderr.lines[2..-1].join if stderr.start_with? %(WARNING: The convert command is deprecated in IMv7)
20
21
  $stderr.print(stderr) unless options[:stderr] == false
21
22
 
22
23
  [stdout, stderr, status]
@@ -25,10 +26,10 @@ module MiniMagick
25
26
  def execute(command, options = {})
26
27
  stdout, stderr, status =
27
28
  log(command.join(" ")) do
28
- send("execute_#{MiniMagick.shell_api.gsub("-", "_")}", command, options)
29
+ send("execute_#{MiniMagick.shell_api.tr("-", "_")}", command, options)
29
30
  end
30
31
 
31
- [stdout, stderr, status.exitstatus]
32
+ [stdout, stderr, status&.exitstatus]
32
33
  rescue Errno::ENOENT, IOError
33
34
  ["", "executable not found: \"#{command.first}\"", 127]
34
35
  end
@@ -38,38 +39,34 @@ module MiniMagick
38
39
  def execute_open3(command, options = {})
39
40
  require "open3"
40
41
 
41
- in_w, out_r, err_r, subprocess_thread = Open3.popen3(*command)
42
+ # We would ideally use Open3.capture3, but it wouldn't allow us to
43
+ # terminate the command after timing out.
44
+ Open3.popen3(*command) do |in_w, out_r, err_r, thread|
45
+ [in_w, out_r, err_r].each(&:binmode)
46
+ stdout_reader = Thread.new { out_r.read }
47
+ stderr_reader = Thread.new { err_r.read }
48
+ begin
49
+ in_w.write options[:stdin].to_s
50
+ rescue Errno::EPIPE
51
+ end
52
+ in_w.close
53
+
54
+ unless thread.join(MiniMagick.timeout)
55
+ Process.kill("TERM", thread.pid) rescue nil
56
+ Process.waitpid(thread.pid) rescue nil
57
+ raise Timeout::Error, "MiniMagick command timed out: #{command}"
58
+ end
42
59
 
43
- capture_command(in_w, out_r, err_r, subprocess_thread, options)
60
+ [stdout_reader.value, stderr_reader.value, thread.value]
61
+ end
44
62
  end
45
63
 
46
64
  def execute_posix_spawn(command, options = {})
47
65
  require "posix-spawn"
48
-
49
- pid, in_w, out_r, err_r = POSIX::Spawn.popen4(*command)
50
- subprocess_thread = Process.detach(pid)
51
-
52
- capture_command(in_w, out_r, err_r, subprocess_thread, options)
53
- end
54
-
55
- def capture_command(in_w, out_r, err_r, subprocess_thread, options)
56
- [in_w, out_r, err_r].each(&:binmode)
57
- stdout_reader = Thread.new { out_r.read }
58
- stderr_reader = Thread.new { err_r.read }
59
- begin
60
- in_w.write options[:stdin].to_s
61
- rescue Errno::EPIPE
62
- end
63
- in_w.close
64
-
65
- Timeout.timeout(MiniMagick.timeout) { subprocess_thread.join }
66
-
67
- [stdout_reader.value, stderr_reader.value, subprocess_thread.value]
68
- rescue Timeout::Error => error
69
- Process.kill("TERM", subprocess_thread.pid)
70
- raise error
71
- ensure
72
- [out_r, err_r].each(&:close)
66
+ child = POSIX::Spawn::Child.new(*command, input: options[:stdin].to_s, timeout: MiniMagick.timeout)
67
+ [child.out, child.err, child.status]
68
+ rescue POSIX::Spawn::TimeoutExceeded
69
+ raise Timeout::Error, "MiniMagick command timed out: #{command}"
73
70
  end
74
71
 
75
72
  def log(command, &block)
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/command-line-processing.php
5
+ #
6
+ class Magick < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("magick", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end