mini_magick 1.2.3 → 2.0.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.
data/MIT-LICENSE CHANGED
File without changes
data/README.rdoc ADDED
@@ -0,0 +1,81 @@
1
+ = MiniMagick
2
+
3
+ A ruby wrapper for ImageMagick or GraphicsMagick command line.
4
+
5
+ Tested on both Ruby 1.9.2 and Ruby 1.8.7.
6
+
7
+ == Why?
8
+
9
+ I was using RMagick and loving it, but it was eating up huge amounts
10
+ of memory. A simple script like this...
11
+
12
+ Magick::read("image.jpg") do |f|
13
+ f.write("manipulated.jpg")
14
+ end
15
+
16
+ ...would use over 100 Megs of Ram. On my local machine this wasn't a
17
+ problem, but on my hosting server the ruby apps would crash because of
18
+ their 100 Meg memory limit.
19
+
20
+
21
+ == Solution!
22
+
23
+ Using MiniMagick the ruby processes memory remains small (it spawns
24
+ ImageMagick's command line program mogrify which takes up some memory
25
+ as well, but is much smaller compared to RMagick)
26
+
27
+ MiniMagick gives you access to all the commandline options ImageMagick
28
+ has (Found here http://www.imagemagick.org/script/mogrify.php)
29
+
30
+
31
+ == Examples
32
+
33
+ Want to make a thumbnail from a file...
34
+
35
+ image = MiniMagick::Image.from_file("input.jpg")
36
+ image.resize "100x100"
37
+ image.write("output.jpg")
38
+
39
+ Want to make a thumbnail from a blob...
40
+
41
+ image = MiniMagick::Image.from_blob(blob)
42
+ image.resize "100x100"
43
+ image.write("output.jpg")
44
+
45
+ Need to combine several options?
46
+
47
+ image = MiniMagick::Image.from_file("input.jpg")
48
+ image.combine_options do |c|
49
+ c.sample "50%"
50
+ c.rotate "-90>"
51
+ end
52
+ image.write("output.jpg")
53
+
54
+ Want to composite two images? Super easy! (Aka, put a watermark on!)
55
+
56
+ image = Image.from_file("original.png")
57
+ result = image.composite(Image.open("watermark.png", "jpg") do |c|
58
+ c.gravity "center"
59
+ end
60
+ result.write("my_output_file.jpg")
61
+
62
+ Want to manipulate an image at its source (You won't have to write it
63
+ out because the transformations are done on that file)
64
+
65
+ image = MiniMagick::Image.new("input.jpg")
66
+ image.resize "100x100"
67
+
68
+ Want to get some meta-information out?
69
+
70
+ image = MiniMagick::Image.from_file("input.jpg")
71
+ image[:width] # will get the width (you can also use :height and :format)
72
+ image["EXIF:BitsPerSample"] # It also can get all the EXIF tags
73
+ image["%m:%f %wx%h"] # Or you can use one of the many options of the format command
74
+
75
+ For more on the format command see
76
+ http://www.imagemagick.org/script/command-line-options.php#format
77
+
78
+
79
+ == Requirements
80
+
81
+ You must have ImageMagick or GraphicsMagick installed.
data/Rakefile CHANGED
@@ -1,17 +1,32 @@
1
- # -*- ruby -*-
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
2
5
 
3
- require 'rubygems'
4
- require 'hoe'
5
- require './lib/mini_magick.rb'
6
+ $:.unshift(File.dirname(__FILE__) + "/lib")
7
+ require 'mini_magick'
6
8
 
7
- Hoe.new('mini_magick', MiniMagick::VERSION) do |p|
8
- p.rubyforge_name = 'mini_magick'
9
- p.author = 'Corey Johnson'
10
- p.email = 'probablycorey+ruby@gmail.com'
11
- p.summary = 'A simple image manipulation library based on ImageMagick.'
12
- p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
13
- #p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
14
- p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
9
+ desc 'Default: run unit tests.'
10
+ task :default => :test
11
+
12
+ desc 'Test the mini_magick plugin.'
13
+ Rake::TestTask.new(:test) do |t|
14
+ t.libs << 'test'
15
+ t.pattern = 'test/**/*_test.rb'
16
+ t.verbose = true
17
+ end
18
+
19
+ desc 'Generate documentation for the mini_magick plugin.'
20
+ Rake::RDocTask.new(:rdoc) do |rdoc|
21
+ rdoc.rdoc_dir = 'rdoc'
22
+ rdoc.title = 'MiniMagick'
23
+ rdoc.options << '--line-numbers'
24
+ rdoc.options << '--inline-source'
25
+ rdoc.rdoc_files.include('README.rdoc')
26
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
27
  end
16
28
 
17
- # vim: syntax=Ruby
29
+ spec = eval(File.read('mini_magick.gemspec'))
30
+ Rake::GemPackageTask.new(spec) do |pkg|
31
+ pkg.gem_spec = spec
32
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -0,0 +1,2 @@
1
+ require 'mini_magick'
2
+ MiniMagick.processor = :gm
data/lib/mini_magick.rb CHANGED
@@ -1,13 +1,16 @@
1
- require "open-uri"
2
- require "stringio"
3
- require "fileutils"
4
-
5
- require File.join(File.dirname(__FILE__), '/image_temp_file')
1
+ require 'tempfile'
2
+ require 'subexec'
6
3
 
7
4
  module MiniMagick
8
- class MiniMagickError < RuntimeError; end
5
+ class << self
6
+ attr_accessor :processor
7
+ attr_accessor :timeout
8
+ end
9
+
10
+ MOGRIFY_COMMANDS = %w{adaptive-blur adaptive-resize adaptive-sharpen adjoin affine alpha annotate antialias append authenticate auto-gamma auto-level auto-orient background bench iterations bias black-threshold blue-primary point blue-shift factor blur border bordercolor brightness-contrast caption string cdl filename channel type charcoal radius chop clip clamp clip-mask filename clip-path id clone index clut contrast-stretch coalesce colorize color-matrix colors colorspace type combine comment string compose operator composite compress type contrast convolve coefficients crop cycle amount decipher filename debug events define format:option deconstruct delay delete index density depth despeckle direction type display server dispose method distort type coefficients dither method draw string edge radius emboss radius encipher filename encoding type endian type enhance equalize evaluate operator evaluate-sequence operator extent extract family name fft fill filter type flatten flip floodfill flop font name format string frame function name fuzz distance fx expression gamma gaussian-blur geometry gravity type green-primary point help identify ifft implode amount insert index intent type interlace type interline-spacing interpolate method interword-spacing kerning label string lat layers method level limit type linear-stretch liquid-rescale log format loop iterations mask filename mattecolor median radius modulate monitor monochrome morph morphology method kernel motion-blur negate noise radius normalize opaque ordered-dither NxN orient type page paint radius ping pointsize polaroid angle posterize levels precision preview type print string process image-filter profile filename quality quantizespace quiet radial-blur angle raise random-threshold low,high red-primary point regard-warnings region remap filename render repage resample resize respect-parentheses roll rotate degrees sample sampling-factor scale scene seed segments selective-blur separate sepia-tone threshold set attribute shade degrees shadow sharpen shave shear sigmoidal-contrast size sketch solarize threshold splice spread radius strip stroke strokewidth stretch type style type swap indexes swirl degrees texture filename threshold thumbnail tile filename tile-offset tint transform transparent transparent-color transpose transverse treedepth trim type type undercolor unique-colors units type unsharp verbose version view vignette virtual-pixel method wave weight type white-point point white-threshold write filename}
9
11
 
10
- VERSION = '1.2.3'
12
+ class Error < RuntimeError; end
13
+ class Invalid < StandardError; end
11
14
 
12
15
  class Image
13
16
  attr :path
@@ -16,25 +19,30 @@ module MiniMagick
16
19
 
17
20
  # Class Methods
18
21
  # -------------
19
- class <<self
20
- def from_blob(blob, extension=nil)
22
+ class << self
23
+ def from_blob(blob, ext = nil)
21
24
  begin
22
- tempfile = ImageTempFile.new("minimagick#{extension}")
25
+ tempfile = Tempfile.new(['mini_magick', ext.to_s])
23
26
  tempfile.binmode
24
27
  tempfile.write(blob)
25
28
  ensure
26
- tempfile.close
29
+ tempfile.close if tempfile
27
30
  end
28
-
29
- return self.new(tempfile.path, tempfile)
31
+
32
+ image = self.new(tempfile.path, tempfile)
33
+ if !image.valid?
34
+ raise MiniMagick::Invalid
35
+ end
36
+ image
30
37
  end
31
38
 
32
39
  # Use this if you don't want to overwrite the image file
33
- def from_file(image_path)
40
+ def open(image_path)
34
41
  File.open(image_path, "rb") do |f|
35
42
  self.from_blob(f.read, File.extname(image_path))
36
43
  end
37
44
  end
45
+ alias_method :from_file, :open
38
46
  end
39
47
 
40
48
  # Instance Methods
@@ -42,9 +50,13 @@ module MiniMagick
42
50
  def initialize(input_path, tempfile=nil)
43
51
  @path = input_path
44
52
  @tempfile = tempfile # ensures that the tempfile will stick around until this image is garbage collected.
45
-
46
- # Ensure that the file is an image
53
+ end
54
+
55
+ def valid?
47
56
  run_command("identify", @path)
57
+ true
58
+ rescue MiniMagick::Invalid
59
+ false
48
60
  end
49
61
 
50
62
  # For reference see http://www.imagemagick.org/script/command-line-options.php#format
@@ -57,22 +69,58 @@ module MiniMagick
57
69
  run_command("identify", "-format", format_option("%h"), @path).split("\n")[0].to_i
58
70
  when "width"
59
71
  run_command("identify", "-format", format_option("%w"), @path).split("\n")[0].to_i
72
+ when "dimensions"
73
+ run_command("identify", "-format", format_option("%w %h"), @path).split("\n")[0].split.map{|v|v.to_i}
74
+ when "size"
75
+ File.size(@path) # Do this because calling identify -format "%b" on an animated gif fails!
60
76
  when "original_at"
61
77
  # Get the EXIF original capture as a Time object
62
78
  Time.local(*self["EXIF:DateTimeOriginal"].split(/:|\s+/)) rescue nil
63
79
  when /^EXIF\:/i
64
- run_command('identify', '-format', "\"%[#{value}]\"", @path).chop
80
+ result = run_command('identify', '-format', "\"%[#{value}]\"", @path).chop
81
+ if result.include?(",")
82
+ read_character_data(result)
83
+ else
84
+ result
85
+ end
65
86
  else
66
87
  run_command('identify', '-format', "\"#{value}\"", @path).split("\n")[0]
67
88
  end
68
89
  end
69
90
 
91
+ # Sends raw commands to imagemagick's mogrify command. The image path is automatically appended to the command
92
+ def <<(*args)
93
+ run_command("mogrify", *args << @path)
94
+ end
95
+
70
96
  # This is a 'special' command because it needs to change @path to reflect the new extension
71
- def format(format)
97
+ # Formatting an animation into a non-animated type will result in ImageMagick creating multiple
98
+ # pages (starting with 0). You can choose which page you want to manipulate. We default to the
99
+ # first page.
100
+ def format(format, page=0)
72
101
  run_command("mogrify", "-format", format, @path)
73
- @path = @path.sub(/(\.\w+)?$/, ".#{format}")
74
-
75
- raise "Unable to format to #{format}" unless File.exists?(@path)
102
+
103
+ old_path = @path.dup
104
+ @path.sub!(/(\.\w*)?$/, ".#{format}")
105
+ File.delete(old_path) unless old_path == @path
106
+
107
+ unless File.exists?(@path)
108
+ begin
109
+ FileUtils.copy_file(@path.sub(".#{format}", "-#{page}.#{format}"), @path)
110
+ rescue => ex
111
+ raise MiniMagickError, "Unable to format to #{format}; #{ex}" unless File.exist?(@path)
112
+ end
113
+ end
114
+ ensure
115
+ Dir[@path.sub(/(\.\w+)?$/, "-[0-9]*.#{format}")].each do |fname|
116
+ File.unlink(fname)
117
+ end
118
+ end
119
+
120
+ # Collapse images with sequences to the first frame (ie. animated gifs) and
121
+ # preserve quality
122
+ def collapse!
123
+ run_command("mogrify", "-quality", "100", "#{path}[0]")
76
124
  end
77
125
 
78
126
  # Writes the temporary image that we are using for processing to the output path
@@ -83,28 +131,57 @@ module MiniMagick
83
131
 
84
132
  # Give you raw data back
85
133
  def to_blob
86
- File.read @path
134
+ f = File.new @path
135
+ f.binmode
136
+ f.read
137
+ ensure
138
+ f.close if f
87
139
  end
88
140
 
89
141
  # If an unknown method is called then it is sent through the morgrify program
90
142
  # Look here to find all the commands (http://www.imagemagick.org/script/mogrify.php)
91
143
  def method_missing(symbol, *args)
92
- args.push(@path) # push the path onto the end
93
- run_command("mogrify", "-#{symbol}", *args)
94
- self
144
+ guessed_command_name = symbol.to_s.gsub('_','-')
145
+
146
+ if MOGRIFY_COMMANDS.include?(guessed_command_name)
147
+ args.push(@path) # push the path onto the end
148
+ run_command("mogrify", "-#{guessed_command_name}", *args)
149
+ self
150
+ else
151
+ super(symbol, *args)
152
+ end
95
153
  end
96
154
 
97
155
  # You can use multiple commands together using this method
98
156
  def combine_options(&block)
99
- c = CommandBuilder.new
157
+ c = CommandBuilder.new('mogrify')
100
158
  block.call c
101
- run_command("mogrify", *c.args << @path)
159
+ c << @path
160
+ run(c)
102
161
  end
103
162
 
104
163
  # Check to see if we are running on win32 -- we need to escape things differently
105
164
  def windows?
106
165
  !(RUBY_PLATFORM =~ /win32/).nil?
107
166
  end
167
+
168
+ def composite(other_image, output_extension = 'jpg', &block)
169
+ begin
170
+ tempfile = Tempfile.new(output_extension)
171
+ tempfile.binmode
172
+ ensure
173
+ tempfile.close
174
+ end
175
+
176
+ command = CommandBuilder.new("composite")
177
+ block.call(command) if block
178
+ command.push(other_image.path)
179
+ command.push(self.path)
180
+ command.push(tempfile.path)
181
+
182
+ run(command)
183
+ return Image.new(tempfile.path)
184
+ end
108
185
 
109
186
  # Outputs a carriage-return delimited format string for Unix and Windows
110
187
  def format_option(format)
@@ -112,36 +189,89 @@ module MiniMagick
112
189
  end
113
190
 
114
191
  def run_command(command, *args)
115
- args.collect! do |arg|
116
- arg = arg.to_s
117
- arg = %|"#{arg}"| unless arg[0] == ?- # values quoted because they can contain characters like '>', but don't quote switches
118
- arg
119
- end
192
+ run(CommandBuilder.new(command, *args))
193
+ end
194
+
195
+ def run(command_builder)
196
+ command = command_builder.command
120
197
 
121
- @output = `#{command} #{args.join(' ')}`
198
+ sub = Subexec.run(command, :timeout => MiniMagick.timeout)
122
199
 
123
- if $? != 0
124
- raise MiniMagickError, "ImageMagick command (#{command} #{args.join(' ')}) failed: Error Given #{$?}"
200
+ if sub.exitstatus != 0
201
+ # Clean up after ourselves in case of an error
202
+ destroy!
203
+
204
+ # Raise the appropriate error
205
+ if sub.output =~ /no decode delegate/i || sub.output =~ /did not return an image/i
206
+ raise Invalid, sub.output
207
+ else
208
+ # TODO: should we do something different if the command times out ...?
209
+ # its definitely better for logging.. otherwise we dont really know
210
+ raise Error, "Command (#{command.inspect}) failed: #{{:status_code => sub.exitstatus, :output => sub.output}.inspect}"
211
+ end
125
212
  else
126
- @output
213
+ sub.output
127
214
  end
128
215
  end
216
+
217
+ def destroy!
218
+ return if tempfile.nil?
219
+ File.unlink(tempfile.path)
220
+ @tempfile = nil
221
+ end
222
+
223
+ private
224
+ # Sometimes we get back a list of character values
225
+ def read_character_data(list_of_characters)
226
+ chars = list_of_characters.gsub(" ", "").split(",")
227
+ result = ""
228
+ chars.each do |val|
229
+ result << ("%c" % val.to_i)
230
+ end
231
+ result
232
+ end
129
233
  end
130
234
 
131
235
  class CommandBuilder
132
236
  attr :args
237
+ attr :command
133
238
 
134
- def initialize
239
+ def initialize(command, *options)
240
+ @command = command
135
241
  @args = []
242
+
243
+ options.each { |val| push(val) }
136
244
  end
137
-
245
+
246
+ def command
247
+ "#{MiniMagick.processor} #{@command} #{@args.join(' ')}".strip
248
+ end
249
+
138
250
  def method_missing(symbol, *args)
139
- @args << "-#{symbol}"
140
- @args += args
251
+ guessed_command_name = symbol.to_s.gsub('_','-')
252
+ if MOGRIFY_COMMANDS.include?(guessed_command_name)
253
+ # This makes sure we always quote if we are passed a single
254
+ # arguement with spaces in it
255
+ if (args.size == 1) && (args.first.to_s.include?(' '))
256
+ push("-#{guessed_command_name}")
257
+ push(args.join(" "))
258
+ else
259
+ push("-#{guessed_command_name} #{args.join(" ")}")
260
+ end
261
+ else
262
+ super(symbol, *args)
263
+ end
141
264
  end
142
265
 
266
+ def push(value)
267
+ # args can contain characters like '>' so we must escape them, but don't quote switches
268
+ @args << ((value !~ /^[\+\-]/) ? "\"#{value}\"" : value.to_s.strip)
269
+ end
270
+ alias :<< :push
271
+
143
272
  def +(value)
144
- @args << "+#{value}"
273
+ puts "MINI_MAGICK: The + command has been deprecated. Please use c << '+#{value}')"
274
+ push "+#{value}"
145
275
  end
146
276
  end
147
277
  end
Binary file
Binary file
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require File.expand_path('../../lib/mini_magick', __FILE__)
4
+
5
+ class CommandBuilderTest < Test::Unit::TestCase
6
+ include MiniMagick
7
+
8
+ def test_basic
9
+ c = CommandBuilder.new("test")
10
+ c.resize "30x40"
11
+ assert_equal "-resize 30x40", c.args.join(" ")
12
+ end
13
+
14
+ def test_complicated
15
+ c = CommandBuilder.new("test")
16
+ c.resize "30x40"
17
+ c.alpha 1, 3, 4
18
+ c.resize "mome fingo"
19
+ assert_equal "-resize 30x40 -alpha 1 3 4 -resize \"mome fingo\"", c.args.join(" ")
20
+ end
21
+
22
+ def test_valid_command
23
+ begin
24
+ c = CommandBuilder.new("test", "path")
25
+ c.input 2
26
+ assert false
27
+ rescue NoMethodError
28
+ assert true
29
+ end
30
+ end
31
+
32
+ def test_full_command
33
+ c = CommandBuilder.new("test")
34
+ c.resize "30x40"
35
+ c.alpha 1, 3, 4
36
+ c.resize "mome fingo"
37
+ assert_equal "test -resize 30x40 -alpha 1 3 4 -resize \"mome fingo\"", c.command
38
+ end
39
+
40
+ def test_dashed
41
+ c = CommandBuilder.new("test")
42
+ c.auto_orient
43
+ assert_equal "-auto-orient", c.args.join(" ")
44
+ end
45
+ end
Binary file