mini_magick 4.12.0 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,14 +4,6 @@ require 'logger'
4
4
  module MiniMagick
5
5
  module Configuration
6
6
 
7
- ##
8
- # If you don't have the CLI tools in your PATH, you can set the path to the
9
- # executables.
10
- #
11
- attr_writer :cli_path
12
- # @private (for backwards compatibility)
13
- attr_accessor :processor_path
14
-
15
7
  ##
16
8
  # Adds a prefix to the CLI command.
17
9
  # For example, you could use `firejail` to run all commands in a sandbox.
@@ -31,16 +23,8 @@ module MiniMagick
31
23
  #
32
24
  attr_accessor :timeout
33
25
  ##
34
- # When get to `true`, it outputs each command to STDOUT in their shell
35
- # version.
36
- #
37
- # @return [Boolean]
38
- #
39
- attr_reader :debug
40
- ##
41
- # Logger for {#debug}, default is `MiniMagick::Logger.new(STDOUT)`, but
42
- # you can override it, for example if you want the logs to be written to
43
- # a file.
26
+ # Logger for commands, default is `Logger.new($stdout)`, but you can
27
+ # override it, for example if you want the logs to be written to a file.
44
28
  #
45
29
  # @return [Logger]
46
30
  #
@@ -53,146 +37,35 @@ module MiniMagick
53
37
  #
54
38
  attr_accessor :tmpdir
55
39
 
56
- ##
57
- # If set to `true`, it will `identify` every newly created image, and raise
58
- # `MiniMagick::Invalid` if the image is not valid. Useful for validating
59
- # user input, although it adds a bit of overhead. Defaults to `true`.
60
- #
61
- # @return [Boolean]
62
- #
63
- attr_accessor :validate_on_create
64
- ##
65
- # If set to `true`, it will `identify` every image that gets written (with
66
- # {MiniMagick::Image#write}), and raise `MiniMagick::Invalid` if the image
67
- # is not valid. Useful for validating that processing was sucessful,
68
- # although it adds a bit of overhead. Defaults to `true`.
69
- #
70
- # @return [Boolean]
71
- #
72
- attr_accessor :validate_on_write
73
-
74
40
  ##
75
41
  # If set to `false`, it will not raise errors when ImageMagick returns
76
42
  # status code different than 0. Defaults to `true`.
77
43
  #
78
44
  # @return [Boolean]
79
45
  #
80
- attr_accessor :whiny
46
+ attr_accessor :errors
81
47
 
82
48
  ##
83
- # Instructs MiniMagick how to execute the shell commands. Available
84
- # APIs are "open3" (default) and "posix-spawn" (requires the "posix-spawn"
85
- # gem).
86
- #
87
- # @return [String]
88
- #
89
- attr_accessor :shell_api
49
+ # If set to `false`, it will not forward warnings from ImageMagick to
50
+ # standard error.
51
+ attr_accessor :warnings
90
52
 
91
53
  def self.extended(base)
92
54
  base.tmpdir = Dir.tmpdir
93
- base.validate_on_create = true
94
- base.validate_on_write = true
95
- base.whiny = true
96
- base.shell_api = "open3"
55
+ base.errors = true
97
56
  base.logger = Logger.new($stdout).tap { |l| l.level = Logger::INFO }
57
+ base.warnings = true
98
58
  end
99
59
 
100
60
  ##
101
61
  # @yield [self]
102
62
  # @example
103
63
  # MiniMagick.configure do |config|
104
- # config.cli = :graphicsmagick
105
64
  # config.timeout = 5
106
65
  # end
107
66
  #
108
67
  def configure
109
68
  yield self
110
69
  end
111
-
112
- CLI_DETECTION = {
113
- imagemagick7: "magick",
114
- imagemagick: "mogrify",
115
- graphicsmagick: "gm",
116
- }
117
-
118
- # @private (for backwards compatibility)
119
- def processor
120
- @processor ||= CLI_DETECTION.values.detect do |processor|
121
- MiniMagick::Utilities.which(processor)
122
- end
123
- end
124
-
125
- # @private (for backwards compatibility)
126
- def processor=(processor)
127
- @processor = processor.to_s
128
-
129
- unless CLI_DETECTION.value?(@processor)
130
- raise ArgumentError,
131
- "processor has to be set to either \"magick\", \"mogrify\" or \"gm\"" \
132
- ", was set to #{@processor.inspect}"
133
- end
134
- end
135
-
136
- ##
137
- # Get [ImageMagick](http://www.imagemagick.org) or
138
- # [GraphicsMagick](http://www.graphicsmagick.org).
139
- #
140
- # @return [Symbol] `:imagemagick` or `:graphicsmagick`
141
- #
142
- def cli
143
- if instance_variable_defined?("@cli")
144
- instance_variable_get("@cli")
145
- else
146
- cli = CLI_DETECTION.key(processor) or
147
- fail MiniMagick::Error, "You must have ImageMagick or GraphicsMagick installed"
148
-
149
- instance_variable_set("@cli", cli)
150
- end
151
- end
152
-
153
- ##
154
- # Set whether you want to use [ImageMagick](http://www.imagemagick.org) or
155
- # [GraphicsMagick](http://www.graphicsmagick.org).
156
- #
157
- def cli=(value)
158
- @cli = value
159
-
160
- if not CLI_DETECTION.key?(@cli)
161
- raise ArgumentError,
162
- "CLI has to be set to either :imagemagick, :imagemagick7 or :graphicsmagick" \
163
- ", was set to #{@cli.inspect}"
164
- end
165
- end
166
-
167
- ##
168
- # If you set the path of CLI tools, you can get the path of the
169
- # executables.
170
- #
171
- # @return [String]
172
- #
173
- def cli_path
174
- if instance_variable_defined?("@cli_path")
175
- instance_variable_get("@cli_path")
176
- else
177
- processor_path = instance_variable_get("@processor_path") if instance_variable_defined?("@processor_path")
178
-
179
- instance_variable_set("@cli_path", processor_path)
180
- end
181
- end
182
-
183
- ##
184
- # When set to `true`, it outputs each command to STDOUT in their shell
185
- # version.
186
- #
187
- def debug=(value)
188
- warn "MiniMagick.debug is deprecated and will be removed in MiniMagick 5. Use `MiniMagick.logger.level = Logger::DEBUG` instead."
189
- logger.level = value ? Logger::DEBUG : Logger::INFO
190
- end
191
-
192
- # Backwards compatibility
193
- def reload_tools
194
- warn "MiniMagick.reload_tools is deprecated because it is no longer necessary"
195
- end
196
-
197
70
  end
198
71
  end
@@ -17,8 +17,6 @@ module MiniMagick
17
17
  cheap_info(value)
18
18
  when "colorspace"
19
19
  colorspace
20
- when "mime_type"
21
- mime_type
22
20
  when "resolution"
23
21
  resolution(*args)
24
22
  when "signature"
@@ -27,8 +25,6 @@ module MiniMagick
27
25
  raw_exif(value)
28
26
  when "exif"
29
27
  exif
30
- when "details"
31
- details
32
28
  when "data"
33
29
  data
34
30
  else
@@ -76,10 +72,6 @@ module MiniMagick
76
72
  @info["colorspace"] ||= self["%r"]
77
73
  end
78
74
 
79
- def mime_type
80
- "image/#{self["format"].downcase}"
81
- end
82
-
83
75
  def resolution(unit = nil)
84
76
  output = identify do |b|
85
77
  b.units unit if unit
@@ -100,20 +92,12 @@ module MiniMagick
100
92
  output.each_line do |line|
101
93
  line = line.chomp("\n")
102
94
 
103
- case MiniMagick.cli
104
- when :imagemagick, :imagemagick7
105
- if match = line.match(/^exif:/)
106
- key, value = match.post_match.split("=", 2)
107
- value = decode_comma_separated_ascii_characters(value) if ASCII_ENCODED_EXIF_KEYS.include?(key)
108
- hash[key] = value
109
- else
110
- hash[hash.keys.last] << "\n#{line}"
111
- end
112
- when :graphicsmagick
113
- next if line == "unknown"
114
- key, value = line.split("=", 2)
115
- value.gsub!("\\012", "\n") # convert "\012" characters to newlines
95
+ if match = line.match(/^exif:/)
96
+ key, value = match.post_match.split("=", 2)
97
+ value = decode_comma_separated_ascii_characters(value) if ASCII_ENCODED_EXIF_KEYS.include?(key)
116
98
  hash[key] = value
99
+ else
100
+ hash[hash.keys.last] << "\n#{line}"
117
101
  end
118
102
  end
119
103
 
@@ -129,44 +113,9 @@ module MiniMagick
129
113
  @info["signature"] ||= self["%#"]
130
114
  end
131
115
 
132
- def details
133
- 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?
134
-
135
- @info["details"] ||= (
136
- details_string = identify(&:verbose)
137
- key_stack = []
138
- details_string.lines.to_a[1..-1].each_with_object({}) do |line, details_hash|
139
- next if !line.valid_encoding? || line.strip.length.zero?
140
-
141
- level = line[/^\s*/].length / 2 - 1
142
- if level >= 0
143
- key_stack.pop until key_stack.size <= level
144
- else
145
- # Some metadata, such as SVG clipping paths, will be saved without
146
- # indentation, resulting in a level of -1
147
- last_key = details_hash.keys.last
148
- details_hash[last_key] = '' if details_hash[last_key].empty?
149
- details_hash[last_key] << line
150
- next
151
- end
152
-
153
- key, _, value = line.partition(/:[\s]/).map(&:strip)
154
- hash = key_stack.inject(details_hash) { |_hash, _key| _hash.fetch(_key) }
155
- if value.empty?
156
- hash[key] = {}
157
- key_stack.push key
158
- else
159
- hash[key] = value
160
- end
161
- end
162
- )
163
- end
164
-
165
116
  def data
166
- raise Error, "MiniMagick::Image#data isn't supported on GraphicsMagick. Use MiniMagick::Image#details instead." if MiniMagick.graphicsmagick?
167
-
168
117
  @info["data"] ||= (
169
- json = MiniMagick::Tool::Convert.new do |convert|
118
+ json = MiniMagick.convert do |convert|
170
119
  convert << path
171
120
  convert << "json:"
172
121
  end
@@ -178,7 +127,7 @@ module MiniMagick
178
127
  end
179
128
 
180
129
  def identify
181
- MiniMagick::Tool::Identify.new do |builder|
130
+ MiniMagick.identify do |builder|
182
131
  yield builder if block_given?
183
132
  builder << path
184
133
  end
@@ -51,11 +51,11 @@ module MiniMagick
51
51
  #
52
52
  def self.import_pixels(blob, columns, rows, depth, map, format = 'png')
53
53
  # Create an image object with the raw pixel data string:
54
- create(".dat", false) { |f| f.write(blob) }.tap do |image|
54
+ read(blob, ".dat").tap do |image|
55
55
  output_path = image.path.sub(/\.\w+$/, ".#{format}")
56
56
  # Use ImageMagick to convert the raw data file to an image file of the
57
57
  # desired format:
58
- MiniMagick::Tool::Convert.new do |convert|
58
+ MiniMagick.convert do |convert|
59
59
  convert.size "#{columns}x#{rows}"
60
60
  convert.depth depth
61
61
  convert << "#{map}:#{image.path}"
@@ -79,33 +79,15 @@ module MiniMagick
79
79
  # @param options [Hash] Specify options for the open method
80
80
  # @return [MiniMagick::Image] The loaded image
81
81
  #
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
93
- else
94
- options = { binmode: true }.merge(options)
95
- Pathname(path_or_url)
96
- end
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
103
- ext.sub!(/:.*/, '') # hack for filenames or URLs that include a colon
104
-
105
- if openable.is_a?(URI::Generic)
106
- openable.open(options) { |file| read(file, ext) }
82
+ def self.open(path_or_url, ext = nil, **options)
83
+ if path_or_url.to_s =~ %r{\A(https?|ftp)://}
84
+ uri = URI(path_or_url)
85
+ ext ||= File.extname(uri.path).sub(/:.*/, '') # handle URL including a colon
86
+ uri.open(options) { |file| read(file, ext) }
107
87
  else
108
- openable.open(**options) { |file| read(file, ext) }
88
+ pathname = Pathname(path_or_url)
89
+ ext ||= File.extname(pathname.to_s)
90
+ pathname.open(binmode: true, **options) { |file| read(file, ext) }
109
91
  end
110
92
  end
111
93
 
@@ -118,18 +100,14 @@ module MiniMagick
118
100
  # we have a good tempfile.
119
101
  #
120
102
  # @param ext [String] Specify the extension you want to read it as
121
- # @param validate [Boolean] If false, skips validation of the created
122
- # image. Defaults to true.
123
103
  # @yield [Tempfile] You can #write bits to this object to create the new
124
104
  # Image
125
105
  # @return [MiniMagick::Image] The created image
126
106
  #
127
- def self.create(ext = nil, validate = MiniMagick.validate_on_create, &block)
107
+ def self.create(ext = nil, &block)
128
108
  tempfile = MiniMagick::Utilities.tempfile(ext.to_s.downcase, &block)
129
109
 
130
- new(tempfile.path, tempfile).tap do |image|
131
- image.validate! if validate
132
- end
110
+ new(tempfile.path, tempfile)
133
111
  end
134
112
 
135
113
  ##
@@ -164,7 +142,7 @@ module MiniMagick
164
142
  # which creates a temporary file for you and protects your original.
165
143
  #
166
144
  # @param input_path [String, Pathname] The location of an image file
167
- # @yield [MiniMagick::Tool::Mogrify] If block is given, {#combine_options}
145
+ # @yield [MiniMagick::Tool] If block is given, {#combine_options}
168
146
  # is called.
169
147
  #
170
148
  def initialize(input_path, tempfile = nil, &block)
@@ -228,10 +206,6 @@ module MiniMagick
228
206
  #
229
207
  attribute :type, "format"
230
208
  ##
231
- # @return [String]
232
- #
233
- attribute :mime_type
234
- ##
235
209
  # @return [Integer]
236
210
  #
237
211
  attribute :width
@@ -274,7 +248,7 @@ module MiniMagick
274
248
  #
275
249
  attribute :resolution
276
250
  ##
277
- # Returns the message digest of this image as a SHA-256, hexidecimal
251
+ # Returns the message digest of this image as a SHA-256, hexadecimal
278
252
  # encoded string. This signature uniquely identifies the image and is
279
253
  # convenient for determining if an image has been modified or whether two
280
254
  # images are identical.
@@ -286,17 +260,10 @@ module MiniMagick
286
260
  #
287
261
  attribute :signature
288
262
  ##
289
- # Returns the information from `identify -verbose` in a Hash format, for
290
- # ImageMagick.
263
+ # Returns the result of converting the image to JSON format.
291
264
  #
292
265
  # @return [Hash]
293
266
  attribute :data
294
- ##
295
- # Returns the information from `identify -verbose` in a Hash format, for
296
- # GraphicsMagick.
297
- #
298
- # @return [Hash]
299
- attribute :details
300
267
 
301
268
  ##
302
269
  # Use this method if you want to access raw Identify's format API.
@@ -367,7 +334,7 @@ module MiniMagick
367
334
  # @return [Array] Matrix of each color of each pixel
368
335
  def get_pixels(map="RGB")
369
336
  raise ArgumentError, "Invalid map value" unless ["RGB", "RGBA"].include?(map)
370
- convert = MiniMagick::Tool::Convert.new
337
+ convert = MiniMagick.convert
371
338
  convert << path
372
339
  convert.depth(8)
373
340
  convert << "#{map}:-"
@@ -397,10 +364,10 @@ module MiniMagick
397
364
  # @example
398
365
  # # It is given in readme.md file
399
366
  ##
400
- def self.get_image_from_pixels(pixels, dimension, map, depth, mime_type)
367
+ def self.get_image_from_pixels(pixels, dimension, map, depth, format)
401
368
  pixels = pixels.flatten
402
369
  blob = pixels.pack('C*')
403
- import_pixels(blob, *dimension, depth, map, mime_type)
370
+ import_pixels(blob, *dimension, depth, map, format)
404
371
  end
405
372
 
406
373
  ##
@@ -426,7 +393,7 @@ module MiniMagick
426
393
  # will convert all pages.
427
394
  # @param read_opts [Hash] Any read options to be passed to ImageMagick
428
395
  # for example: image.format('jpg', page, {density: '300'})
429
- # @yield [MiniMagick::Tool::Convert] It optionally yields the command,
396
+ # @yield [MiniMagick::Tool] It optionally yields the command,
430
397
  # if you want to add something.
431
398
  # @return [self]
432
399
  #
@@ -441,7 +408,7 @@ module MiniMagick
441
408
  input_path = path.dup
442
409
  input_path << "[#{page}]" if page && !layer?
443
410
 
444
- MiniMagick::Tool::Convert.new do |convert|
411
+ MiniMagick.convert do |convert|
445
412
  read_opts.each do |opt, val|
446
413
  convert.send(opt.to_s, val)
447
414
  end
@@ -477,7 +444,7 @@ module MiniMagick
477
444
  # c.background "blue"
478
445
  # end
479
446
  #
480
- # @yield [MiniMagick::Tool::Mogrify]
447
+ # @yield [MiniMagick::Command]
481
448
  # @see http://www.imagemagick.org/script/mogrify.php
482
449
  # @return [self]
483
450
  #
@@ -498,8 +465,11 @@ module MiniMagick
498
465
  end
499
466
  end
500
467
 
501
- def respond_to_missing?(method_name, include_private = false)
502
- MiniMagick::Tool::Mogrify.option_methods.include?(method_name.to_s)
468
+ ##
469
+ # Prevents ruby from calling `#to_ary` on the image when checking if it's a
470
+ # splattable data structure in certain cases.
471
+ def respond_to_missing?(name, include_all)
472
+ false
503
473
  end
504
474
 
505
475
  ##
@@ -514,7 +484,7 @@ module MiniMagick
514
484
  case output_to
515
485
  when String, Pathname
516
486
  if layer?
517
- MiniMagick::Tool::Convert.new do |builder|
487
+ MiniMagick.convert do |builder|
518
488
  builder << path
519
489
  builder << output_to
520
490
  end
@@ -541,7 +511,7 @@ module MiniMagick
541
511
  def composite(other_image, output_extension = type.downcase, mask = nil)
542
512
  output_tempfile = MiniMagick::Utilities.tempfile(".#{output_extension}")
543
513
 
544
- MiniMagick::Tool::Composite.new do |composite|
514
+ MiniMagick.composite do |composite|
545
515
  yield composite if block_given?
546
516
  composite << other_image.path
547
517
  composite << path
@@ -583,27 +553,21 @@ module MiniMagick
583
553
  # b.verbose
584
554
  # end # runs `identify -verbose image.jpg`
585
555
  # @return [String] Output from `identify`
586
- # @yield [MiniMagick::Tool::Identify]
556
+ # @yield [MiniMagick::Tool]
587
557
  #
588
558
  def identify
589
- MiniMagick::Tool::Identify.new do |builder|
559
+ MiniMagick.identify do |builder|
590
560
  yield builder if block_given?
591
561
  builder << path
592
562
  end
593
563
  end
594
564
 
595
- # @private
596
- def run_command(tool_name, *args)
597
- MiniMagick::Tool.const_get(tool_name.capitalize).new do |builder|
598
- args.each do |arg|
599
- builder << arg
600
- end
601
- end
602
- end
603
-
604
565
  def mogrify(page = nil)
605
- MiniMagick::Tool::MogrifyRestricted.new do |builder|
566
+ MiniMagick.mogrify do |builder|
606
567
  yield builder if block_given?
568
+ if builder.args.include?("-format")
569
+ fail MiniMagick::Error, "you must call #format on a MiniMagick::Image directly"
570
+ end
607
571
  builder << (page ? "#{path}[#{page}]" : path)
608
572
  end
609
573
 
@@ -1,4 +1,4 @@
1
- require "timeout"
1
+ require "open3"
2
2
  require "benchmark"
3
3
 
4
4
  module MiniMagick
@@ -10,64 +10,34 @@ module MiniMagick
10
10
  #
11
11
  class Shell
12
12
 
13
- def run(command, options = {})
14
- stdout, stderr, status = execute(command, stdin: options[:stdin])
13
+ def run(command, errors: MiniMagick.errors, warnings: MiniMagick.warnings, **options)
14
+ stdout, stderr, status = execute(command, **options)
15
15
 
16
- if status != 0 && options.fetch(:whiny, MiniMagick.whiny)
17
- fail MiniMagick::Error, "`#{command.join(" ")}` failed with status: #{status} and error:\n#{stderr}"
16
+ if status != 0
17
+ if stderr.include?("time limit exceeded")
18
+ fail MiniMagick::TimeoutError, "`#{command.join(" ")}` has timed out"
19
+ elsif errors
20
+ fail MiniMagick::Error, "`#{command.join(" ")}` failed with status: #{status.inspect} and error:\n#{stderr}"
21
+ end
18
22
  end
19
23
 
20
- $stderr.print(stderr) unless options[:stderr] == false
24
+ $stderr.print(stderr) if warnings
21
25
 
22
26
  [stdout, stderr, status]
23
27
  end
24
28
 
25
- def execute(command, options = {})
26
- stdout, stderr, status =
27
- log(command.join(" ")) do
28
- send("execute_#{MiniMagick.shell_api.tr("-", "_")}", command, options)
29
- end
29
+ def execute(command, stdin: "", timeout: MiniMagick.timeout)
30
+ stdout, stderr, status = log(command.join(" ")) do
31
+ Open3.capture3({ "MAGICK_TIME_LIMIT" => timeout&.to_s }, *command, stdin_data: stdin)
32
+ end
30
33
 
31
- [stdout, stderr, status.exitstatus]
34
+ [stdout, stderr, status&.exitstatus]
32
35
  rescue Errno::ENOENT, IOError
33
36
  ["", "executable not found: \"#{command.first}\"", 127]
34
37
  end
35
38
 
36
39
  private
37
40
 
38
- def execute_open3(command, options = {})
39
- require "open3"
40
-
41
- # We would ideally use Open3.capture3, but it wouldn't allow us to
42
- # terminate the command after timing out.
43
- Open3.popen3(*command) do |in_w, out_r, err_r, thread|
44
- [in_w, out_r, err_r].each(&:binmode)
45
- stdout_reader = Thread.new { out_r.read }
46
- stderr_reader = Thread.new { err_r.read }
47
- begin
48
- in_w.write options[:stdin].to_s
49
- rescue Errno::EPIPE
50
- end
51
- in_w.close
52
-
53
- unless thread.join(MiniMagick.timeout)
54
- Process.kill("TERM", thread.pid) rescue nil
55
- Process.waitpid(thread.pid) rescue nil
56
- raise Timeout::Error, "MiniMagick command timed out: #{command}"
57
- end
58
-
59
- [stdout_reader.value, stderr_reader.value, thread.value]
60
- end
61
- end
62
-
63
- def execute_posix_spawn(command, options = {})
64
- require "posix-spawn"
65
- child = POSIX::Spawn::Child.new(*command, input: options[:stdin].to_s, timeout: MiniMagick.timeout)
66
- [child.out, child.err, child.status]
67
- rescue POSIX::Spawn::TimeoutExceeded
68
- raise Timeout::Error, "MiniMagick command timed out: #{command}"
69
- end
70
-
71
41
  def log(command, &block)
72
42
  value = nil
73
43
  duration = Benchmark.realtime { value = block.call }