mini_magick 3.8.1 → 4.9.4

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.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/lib/mini_gmagick.rb +1 -0
  3. data/lib/mini_magick/configuration.rb +198 -0
  4. data/lib/mini_magick/image/info.rb +192 -0
  5. data/lib/mini_magick/image.rb +456 -341
  6. data/lib/mini_magick/shell.rb +81 -0
  7. data/lib/mini_magick/tool/animate.rb +14 -0
  8. data/lib/mini_magick/tool/compare.rb +14 -0
  9. data/lib/mini_magick/tool/composite.rb +14 -0
  10. data/lib/mini_magick/tool/conjure.rb +14 -0
  11. data/lib/mini_magick/tool/convert.rb +14 -0
  12. data/lib/mini_magick/tool/display.rb +14 -0
  13. data/lib/mini_magick/tool/identify.rb +14 -0
  14. data/lib/mini_magick/tool/import.rb +14 -0
  15. data/lib/mini_magick/tool/magick.rb +14 -0
  16. data/lib/mini_magick/tool/mogrify.rb +14 -0
  17. data/lib/mini_magick/tool/mogrify_restricted.rb +15 -0
  18. data/lib/mini_magick/tool/montage.rb +14 -0
  19. data/lib/mini_magick/tool/stream.rb +14 -0
  20. data/lib/mini_magick/tool.rb +297 -0
  21. data/lib/mini_magick/utilities.rb +23 -50
  22. data/lib/mini_magick/version.rb +5 -5
  23. data/lib/mini_magick.rb +54 -65
  24. metadata +49 -51
  25. data/lib/mini_magick/command_builder.rb +0 -94
  26. data/lib/mini_magick/errors.rb +0 -4
  27. data/spec/files/actually_a_gif.jpg +0 -0
  28. data/spec/files/animation.gif +0 -0
  29. data/spec/files/composited.jpg +0 -0
  30. data/spec/files/erroneous.jpg +0 -0
  31. data/spec/files/layers.psd +0 -0
  32. data/spec/files/leaves (spaced).tiff +0 -0
  33. data/spec/files/not_an_image.php +0 -1
  34. data/spec/files/png.png +0 -0
  35. data/spec/files/simple-minus.gif +0 -0
  36. data/spec/files/simple.gif +0 -0
  37. data/spec/files/trogdor.jpg +0 -0
  38. data/spec/files/trogdor_capitalized.JPG +0 -0
  39. data/spec/lib/mini_magick/command_builder_spec.rb +0 -153
  40. data/spec/lib/mini_magick/image_spec.rb +0 -499
  41. data/spec/lib/mini_magick_spec.rb +0 -63
  42. data/spec/spec_helper.rb +0 -29
@@ -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,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/animate.php
5
+ #
6
+ class Animate < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("animate", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/compare.php
5
+ #
6
+ class Compare < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("compare", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/composite.php
5
+ #
6
+ class Composite < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("composite", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/conjure.php
5
+ #
6
+ class Conjure < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("conjure", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/convert.php
5
+ #
6
+ class Convert < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("convert", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/display.php
5
+ #
6
+ class Display < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("display", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/identify.php
5
+ #
6
+ class Identify < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("identify", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/import.php
5
+ #
6
+ class Import < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("import", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -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
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/mogrify.php
5
+ #
6
+ class Mogrify < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("mogrify", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ 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,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/montage.php
5
+ #
6
+ class Montage < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("montage", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module MiniMagick
2
+ class Tool
3
+ ##
4
+ # @see http://www.imagemagick.org/script/stream.php
5
+ #
6
+ class Stream < MiniMagick::Tool
7
+
8
+ def initialize(*args)
9
+ super("stream", *args)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,297 @@
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 whiny [Boolean] Whether to raise errors on exit codes different
48
+ # than 0.
49
+ # @example
50
+ # MiniMagick::Tool::Identify.new(whiny: false) do |identify|
51
+ # identify.help # returns exit status 1, which would otherwise throw an error
52
+ # end
53
+ def initialize(name, options = {})
54
+ 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)
55
+
56
+ @name = name
57
+ @args = []
58
+ @whiny = options.is_a?(Hash) ? options.fetch(:whiny, MiniMagick.whiny) : options
59
+ end
60
+
61
+ ##
62
+ # Executes the command that has been built up.
63
+ #
64
+ # @example
65
+ # mogrify = MiniMagick::Tool::Mogrify.new
66
+ # mogrify.resize("500x500")
67
+ # mogrify << "path/to/image.jpg"
68
+ # mogrify.call # executes `mogrify -resize 500x500 path/to/image.jpg`
69
+ #
70
+ # @example
71
+ # mogrify = MiniMagick::Tool::Mogrify.new
72
+ # # build the command
73
+ # mogrify.call do |stdout, stderr, status|
74
+ # # ...
75
+ # end
76
+ #
77
+ # @yield [Array] Optionally yields stdout, stderr, and exit status
78
+ #
79
+ # @return [String] Returns the output of the command
80
+ #
81
+ def call(*args)
82
+ options = args[-1].is_a?(Hash) ? args.pop : {}
83
+ 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?
84
+ whiny = args.fetch(0, @whiny)
85
+
86
+ options[:whiny] = whiny
87
+ options[:stderr] = false if block_given?
88
+
89
+ shell = MiniMagick::Shell.new
90
+ stdout, stderr, status = shell.run(command, options)
91
+ yield stdout, stderr, status if block_given?
92
+
93
+ stdout.chomp("\n")
94
+ end
95
+
96
+ ##
97
+ # The currently built-up command.
98
+ #
99
+ # @return [Array<String>]
100
+ #
101
+ # @example
102
+ # mogrify = MiniMagick::Tool::Mogrify.new
103
+ # mogrify.resize "500x500"
104
+ # mogrify.contrast
105
+ # mogrify.command #=> ["mogrify", "-resize", "500x500", "-contrast"]
106
+ #
107
+ def command
108
+ [*executable, *args]
109
+ end
110
+
111
+ ##
112
+ # The executable used for this tool. Respects
113
+ # {MiniMagick::Configuration#cli}, {MiniMagick::Configuration#cli_path},
114
+ # and {MiniMagick::Configuration#cli_prefix}.
115
+ #
116
+ # @return [Array<String>]
117
+ #
118
+ # @example
119
+ # MiniMagick.configure { |config| config.cli = :graphicsmagick }
120
+ # identify = MiniMagick::Tool::Identify.new
121
+ # identify.executable #=> ["gm", "identify"]
122
+ #
123
+ # @example
124
+ # MiniMagick.configure do |config|
125
+ # config.cli = :graphicsmagick
126
+ # config.cli_prefix = ['firejail', '--force']
127
+ # end
128
+ # identify = MiniMagick::Tool::Identify.new
129
+ # identify.executable #=> ["firejail", "--force", "gm", "identify"]
130
+ #
131
+ def executable
132
+ exe = [name]
133
+ exe.unshift "magick" if MiniMagick.imagemagick7? && name != "magick"
134
+ exe.unshift "gm" if MiniMagick.graphicsmagick?
135
+ exe.unshift File.join(MiniMagick.cli_path, exe.shift) if MiniMagick.cli_path
136
+ Array(MiniMagick.cli_prefix).reverse_each { |p| exe.unshift p } if MiniMagick.cli_prefix
137
+ exe
138
+ end
139
+
140
+ ##
141
+ # Appends raw options, useful for appending image paths.
142
+ #
143
+ # @return [self]
144
+ #
145
+ def <<(arg)
146
+ args << arg.to_s
147
+ self
148
+ end
149
+
150
+ ##
151
+ # Merges a list of raw options.
152
+ #
153
+ # @return [self]
154
+ #
155
+ def merge!(new_args)
156
+ new_args.each { |arg| self << arg }
157
+ self
158
+ end
159
+
160
+ ##
161
+ # Changes the last operator to its "plus" form.
162
+ #
163
+ # @example
164
+ # MiniMagick::Tool::Mogrify.new do |mogrify|
165
+ # mogrify.antialias.+
166
+ # mogrify.distort.+("Perspective", "0,0,4,5 89,0,45,46")
167
+ # end
168
+ # # executes `mogrify +antialias +distort Perspective '0,0,4,5 89,0,45,46'`
169
+ #
170
+ # @return [self]
171
+ #
172
+ def +(*values)
173
+ args[-1] = args[-1].sub(/^-/, '+')
174
+ self.merge!(values)
175
+ self
176
+ end
177
+
178
+ ##
179
+ # Create an ImageMagick stack in the command (surround.
180
+ #
181
+ # @example
182
+ # MiniMagick::Tool::Convert.new do |convert|
183
+ # convert << "wand.gif"
184
+ # convert.stack do |stack|
185
+ # stack << "wand.gif"
186
+ # stack.rotate(30)
187
+ # end
188
+ # convert.append.+
189
+ # convert << "images.gif"
190
+ # end
191
+ # # executes `convert wand.gif \( wizard.gif -rotate 30 \) +append images.gif`
192
+ #
193
+ def stack
194
+ self << "("
195
+ yield self
196
+ self << ")"
197
+ end
198
+
199
+ ##
200
+ # Adds ImageMagick's pseudo-filename `-` for standard input.
201
+ #
202
+ # @example
203
+ # identify = MiniMagick::Tool::Identify.new
204
+ # identify.stdin
205
+ # identify.call(stdin: image_content)
206
+ # # executes `identify -` with the given standard input
207
+ #
208
+ def stdin
209
+ self << "-"
210
+ end
211
+
212
+ ##
213
+ # Adds ImageMagick's pseudo-filename `-` for standard output.
214
+ #
215
+ # @example
216
+ # content = MiniMagick::Tool::Convert.new do |convert|
217
+ # convert << "input.jpg"
218
+ # convert.auto_orient
219
+ # convert.stdout
220
+ # end
221
+ # # executes `convert input.jpg -auto-orient -` which returns file contents
222
+ #
223
+ def stdout
224
+ self << "-"
225
+ end
226
+
227
+ ##
228
+ # Define creator operator methods
229
+ #
230
+ # mogrify = MiniMagick::Tool.new("mogrify")
231
+ # mogrify.canvas("khaki")
232
+ # mogrify.command.join(" ") #=> "mogrify canvas:khaki"
233
+ #
234
+ CREATION_OPERATORS.each do |operator|
235
+ define_method(operator.gsub('-', '_')) do |value = nil|
236
+ self << "#{operator}:#{value}"
237
+ self
238
+ end
239
+ end
240
+
241
+ ##
242
+ # This option is a valid ImageMagick option, but it's also a Ruby method,
243
+ # so we need to override it so that it correctly acts as an option method.
244
+ #
245
+ def clone(*args)
246
+ self << '-clone'
247
+ self.merge!(args)
248
+ self
249
+ end
250
+
251
+ ##
252
+ # Any undefined method will be transformed into a CLI option
253
+ #
254
+ # mogrify = MiniMagick::Tool.new("mogrify")
255
+ # mogrify.adaptive_blur("...")
256
+ # mogrify.foo_bar
257
+ # mogrify.command.join(" ") "mogrify -adaptive-blur ... -foo-bar"
258
+ #
259
+ def method_missing(name, *args)
260
+ option = "-#{name.to_s.tr('_', '-')}"
261
+ self << option
262
+ self.merge!(args)
263
+ self
264
+ end
265
+
266
+ def self.option_methods
267
+ @option_methods ||= (
268
+ tool = new(whiny: false)
269
+ tool << "-help"
270
+ help_page = tool.call(stderr: false)
271
+
272
+ cli_options = help_page.scan(/^\s+-[a-z\-]+/).map(&:strip)
273
+ if tool.name == "mogrify" && MiniMagick.graphicsmagick?
274
+ # These options were undocumented before 2015-06-14 (see gm bug 302)
275
+ cli_options |= %w[-box -convolve -gravity -linewidth -mattecolor -render -shave]
276
+ end
277
+
278
+ cli_options.map { |o| o[1..-1].tr('-','_') }
279
+ )
280
+ end
281
+
282
+ end
283
+ end
284
+
285
+ require "mini_magick/tool/animate"
286
+ require "mini_magick/tool/compare"
287
+ require "mini_magick/tool/composite"
288
+ require "mini_magick/tool/conjure"
289
+ require "mini_magick/tool/convert"
290
+ require "mini_magick/tool/display"
291
+ require "mini_magick/tool/identify"
292
+ require "mini_magick/tool/import"
293
+ require "mini_magick/tool/magick"
294
+ require "mini_magick/tool/mogrify"
295
+ require "mini_magick/tool/mogrify_restricted"
296
+ require "mini_magick/tool/montage"
297
+ require "mini_magick/tool/stream"
@@ -1,62 +1,35 @@
1
- require 'rbconfig'
2
- require 'shellwords'
3
- require 'pathname'
1
+ require "tempfile"
4
2
 
5
3
  module MiniMagick
4
+ # @private
6
5
  module Utilities
7
- class << self
8
- # Cross-platform way of finding an executable in the $PATH.
9
- #
10
- # which('ruby') #=> /usr/bin/ruby
11
- def which(cmd)
12
- exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
13
- ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
14
- exts.each do |ext|
15
- exe = File.join(path, "#{cmd}#{ext}")
16
- return exe if File.executable? exe
17
- end
18
- end
19
- nil
20
- end
21
6
 
22
- # Finds out if the host OS is windows
23
- def windows?
24
- RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
25
- end
7
+ module_function
26
8
 
27
- def escape(value)
28
- if windows?
29
- windows_escape(value)
30
- else
31
- shell_escape(value)
32
- end
33
- end
34
-
35
- def shell_escape(value)
36
- Shellwords.escape(value)
37
- end
38
-
39
- def windows_escape(value)
40
- # For Windows, ^ is the escape char, equivalent to \ in Unix.
41
- escaped = value.gsub(/\^/, '^^').gsub(/>/, '^>')
42
- if escaped !~ /^".+"$/ && escaped.include?("'")
43
- escaped.inspect
44
- else
45
- escaped
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
46
21
  end
47
22
  end
23
+ nil
24
+ end
48
25
 
49
- def path(path)
50
- if windows?
51
- # For Windows, if a path contains space char, you need to quote it,
52
- # otherwise you SHOULD NOT quote it. If you quote a path that does
53
- # not contains space, it will not work.
54
- pathname = Pathname.new(path).to_s
55
- path.include?(' ') ? pathname.inspect : pathname
56
- else
57
- path
58
- end
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
59
31
  end
60
32
  end
33
+
61
34
  end
62
35
  end
@@ -1,15 +1,15 @@
1
1
  module MiniMagick
2
2
  ##
3
- # Returns the version of the currently loaded MiniMagick as
4
- # a <tt>Gem::Version</tt>.
3
+ # @return [Gem::Version]
4
+ #
5
5
  def self.version
6
6
  Gem::Version.new VERSION::STRING
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 3
11
- MINOR = 8
12
- TINY = 1
10
+ MAJOR = 4
11
+ MINOR = 9
12
+ TINY = 4
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')