mini_magick 3.7.0 → 4.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Rakefile +3 -3
- data/lib/mini_gmagick.rb +1 -0
- data/lib/mini_magick/configuration.rb +190 -0
- data/lib/mini_magick/image/info.rb +202 -0
- data/lib/mini_magick/image.rb +540 -311
- data/lib/mini_magick/shell.rb +81 -0
- data/lib/mini_magick/tool/animate.rb +14 -0
- data/lib/mini_magick/tool/compare.rb +14 -0
- data/lib/mini_magick/tool/composite.rb +14 -0
- data/lib/mini_magick/tool/conjure.rb +14 -0
- data/lib/mini_magick/tool/convert.rb +14 -0
- data/lib/mini_magick/tool/display.rb +14 -0
- data/lib/mini_magick/tool/identify.rb +14 -0
- data/lib/mini_magick/tool/import.rb +14 -0
- data/lib/mini_magick/tool/magick.rb +14 -0
- data/lib/mini_magick/tool/mogrify.rb +14 -0
- data/lib/mini_magick/tool/mogrify_restricted.rb +15 -0
- data/lib/mini_magick/tool/montage.rb +14 -0
- data/lib/mini_magick/tool/stream.rb +14 -0
- data/lib/mini_magick/tool.rb +307 -0
- data/lib/mini_magick/utilities.rb +25 -21
- data/lib/mini_magick/version.rb +15 -1
- data/lib/mini_magick.rb +54 -70
- metadata +65 -30
- data/lib/mini_magick/command_builder.rb +0 -104
- data/lib/mini_magick/errors.rb +0 -4
@@ -0,0 +1,81 @@
|
|
1
|
+
require "timeout"
|
2
|
+
require "benchmark"
|
3
|
+
|
4
|
+
module MiniMagick
|
5
|
+
##
|
6
|
+
# Sends commands to the shell (more precisely, it sends commands directly to
|
7
|
+
# the operating system).
|
8
|
+
#
|
9
|
+
# @private
|
10
|
+
#
|
11
|
+
class Shell
|
12
|
+
|
13
|
+
def run(command, options = {})
|
14
|
+
stdout, stderr, status = execute(command, stdin: options[:stdin])
|
15
|
+
|
16
|
+
if status != 0 && options.fetch(:whiny, MiniMagick.whiny)
|
17
|
+
fail MiniMagick::Error, "`#{command.join(" ")}` failed with error:\n#{stderr}"
|
18
|
+
end
|
19
|
+
|
20
|
+
$stderr.print(stderr) unless options[:stderr] == false
|
21
|
+
|
22
|
+
[stdout, stderr, status]
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute(command, options = {})
|
26
|
+
stdout, stderr, status =
|
27
|
+
log(command.join(" ")) do
|
28
|
+
send("execute_#{MiniMagick.shell_api.gsub("-", "_")}", command, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
[stdout, stderr, status.exitstatus]
|
32
|
+
rescue Errno::ENOENT, IOError
|
33
|
+
["", "executable not found: \"#{command.first}\"", 127]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
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
|
+
begin
|
54
|
+
Timeout.timeout(MiniMagick.timeout) { thread.join }
|
55
|
+
rescue Timeout::Error
|
56
|
+
Process.kill("TERM", thread.pid) rescue nil
|
57
|
+
Process.waitpid(thread.pid) rescue nil
|
58
|
+
raise Timeout::Error, "MiniMagick command timed out: #{command}"
|
59
|
+
end
|
60
|
+
|
61
|
+
[stdout_reader.value, stderr_reader.value, thread.value]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def execute_posix_spawn(command, options = {})
|
66
|
+
require "posix-spawn"
|
67
|
+
child = POSIX::Spawn::Child.new(*command, input: options[:stdin].to_s, timeout: MiniMagick.timeout)
|
68
|
+
[child.out, child.err, child.status]
|
69
|
+
rescue POSIX::Spawn::TimeoutExceeded
|
70
|
+
raise Timeout::Error, "MiniMagick command timed out: #{command}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def log(command, &block)
|
74
|
+
value = nil
|
75
|
+
duration = Benchmark.realtime { value = block.call }
|
76
|
+
MiniMagick.logger.debug "[%.2fs] %s" % [duration, command]
|
77
|
+
value
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "mini_magick/tool/mogrify"
|
2
|
+
|
3
|
+
module MiniMagick
|
4
|
+
class Tool
|
5
|
+
##
|
6
|
+
# @see http://www.imagemagick.org/script/mogrify.php
|
7
|
+
#
|
8
|
+
class MogrifyRestricted < Mogrify
|
9
|
+
def format(*args)
|
10
|
+
fail NoMethodError,
|
11
|
+
"you must call #format on a MiniMagick::Image directly"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,307 @@
|
|
1
|
+
require "mini_magick/shell"
|
2
|
+
|
3
|
+
module MiniMagick
|
4
|
+
##
|
5
|
+
# Abstract class that wraps command-line tools. It shouldn't be used directly,
|
6
|
+
# but through one of its subclasses (e.g. {MiniMagick::Tool::Mogrify}). Use
|
7
|
+
# this class if you want to be closer to the metal and execute ImageMagick
|
8
|
+
# commands directly, but still with a nice Ruby interface.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# MiniMagick::Tool::Mogrify.new do |builder|
|
12
|
+
# builder.resize "500x500"
|
13
|
+
# builder << "path/to/image.jpg"
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
class Tool
|
17
|
+
|
18
|
+
CREATION_OPERATORS = %w[xc canvas logo rose gradient radial-gradient plasma
|
19
|
+
pattern text pango]
|
20
|
+
|
21
|
+
##
|
22
|
+
# Aside from classic instantiation, it also accepts a block, and then
|
23
|
+
# executes the command in the end.
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# version = MiniMagick::Tool::Identify.new { |b| b.version }
|
27
|
+
# puts version
|
28
|
+
#
|
29
|
+
# @return [MiniMagick::Tool, String] If no block is given, returns an
|
30
|
+
# instance of the tool, if block is given, returns the output of the
|
31
|
+
# command.
|
32
|
+
#
|
33
|
+
def self.new(*args)
|
34
|
+
instance = super(*args)
|
35
|
+
|
36
|
+
if block_given?
|
37
|
+
yield instance
|
38
|
+
instance.call
|
39
|
+
else
|
40
|
+
instance
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @private
|
45
|
+
attr_reader :name, :args
|
46
|
+
|
47
|
+
# @param name [String]
|
48
|
+
# @param options [Hash]
|
49
|
+
# @option options [Boolean] :whiny Whether to raise errors on non-zero
|
50
|
+
# exit codes.
|
51
|
+
# @example
|
52
|
+
# MiniMagick::Tool::Identify.new(whiny: false) do |identify|
|
53
|
+
# identify.help # returns exit status 1, which would otherwise throw an error
|
54
|
+
# end
|
55
|
+
def initialize(name, options = {})
|
56
|
+
warn "MiniMagick::Tool.new(false) is deprecated and will be removed in MiniMagick 5, use MiniMagick::Tool.new(whiny: false) instead." if !options.is_a?(Hash)
|
57
|
+
|
58
|
+
@name = name
|
59
|
+
@args = []
|
60
|
+
@whiny = options.is_a?(Hash) ? options.fetch(:whiny, MiniMagick.whiny) : options
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Executes the command that has been built up.
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# mogrify = MiniMagick::Tool::Mogrify.new
|
68
|
+
# mogrify.resize("500x500")
|
69
|
+
# mogrify << "path/to/image.jpg"
|
70
|
+
# mogrify.call # executes `mogrify -resize 500x500 path/to/image.jpg`
|
71
|
+
#
|
72
|
+
# @example
|
73
|
+
# mogrify = MiniMagick::Tool::Mogrify.new
|
74
|
+
# # build the command
|
75
|
+
# mogrify.call do |stdout, stderr, status|
|
76
|
+
# # ...
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# @yield [Array] Optionally yields stdout, stderr, and exit status
|
80
|
+
#
|
81
|
+
# @return [String] Returns the output of the command
|
82
|
+
#
|
83
|
+
def call(*args)
|
84
|
+
options = args[-1].is_a?(Hash) ? args.pop : {}
|
85
|
+
warn "Passing whiny to MiniMagick::Tool#call is deprecated and will be removed in MiniMagick 5, use MiniMagick::Tool.new(whiny: false) instead." if args.any?
|
86
|
+
whiny = args.fetch(0, @whiny)
|
87
|
+
|
88
|
+
options[:whiny] = whiny
|
89
|
+
options[:stderr] = false if block_given?
|
90
|
+
|
91
|
+
shell = MiniMagick::Shell.new
|
92
|
+
stdout, stderr, status = shell.run(command, options)
|
93
|
+
yield stdout, stderr, status if block_given?
|
94
|
+
|
95
|
+
stdout.chomp("\n")
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# The currently built-up command.
|
100
|
+
#
|
101
|
+
# @return [Array<String>]
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# mogrify = MiniMagick::Tool::Mogrify.new
|
105
|
+
# mogrify.resize "500x500"
|
106
|
+
# mogrify.contrast
|
107
|
+
# mogrify.command #=> ["mogrify", "-resize", "500x500", "-contrast"]
|
108
|
+
#
|
109
|
+
def command
|
110
|
+
[*executable, *args]
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# The executable used for this tool. Respects
|
115
|
+
# {MiniMagick::Configuration#cli}, {MiniMagick::Configuration#cli_path},
|
116
|
+
# and {MiniMagick::Configuration#cli_prefix}.
|
117
|
+
#
|
118
|
+
# @return [Array<String>]
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
# MiniMagick.configure { |config| config.cli = :graphicsmagick }
|
122
|
+
# identify = MiniMagick::Tool::Identify.new
|
123
|
+
# identify.executable #=> ["gm", "identify"]
|
124
|
+
#
|
125
|
+
# @example
|
126
|
+
# MiniMagick.configure do |config|
|
127
|
+
# config.cli = :graphicsmagick
|
128
|
+
# config.cli_prefix = ['firejail', '--force']
|
129
|
+
# end
|
130
|
+
# identify = MiniMagick::Tool::Identify.new
|
131
|
+
# identify.executable #=> ["firejail", "--force", "gm", "identify"]
|
132
|
+
#
|
133
|
+
def executable
|
134
|
+
exe = [name]
|
135
|
+
exe.unshift "magick" if MiniMagick.imagemagick7? && name != "magick"
|
136
|
+
exe.unshift "gm" if MiniMagick.graphicsmagick?
|
137
|
+
exe.unshift File.join(MiniMagick.cli_path, exe.shift) if MiniMagick.cli_path
|
138
|
+
Array(MiniMagick.cli_prefix).reverse_each { |p| exe.unshift p } if MiniMagick.cli_prefix
|
139
|
+
exe
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Appends raw options, useful for appending image paths.
|
144
|
+
#
|
145
|
+
# @return [self]
|
146
|
+
#
|
147
|
+
def <<(arg)
|
148
|
+
args << arg.to_s
|
149
|
+
self
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Merges a list of raw options.
|
154
|
+
#
|
155
|
+
# @return [self]
|
156
|
+
#
|
157
|
+
def merge!(new_args)
|
158
|
+
new_args.each { |arg| self << arg }
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# Changes the last operator to its "plus" form.
|
164
|
+
#
|
165
|
+
# @example
|
166
|
+
# MiniMagick::Tool::Mogrify.new do |mogrify|
|
167
|
+
# mogrify.antialias.+
|
168
|
+
# mogrify.distort.+("Perspective", "0,0,4,5 89,0,45,46")
|
169
|
+
# end
|
170
|
+
# # executes `mogrify +antialias +distort Perspective '0,0,4,5 89,0,45,46'`
|
171
|
+
#
|
172
|
+
# @return [self]
|
173
|
+
#
|
174
|
+
def +(*values)
|
175
|
+
args[-1] = args[-1].sub(/^-/, '+')
|
176
|
+
self.merge!(values)
|
177
|
+
self
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Create an ImageMagick stack in the command (surround.
|
182
|
+
#
|
183
|
+
# @example
|
184
|
+
# MiniMagick::Tool::Convert.new do |convert|
|
185
|
+
# convert << "wand.gif"
|
186
|
+
# convert.stack do |stack|
|
187
|
+
# stack << "wand.gif"
|
188
|
+
# stack.rotate(30)
|
189
|
+
# end
|
190
|
+
# convert.append.+
|
191
|
+
# convert << "images.gif"
|
192
|
+
# end
|
193
|
+
# # executes `convert wand.gif \( wizard.gif -rotate 30 \) +append images.gif`
|
194
|
+
#
|
195
|
+
def stack(*args)
|
196
|
+
self << "("
|
197
|
+
args.each do |value|
|
198
|
+
case value
|
199
|
+
when Hash then value.each { |key, value| send(key, *value) }
|
200
|
+
when String then self << value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
yield self if block_given?
|
204
|
+
self << ")"
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
# Adds ImageMagick's pseudo-filename `-` for standard input.
|
209
|
+
#
|
210
|
+
# @example
|
211
|
+
# identify = MiniMagick::Tool::Identify.new
|
212
|
+
# identify.stdin
|
213
|
+
# identify.call(stdin: image_content)
|
214
|
+
# # executes `identify -` with the given standard input
|
215
|
+
#
|
216
|
+
def stdin
|
217
|
+
self << "-"
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Adds ImageMagick's pseudo-filename `-` for standard output.
|
222
|
+
#
|
223
|
+
# @example
|
224
|
+
# content = MiniMagick::Tool::Convert.new do |convert|
|
225
|
+
# convert << "input.jpg"
|
226
|
+
# convert.auto_orient
|
227
|
+
# convert.stdout
|
228
|
+
# end
|
229
|
+
# # executes `convert input.jpg -auto-orient -` which returns file contents
|
230
|
+
#
|
231
|
+
def stdout
|
232
|
+
self << "-"
|
233
|
+
end
|
234
|
+
|
235
|
+
##
|
236
|
+
# Define creator operator methods
|
237
|
+
#
|
238
|
+
# @example
|
239
|
+
# mogrify = MiniMagick::Tool.new("mogrify")
|
240
|
+
# mogrify.canvas("khaki")
|
241
|
+
# mogrify.command.join(" ") #=> "mogrify canvas:khaki"
|
242
|
+
#
|
243
|
+
CREATION_OPERATORS.each do |operator|
|
244
|
+
define_method(operator.gsub('-', '_')) do |value = nil|
|
245
|
+
self << "#{operator}:#{value}"
|
246
|
+
self
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
##
|
251
|
+
# This option is a valid ImageMagick option, but it's also a Ruby method,
|
252
|
+
# so we need to override it so that it correctly acts as an option method.
|
253
|
+
#
|
254
|
+
def clone(*args)
|
255
|
+
self << '-clone'
|
256
|
+
self.merge!(args)
|
257
|
+
self
|
258
|
+
end
|
259
|
+
|
260
|
+
##
|
261
|
+
# Any undefined method will be transformed into a CLI option
|
262
|
+
#
|
263
|
+
# @example
|
264
|
+
# mogrify = MiniMagick::Tool.new("mogrify")
|
265
|
+
# mogrify.adaptive_blur("...")
|
266
|
+
# mogrify.foo_bar
|
267
|
+
# mogrify.command.join(" ") # => "mogrify -adaptive-blur ... -foo-bar"
|
268
|
+
#
|
269
|
+
def method_missing(name, *args)
|
270
|
+
option = "-#{name.to_s.tr('_', '-')}"
|
271
|
+
self << option
|
272
|
+
self.merge!(args)
|
273
|
+
self
|
274
|
+
end
|
275
|
+
|
276
|
+
def self.option_methods
|
277
|
+
@option_methods ||= (
|
278
|
+
tool = new(whiny: false)
|
279
|
+
tool << "-help"
|
280
|
+
help_page = tool.call(stderr: false)
|
281
|
+
|
282
|
+
cli_options = help_page.scan(/^\s+-[a-z\-]+/).map(&:strip)
|
283
|
+
if tool.name == "mogrify" && MiniMagick.graphicsmagick?
|
284
|
+
# These options were undocumented before 2015-06-14 (see gm bug 302)
|
285
|
+
cli_options |= %w[-box -convolve -gravity -linewidth -mattecolor -render -shave]
|
286
|
+
end
|
287
|
+
|
288
|
+
cli_options.map { |o| o[1..-1].tr('-','_') }
|
289
|
+
)
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
require "mini_magick/tool/animate"
|
296
|
+
require "mini_magick/tool/compare"
|
297
|
+
require "mini_magick/tool/composite"
|
298
|
+
require "mini_magick/tool/conjure"
|
299
|
+
require "mini_magick/tool/convert"
|
300
|
+
require "mini_magick/tool/display"
|
301
|
+
require "mini_magick/tool/identify"
|
302
|
+
require "mini_magick/tool/import"
|
303
|
+
require "mini_magick/tool/magick"
|
304
|
+
require "mini_magick/tool/mogrify"
|
305
|
+
require "mini_magick/tool/mogrify_restricted"
|
306
|
+
require "mini_magick/tool/montage"
|
307
|
+
require "mini_magick/tool/stream"
|
@@ -1,31 +1,35 @@
|
|
1
|
-
require
|
1
|
+
require "tempfile"
|
2
2
|
|
3
3
|
module MiniMagick
|
4
|
+
# @private
|
4
5
|
module Utilities
|
5
|
-
class << self
|
6
|
-
# Cross-platform way of finding an executable in the $PATH.
|
7
|
-
#
|
8
|
-
# which('ruby') #=> /usr/bin/ruby
|
9
|
-
def which(cmd)
|
10
|
-
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
11
|
-
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
12
|
-
exts.each { |ext|
|
13
|
-
exe = File.join(path, "#{cmd}#{ext}")
|
14
|
-
return exe if File.executable? exe
|
15
|
-
}
|
16
|
-
end
|
17
|
-
return nil
|
18
|
-
end
|
19
6
|
|
20
|
-
|
21
|
-
|
22
|
-
|
7
|
+
module_function
|
8
|
+
|
9
|
+
##
|
10
|
+
# Cross-platform way of finding an executable in the $PATH.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# MiniMagick::Utilities.which('ruby') #=> "/usr/bin/ruby"
|
14
|
+
#
|
15
|
+
def which(cmd)
|
16
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
17
|
+
ENV.fetch('PATH').split(File::PATH_SEPARATOR).each do |path|
|
18
|
+
exts.each do |ext|
|
19
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
20
|
+
return exe if File.executable? exe
|
21
|
+
end
|
23
22
|
end
|
23
|
+
nil
|
24
|
+
end
|
24
25
|
|
25
|
-
|
26
|
-
|
26
|
+
def tempfile(extension)
|
27
|
+
Tempfile.new(["mini_magick", extension]).tap do |tempfile|
|
28
|
+
tempfile.binmode
|
29
|
+
yield tempfile if block_given?
|
30
|
+
tempfile.close
|
27
31
|
end
|
28
32
|
end
|
33
|
+
|
29
34
|
end
|
30
35
|
end
|
31
|
-
|
data/lib/mini_magick/version.rb
CHANGED
@@ -1,3 +1,17 @@
|
|
1
1
|
module MiniMagick
|
2
|
-
|
2
|
+
##
|
3
|
+
# @return [Gem::Version]
|
4
|
+
#
|
5
|
+
def self.version
|
6
|
+
Gem::Version.new VERSION::STRING
|
7
|
+
end
|
8
|
+
|
9
|
+
module VERSION
|
10
|
+
MAJOR = 4
|
11
|
+
MINOR = 11
|
12
|
+
TINY = 0
|
13
|
+
PRE = nil
|
14
|
+
|
15
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
16
|
+
end
|
3
17
|
end
|