quick_magick_hooopo 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ Manifest
2
+ README.rdoc
3
+ Rakefile
4
+ lib/quick_magick.rb
5
+ lib/quick_magick/image.rb
6
+ lib/quick_magick/image_list.rb
7
+ quick_magick_hooopo.gemspec
8
+ test/9.gif
9
+ test/badfile.xxx
10
+ test/image_list_test.rb
11
+ test/image_test.rb
12
+ test/multipage.tif
13
+ test/test_magick.rb
14
+ test/test_quick_magick.rb
15
+ test/warnings.tiff
@@ -0,0 +1,195 @@
1
+ = Quick Magick
2
+
3
+ == What is QuickMagick
4
+ QuickMagick is a gem built by BadrIT (http://www.badrit.com) for easily accessing ImageMagick command line tools from Ruby programs.
5
+
6
+ == When to use QuickMagick
7
+ QuickMagick is a library that allows you to create and manipulate images.
8
+ When you are faced with a problem that requires high quality manipulation of images you can use QuickMagick.
9
+ Use QuickMagick to:
10
+ * Check uploaded images dimensions.
11
+ * Generate captchas.
12
+ * Generate graphical reports and charts.
13
+ * Convert uploaded images formats.
14
+ * Display pdfs as images.
15
+
16
+ == Features
17
+ * Open an existing image from disk and determine basic info like width, height.
18
+ * Open an image from blob. For example, an image read from an upload form.
19
+ * Do basic and advanced operations on images like resize, rotate, shear, motion blur and other.
20
+ * Create an image from scratch and draw basic elements on it like line, circle and text. This allows making captchas for example.
21
+ * Combine images using ImageList to make a multipage or animated images.
22
+ * API is very simple and powerful.
23
+ * Minimizes disk access by calling command line tools only when required.
24
+
25
+ == How to install
26
+ First, you should install ImageMagick (http://www.imagemagick.org/) on your machine.
27
+ Command line tools of ImageMagick must be in your system path.
28
+ You can check this by running the command:
29
+ identify -version
30
+ Now to install QuickMagick just type at your command line:
31
+ gem install quick_magick
32
+ ... and it's done.
33
+ You don't have to install any libraries or compile code from source.
34
+
35
+ == What is different?
36
+ But what's different from other gems like RMagick and mini-magick?
37
+
38
+ The story begins when I was working on a project at BadrIT (http://www.badrit.com) using Ruby on Rails.
39
+ In this projects users were uploading images in many formats like pdf, tiff and png.
40
+ We were using Flex as a front end to display and annotate these images.
41
+ Flex has a limitation in that it can open images up to 8192x8192.
42
+ Unfortunately, users were uploading images much larger than this.
43
+ Another issue is that Flex can only open .jpg, .png and .gif files.
44
+ The solution was to convert all images uploaded to one of these formats and resizing them down to at most 8192x8192.
45
+
46
+ First, I used ImageMagick as a command line tool and was calling it using system calls.
47
+ This accomplished the work perfectly but my source code was a rubbish.
48
+ It has many lines of code to handle creating temporary files and accessing multipage tiff and pdf files.
49
+ I found RMagick at that time and decided to use it.
50
+ It caused the code to be much simple without affecting performance notably.
51
+ It worked with me well while I was using my application with test images.
52
+ Once I decided to test it with real images it failed.
53
+ For example, when I transform a tiff image from 14400x9600 to 8192x8192 while transforming it to gif, my machine runs out of memory (2GB RAM).
54
+ When I tried to make the same operation from command line using (convert) it was working much better.
55
+ It did not finish in a second but at least it worked correctly.
56
+ The solution was to return back to command line.
57
+
58
+ I searched for alternatives and found MiniMagick.
59
+ MiniMagick is a gem that allows you to perform basic operations using ImageMagick through command line tools.
60
+ I tried to use it but it was not good enough.
61
+ First, it writes temporary images and files as it is working which makes it slow for nothing.
62
+ Second, it doesn't handle multipage images.
63
+ I tried it with a .pdf file and I found that it handled the first page only.
64
+ Third, it doesn't give an API to draw images from scratch.
65
+ Actually, I didn't need this feature, but I may need it in the future.
66
+
67
+ At this point I decided to make my own gem and QuickMagick was born.
68
+ I addressed the problems of MiniMagick while using the same main idea.
69
+ First, QuickMagick doesn't write any temporary image files to disk.
70
+ It doesn't issue any command line commands till the very end when you are saving the image.
71
+ Second, I made an API similar to RMagick which allows for accessing multipage images.
72
+ Third, I added API commands to create images from scratch and drawing simple primitives on images.
73
+ I tested my gem, compared it to MiniMagick and RMagick and it pleased me.
74
+
75
+ == Comparison
76
+ I've made some test benches to compare the speed of QuickMagick, MiniMagick and RMagick.
77
+ All denoted numbers are in seconds.
78
+ Here are the results:
79
+
80
+ ===Test 1: resize a normal image
81
+ user system total real
82
+ mini 0.030000 0.040000 3.640000 ( 3.585617)
83
+ quick 0.010000 0.030000 3.330000 ( 3.295369)
84
+ rmagick 1.680000 1.660000 3.340000 ( 3.150202)
85
+
86
+ It's clear that QuickMagick is faster than MiniMagick.
87
+ RMagick was the fastest as it accesses the requested operations directly without the need to load an executable file or parse a command line.
88
+
89
+ ===Test 2: resize a large image
90
+ user system total real
91
+ mini 0.000000 0.040000 57.150000 (130.609229)
92
+ quick 0.010000 0.010000 56.510000 ( 58.426361)
93
+
94
+ Again QuickMagick is faster than MiniMagick.
95
+ However, RMagick has failed to pass this test.
96
+ It kept working and eating memory, cpu and harddisk till I had to unplug my computer to stop it.
97
+ So, I removed it from this test bench.
98
+
99
+ ===Test 3: generate random captchas
100
+ user system total real
101
+ quick 0.000000 0.000000 0.290000 ( 3.623418)
102
+ rmagick 0.150000 0.120000 0.270000 ( 3.171975)
103
+
104
+ In this last test, RMagick was about 12% faster than QuickMagick.
105
+ This is normal because it works in memory and doesn't have to parse a command line string to know what to draw.
106
+ I couldn't test MiniMagick for this because it doesn't support an API for drawing functions.
107
+
108
+ == Conclusion
109
+ QuickMagick is very easy to install, very easy to use and allows you to access most features of ImageMagick.
110
+ RMagick is a bit faster but it's a bit hard to install.
111
+ Also RMagick sometimes fail when it tries to handle large images.
112
+ MiniMagick is proved to be slower than QuickMagick with no advantage.
113
+ So, it's better to use QuickMagick when your application is not image-centric.
114
+ This means, you're not going to build an image manipulation tool or something like this.
115
+ For normal operations like resize, rotate, generating captchas ... etc, QuickMagick will be a good friend of you.
116
+
117
+ == Examples
118
+ Determine image information
119
+ i = QuickMagick::Image.read('test.jpg').first
120
+ i.width # Retrieves width in pixels
121
+ i.height # Retrieves height in pixels
122
+
123
+ Resize an image
124
+ i = QuickMagick::Image.read('test.jpg').first
125
+ i.resize "300x300!"
126
+ i.save "resized_image.jpg"
127
+
128
+ or
129
+ i.append_to_operators 'resize', "300x300!"
130
+
131
+ or
132
+ i.resize 300, 300, nil, nil, QuickMagick::AspectGeometry
133
+
134
+
135
+ Access multipage image
136
+ i = QuickMagick::Image.read("multipage.pdf") {|image| image.density = 300}
137
+ i.size # number of pages
138
+ i.each_with_index do |page, i|
139
+ i.save "page_#{i}.jpg"
140
+ end
141
+
142
+ From blob
143
+ i = QuickMagick::Image.from_blob(blob_data).first
144
+ i.sample "100x100!"
145
+ i.save
146
+
147
+ Modify a file (mogrify)
148
+ i = QuickMagick::Image.read('test.jpg')
149
+ i.resize "100x100!"
150
+ i.save!
151
+
152
+ You can also display an image to Xserver
153
+ i = QuickMagick::Image.read('test.jpg')
154
+ i.display
155
+
156
+ QuickMagick supports also ImageList s
157
+ # Batch convert a list of jpg files to gif while resizing them
158
+ il = QuickMagick::ImageList.new('test.jpg', 'test2.jpg')
159
+ il << QuickMagick::Image.read('test3.jpg')
160
+ il.format = 'gif'
161
+ il.resize "300x300>"
162
+ il.save!
163
+
164
+ You can also create images from scratch
165
+ # Create a 300x300 gradient image from yellow to red
166
+ i1 = QuickMagick::Image::gradient(300, 300, QuickMagick::RadialGradient, :yellow, :red)
167
+ i1.save 'gradient.png'
168
+
169
+ # Create a 100x200 CheckerBoard image
170
+ i1 = QuickMagick::Image::pattern(100, 200, :checkerboard)
171
+ i1.display
172
+
173
+ ... and draw primitives on them
174
+ i = QuickMagick::Image::solid(100, 100, :white)
175
+ i.draw_line(0,0,50,50)
176
+ i.draw_text(30, 30, "Hello world!", :rotate=>45)
177
+ i.save 'hello.jpg'
178
+
179
+ ... you can then convert it to blob using
180
+ i.to_blob
181
+
182
+ (new) You can now access single pixels using QuickMagick
183
+ # Create a 300x300 gradient image from yellow to red
184
+ i1 = QuickMagick::Image::gradient(300, 300, QuickMagick::RadialGradient, :yellow, :red)
185
+ i1.save 'gradient.png'
186
+
187
+ # Now you can access single pixel values
188
+ i1.get_pixel(10,10) # [255, 0, 0]
189
+ i1.get_pixel(50,50) # [255, 15, 0]
190
+
191
+ For more information on drawing API visit:
192
+ http://www.imagemagick.org/Usage/draw/
193
+
194
+ Check all command line options of ImageMagick at:
195
+ http://www.imagemagick.org/script/convert.php
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('quick_magick_hooopo', '0.8.1') do |p|
6
+ p.description = "QuickMagick allows you to access ImageMagick command line functions using Ruby interface."
7
+ p.summary = "A gem build by BadrIT to access ImageMagick command line functions easily and quickly"
8
+ p.url = "http://quickmagick.rubyforge.org/"
9
+ p.author = "Ahmed ElDawy"
10
+ p.email = "ahmed.eldawy@badrit.com"
11
+ p.project = "quickmagick"
12
+ end
13
+
14
+ #Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
15
+
@@ -0,0 +1,208 @@
1
+ # Define quick magick error
2
+ module QuickMagick
3
+ class QuickMagickError < RuntimeError; end
4
+ end
5
+
6
+ # check if ImageMagick is installed
7
+ begin
8
+ x = `identify -version 2>&1`
9
+ raise(QuickMagick::QuickMagickError, "ImageMagick not installed") unless x.index('ImageMagick')
10
+ rescue Errno::ENOENT
11
+ # For Windows machines
12
+ raise(QuickMagick::QuickMagickError, "ImageMagick not installed")
13
+ end
14
+
15
+ module QuickMagick
16
+ # Normally the attributes are treated as pixels.
17
+ # Use this flag when the width and height attributes represent percentages.
18
+ # For example, 125x75 means 125% of the height and 75% of the width.
19
+ # The x and y attributes are not affected by this flag.
20
+ PercentGeometry = "%"
21
+
22
+ # Use this flag when you want to force the new image to have exactly the size specified by the the width and height attributes.
23
+ AspectGeometry = "!"
24
+
25
+ # Use this flag when you want to change the size of the image only if both its width and height
26
+ # are smaller the values specified by those attributes. The image size is changed proportionally.
27
+ LessGeometry = "<"
28
+
29
+ # Use this flag when you want to change the size of the image if either its width and height
30
+ # exceed the values specified by those attributes. The image size is changed proportionally.
31
+ GreaterGeometry = ">"
32
+
33
+ # This flag is useful only with a single width attribute.
34
+ # When present, it means the width attribute represents the total area of the image in pixels.
35
+ AreaGeometry = "@"
36
+
37
+ # Use ^ to set a minimum image size limit.
38
+ # The geometry 640x480^, for example, means the image width will not be less than 640 and
39
+ # the image height will not be less than 480 pixels after the resize.
40
+ # One of those dimensions will match the requested size,
41
+ # but the image will likely overflow the space requested to preserve its aspect ratio.
42
+ MinimumGeometry = "^"
43
+
44
+ # Command for solid color
45
+ SolidColor = "xc"
46
+ # Command for linear gradient
47
+ LinearGradient = "gradient"
48
+ # Command for radial gradient
49
+ RadialGradient = "radial-gradient"
50
+
51
+ # Different possible patterns
52
+ Patterns = %w{bricks checkerboard circles crosshatch crosshatch30 crosshatch45 fishscales} +
53
+ (0..20).collect {|level| "gray#{level}" } +
54
+ %w{hexagons horizontal horizontalsaw hs_bdiagonal hs_cross hs_diagcross hs_fdiagonal hs_horizontal
55
+ hs_vertical left30 left45 leftshingle octagons right30 right45 rightshingle smallfishscales
56
+ vertical verticalbricks verticalleftshingle verticalrightshingle verticalsaw}
57
+
58
+ %w{NorthWest North NorthEast West Center East SouthWest South SouthEast}.each do |gravity_type|
59
+ const_set gravity_type+"Gravity", gravity_type
60
+ end
61
+
62
+
63
+ class << self
64
+ # Generate a random string of specified length.
65
+ # Used to generate random names for temp files
66
+ def random_string(length=10)
67
+ @@CHARS ||= ("a".."z").to_a + ("1".."9").to_a
68
+ Array.new(length, '').collect{@@CHARS[rand(@@CHARS.size)]}.join
69
+ end
70
+
71
+ def verbose
72
+ defined?(@verbose) && @verbose
73
+ end
74
+
75
+ def verbose=(flag)
76
+ @verbose = flag
77
+ end
78
+
79
+ # Encodes a geometry string with the given options
80
+ def geometry(width, height=nil, x=nil, y=nil, flag=nil)
81
+ geometry_string = ""
82
+ geometry_string << width.to_s if width
83
+ geometry_string << 'x' << height.to_s if height
84
+ geometry_string << '+' << x.to_s if x
85
+ geometry_string << '+' << y.to_s if y
86
+ geometry_string << flag if flag
87
+ geometry_string
88
+ end
89
+
90
+ # Returns a formatted string for the color with the given components
91
+ # each component could take one of the following values
92
+ # * an integer from 0 to 255
93
+ # * a float from 0.0 to 1.0
94
+ # * a string showing percentage from "0%" to "100%"
95
+ def rgba_color(red, green, blue, alpha=255)
96
+ "#%02x%02x%02x%02x" % [red, green, blue, alpha].collect do |component|
97
+ case component
98
+ when Integer then component
99
+ when Float then Integer(component*255)
100
+ when String then Integer(component.sub('%', '')) * 255 / 100
101
+ end
102
+ end
103
+ end
104
+
105
+ alias rgb_color rgba_color
106
+
107
+ # Returns a formatted string for a gray color with the given level and alpha.
108
+ # level and alpha could take one of the following values
109
+ # * an integer from 0 to 255
110
+ # * a float from 0.0 to 1.0
111
+ # * a string showing percentage from "0%" to "100%"
112
+ def graya_color(level, alpha=255)
113
+ rgba_color(level, level, level, alpha)
114
+ end
115
+
116
+ alias gray_color graya_color
117
+
118
+ # HSL colors are encoding as a triple (hue, saturation, lightness).
119
+ # Hue is represented as an angle of the color circle (i.e. the rainbow represented in a circle).
120
+ # This angle is so typically measured in degrees that the unit is implicit in CSS;
121
+ # syntactically, only a number is given. By definition red=0=360,
122
+ # and the other colors are spread around the circle, so green=120, blue=240, etc.
123
+ # As an angle, it implicitly wraps around such that -120=240 and 480=120, for instance.
124
+ # (Students of trigonometry would say that "coterminal angles are equivalent" here;
125
+ # an angle (theta) can be standardized by computing the equivalent angle, (theta) mod 360.)
126
+ #
127
+ # Saturation and lightness are represented as percentages.
128
+ # 100% is full saturation, and 0% is a shade of grey.
129
+ # 0% lightness is black, 100% lightness is white, and 50% lightness is 'normal'.
130
+ #
131
+ # Hue can take one of the following values:
132
+ # * an integer from 0...360 representing angle in degrees
133
+ # * a float value from 0...2*PI represeting angle in radians
134
+ #
135
+ # saturation, lightness and alpha can take one of the following values:
136
+ # * an integer from 0 to 255
137
+ # * a float from 0.0 to 1.0
138
+ # * a string showing percentage from "0%" to "100%"
139
+ def hsla_color(hue, saturation, lightness, alpha=1.0)
140
+ components = [case hue
141
+ when Integer then hue
142
+ when Float then Integer(hue * 360 / 2 / Math::PI)
143
+ end]
144
+ components += [saturation, lightness].collect do |component|
145
+ case component
146
+ when Integer then (component * 100.0 / 255).round
147
+ when Float then Integer(component*100)
148
+ when String then Integer(component.sub('%', ''))
149
+ end
150
+ end
151
+ components << case alpha
152
+ when Integer then alpha * 100.0 / 255
153
+ when Float then alpha
154
+ when String then Float(alpha.sub('%', '')) / 100.0
155
+ end
156
+ "hsla(%d,%d%%,%d%%,%g)" % components
157
+ end
158
+
159
+ alias hsl_color hsla_color
160
+
161
+ # Escapes possible special chracters in command line
162
+ # by surrounding it with double quotes
163
+ def escape_commandline(str)
164
+ str =~ /^(\w|\.)*$/ ? str : "\"#{str}\""
165
+ end
166
+
167
+ alias c escape_commandline
168
+
169
+ # Execute a command line and returns its results when it suceeds
170
+ # When the command fails, it throws QuickMagick::QuickMagickError
171
+ def exec3(command)
172
+ error_file = Tempfile.new('error')
173
+ error_filepath = error_file.path
174
+ error_file.close
175
+ puts "Run Command -> #{command}" if verbose
176
+ result = `#{command} 2>"#{error_filepath}"`
177
+ unless $?.success?
178
+ error_message = <<-ERROR
179
+ Error executing command: command
180
+ Result is: #{result}
181
+ Error is: #{File.read(error_filepath)}
182
+ ERROR
183
+ raise QuickMagick::QuickMagickError, error_message
184
+ end
185
+ result
186
+ end
187
+ end
188
+ end
189
+
190
+ # For backward compatibility with ruby < 1.8.7
191
+ unless "".respond_to? :start_with?
192
+ class String
193
+ def start_with?(x)
194
+ self.index(x) == 0
195
+ end
196
+ end
197
+ end
198
+
199
+ unless "".respond_to? :end_with?
200
+ class String
201
+ def end_with?(x)
202
+ self.index(x) == self.length - 1
203
+ end
204
+ end
205
+ end
206
+
207
+ require 'quick_magick/image'
208
+ require 'quick_magick/image_list'
@@ -0,0 +1,495 @@
1
+ require "tempfile"
2
+
3
+ module QuickMagick
4
+
5
+ class Image
6
+ class << self
7
+
8
+ # create an array of images from the given blob data
9
+ def from_blob(blob, &proc)
10
+ file = Tempfile.new(QuickMagick::random_string)
11
+ file.binmode
12
+ file.write(blob)
13
+ file.close
14
+ self.read(file.path, &proc)
15
+ end
16
+
17
+ # create an array of images from the given file
18
+ def read(filename, &proc)
19
+ info = identify(filename)
20
+ info_lines = info.split(/[\r\n]/)
21
+ images = []
22
+ info_lines.each_with_index do |info_line, i|
23
+ images << Image.new(filename, i, info_line)
24
+ end
25
+ images.each(&proc) if block_given?
26
+ return images
27
+ end
28
+
29
+ alias open read
30
+
31
+ # Creates a new image initially set to gradient
32
+ # Default gradient is linear gradient from black to white
33
+ def gradient(width, height, type=QuickMagick::LinearGradient, color1=nil, color2=nil)
34
+ template_name = type + ":"
35
+ template_name << color1.to_s if color1
36
+ template_name << '-' << color2.to_s if color2
37
+ i = self.new(template_name, 0, nil, true)
38
+ i.size = QuickMagick::geometry(width, height)
39
+ yield(i) if block_given?
40
+ i
41
+ end
42
+
43
+ # Creates an image with solid color
44
+ def solid(width, height, color=nil)
45
+ template_name = QuickMagick::SolidColor+":"
46
+ template_name << color.to_s if color
47
+ i = self.new(template_name, 0, nil, true)
48
+ i.size = QuickMagick::geometry(width, height)
49
+ yield(i) if block_given?
50
+ i
51
+ end
52
+
53
+ # Creates an image from pattern
54
+ def pattern(width, height, pattern)
55
+ raise QuickMagick::QuickMagickError, "Invalid pattern '#{pattern.to_s}'" unless QuickMagick::Patterns.include?(pattern.to_s)
56
+ template_name = "pattern:#{pattern.to_s}"
57
+ i = self.new(template_name, 0, nil, true)
58
+ i.size = QuickMagick::geometry(width, height)
59
+ yield(i) if block_given?
60
+ i
61
+ end
62
+
63
+ # returns info for an image using <code>identify</code> command
64
+ def identify(filename)
65
+ QuickMagick.exec3 "identify #{QuickMagick.c filename}"
66
+ end
67
+
68
+ end
69
+
70
+ # append the given option, value pair to the settings of the current image
71
+ def append_to_settings(arg, value=nil)
72
+ @arguments << "-#{arg} #{QuickMagick.c value} "
73
+ @last_is_draw = false
74
+ self
75
+ end
76
+
77
+ # append the given string as is. Used to append special arguments like +antialias or +debug
78
+ def append_basic(arg)
79
+ @arguments << arg << ' '
80
+ end
81
+
82
+ # Image settings supported by ImageMagick
83
+ IMAGE_SETTINGS_METHODS = %w{
84
+ adjoin affine alpha authenticate attenuate background bias black-point-compensation
85
+ blue-primary bordercolor caption channel colors colorspace comment compose compress define
86
+ delay depth display dispose dither encoding endian family fill filter font format fuzz gravity
87
+ green-primary intent interlace interpolate interword-spacing kerning label limit loop mask
88
+ mattecolor monitor orient ping pointsize preview quality quiet red-primary regard-warnings
89
+ remap respect-parentheses scene seed stretch stroke strokewidth style taint texture treedepth
90
+ transparent-color undercolor units verbose view virtual-pixel weight white-point
91
+
92
+ density page sampling-factor size tile-offset
93
+ }
94
+
95
+ # append the given option, value pair to the args for the current image
96
+ def append_to_operators(arg, value=nil)
97
+ is_draw = (arg == 'draw')
98
+ if @last_is_draw && is_draw
99
+ @arguments.insert(@arguments.rindex('"'), " #{value}")
100
+ else
101
+ @arguments << %Q<-#{arg} #{QuickMagick.c value} >
102
+ end
103
+ @last_is_draw = is_draw
104
+ self
105
+ end
106
+
107
+ # Reverts this image to its last saved state.
108
+ # Note that you cannot revert an image created from scratch.
109
+ def revert!
110
+ raise QuickMagick::QuickMagickError, "Cannot revert a pseudo image" if @pseudo_image
111
+ @arguments = ""
112
+ end
113
+
114
+ # Image operators supported by ImageMagick
115
+ IMAGE_OPERATORS_METHODS = %w{
116
+ alpha auto-orient bench black-threshold bordercolor charcoal clip clip-mask clip-path colorize
117
+ contrast convolve cycle decipher deskew despeckle distort edge encipher emboss enhance equalize
118
+ evaluate flip flop function gamma identify implode layers level level-colors median modulate monochrome
119
+ negate noise normalize opaque ordered-dither NxN paint polaroid posterize print profile quantize
120
+ radial-blur Raise random-threshold recolor render rotate segment sepia-tone set shade solarize
121
+ sparse-color spread strip swirl threshold tile tint transform transparent transpose transverse trim
122
+ type unique-colors white-threshold
123
+
124
+ adaptive-blur adaptive-resize adaptive-sharpen annotate blur border chop contrast-stretch extent
125
+ extract frame gaussian-blur geometry lat linear-stretch liquid-rescale motion-blur region repage
126
+ resample resize roll sample scale selective-blur shadow sharpen shave shear sigmoidal-contrast
127
+ sketch splice thumbnail unsharp vignette wave
128
+
129
+ append average clut coalesce combine composite deconstruct flatten fx hald-clut morph mosaic process reverse separate write
130
+ crop
131
+ }
132
+
133
+ # methods that are called with (=)
134
+ WITH_EQUAL_METHODS =
135
+ %w{alpha background bias black-point-compensation blue-primary border bordercolor caption
136
+ cahnnel colors colorspace comment compose compress depth density encoding endian family fill filter
137
+ font format frame fuzz geometry gravity label mattecolor page pointsize quality stroke strokewidth
138
+ undercolor units weight
139
+ brodercolor transparent type size}
140
+
141
+ # methods that takes geometry options
142
+ WITH_GEOMETRY_METHODS =
143
+ %w{density page sampling-factor size tile-offset adaptive-blur adaptive-resize adaptive-sharpen
144
+ annotate blur border chop contrast-stretch extent extract frame gaussian-blur
145
+ geometry lat linear-stretch liquid-rescale motion-blur region repage resample resize roll
146
+ sample scale selective-blur shadow sharpen shave shear sigmoidal-contrast sketch
147
+ splice thumbnail unsharp vignette wave crop}
148
+
149
+ # Methods that need special treatment. This array is used just to keep track of them.
150
+ SPECIAL_COMMANDS =
151
+ %w{floodfill antialias draw}
152
+
153
+ IMAGE_SETTINGS_METHODS.each do |method|
154
+ if WITH_EQUAL_METHODS.include?(method)
155
+ define_method((method+'=').to_sym) do |arg|
156
+ append_to_settings(method, arg)
157
+ end
158
+ elsif WITH_GEOMETRY_METHODS.include?(method)
159
+ define_method((method).to_sym) do |*args|
160
+ append_to_settings(method, QuickMagick::geometry(*args) )
161
+ end
162
+ else
163
+ define_method(method.to_sym) do |*args|
164
+ append_to_settings(method, args.join(" "))
165
+ end
166
+ end
167
+ end
168
+
169
+ IMAGE_OPERATORS_METHODS.each do |method|
170
+ if WITH_EQUAL_METHODS.include?(method)
171
+ define_method((method+'=').to_sym) do |arg|
172
+ append_to_operators(method, arg )
173
+ end
174
+ elsif WITH_GEOMETRY_METHODS.include?(method)
175
+ define_method((method).to_sym) do |*args|
176
+ append_to_operators(method, QuickMagick::geometry(*args) )
177
+ end
178
+ else
179
+ define_method(method.to_sym) do |*args|
180
+ append_to_operators(method, args.join(" "))
181
+ end
182
+ end
183
+ end
184
+
185
+ # Fills a rectangle with a solid color
186
+ def floodfill(width, height=nil, x=nil, y=nil, flag=nil, color=nil)
187
+ append_to_operators "floodfill", QuickMagick::geometry(width, height, x, y, flag), color
188
+ end
189
+
190
+ # Enables/Disables flood fill. Pass a boolean argument.
191
+ def antialias=(flag)
192
+ append_basic flag ? '-antialias' : '+antialias'
193
+ end
194
+
195
+ # define attribute readers (getters)
196
+ attr_reader :image_filename
197
+ alias original_filename image_filename
198
+
199
+ # constructor
200
+ def initialize(filename, index=0, info_line=nil, pseudo_image=false)
201
+ @image_filename = filename
202
+ @index = index
203
+ @pseudo_image = pseudo_image
204
+ if info_line
205
+ @image_infoline = info_line.split
206
+ @image_infoline[0..1] = @image_infoline[0..1].join(' ') while @image_infoline.size > 1 && !@image_infoline[0].start_with?(image_filename)
207
+ end
208
+ @arguments = ""
209
+ end
210
+
211
+ # The command line so far that will be used to convert or save the image
212
+ def command_line
213
+ %Q< "(" #{@arguments} #{QuickMagick.c(image_filename + (@pseudo_image ? "" : "[#{@index}]"))} ")" >
214
+ end
215
+
216
+ # An information line about the image obtained using 'identify' command line
217
+ def image_infoline
218
+ return nil if @pseudo_image
219
+ unless @image_infoline
220
+ @image_infoline = QuickMagick::Image::identify(command_line).split
221
+ @image_infoline[0..1] = @image_infoline[0..1].join(' ') while @image_infoline.size > 1 && !@image_infoline[0].start_with?(image_filename)
222
+ end
223
+ @image_infoline
224
+ end
225
+
226
+ # converts options passed to any primitive to a string that can be passed to ImageMagick
227
+ # options allowed are:
228
+ # * rotate degrees
229
+ # * translate dx,dy
230
+ # * scale sx,sy
231
+ # * skewX degrees
232
+ # * skewY degrees
233
+ # * gravity NorthWest, North, NorthEast, West, Center, East, SouthWest, South, or SouthEast
234
+ # * stroke color
235
+ # * fill color
236
+ # The rotate primitive rotates subsequent shape primitives and text primitives about the origin of the main image.
237
+ # If you set the region before the draw command, the origin for transformations is the upper left corner of the region.
238
+ # The translate primitive translates subsequent shape and text primitives.
239
+ # The scale primitive scales them.
240
+ # The skewX and skewY primitives skew them with respect to the origin of the main image or the region.
241
+ # The text gravity primitive only affects the placement of text and does not interact with the other primitives.
242
+ # It is equivalent to using the gravity method, except that it is limited in scope to the draw_text option in which it appears.
243
+ def options_to_str(options)
244
+ options.to_a.flatten.join " "
245
+ end
246
+
247
+ # Converts an array of coordinates to a string that can be passed to polygon, polyline and bezier
248
+ def points_to_str(points)
249
+ raise QuickMagick::QuickMagickError, "Points must be an even number of coordinates" if points.size.odd?
250
+ points_str = ""
251
+ points.each_slice(2) do |point|
252
+ points_str << point.join(",") << " "
253
+ end
254
+ points_str
255
+ end
256
+
257
+ # The shape primitives are drawn in the color specified by the preceding -fill setting.
258
+ # For unfilled shapes, use -fill none.
259
+ # You can optionally control the stroke (the "outline" of a shape) with the -stroke and -strokewidth settings.
260
+
261
+ # draws a point at the given location in pixels
262
+ # A point primitive is specified by a single point in the pixel plane, that is, by an ordered pair
263
+ # of integer coordinates, x,y.
264
+ # (As it involves only a single pixel, a point primitive is not affected by -stroke or -strokewidth.)
265
+ def draw_point(x, y, options={})
266
+ append_to_operators("draw", "#{options_to_str(options)} point #{x},#{y}")
267
+ end
268
+
269
+ # draws a line between the given two points
270
+ # A line primitive requires a start point and end point.
271
+ def draw_line(x0, y0, x1, y1, options={})
272
+ append_to_operators("draw", "#{options_to_str(options)} line #{x0},#{y0} #{x1},#{y1}")
273
+ end
274
+
275
+ # draw a rectangle with the given two corners
276
+ # A rectangle primitive is specified by the pair of points at the upper left and lower right corners.
277
+ def draw_rectangle(x0, y0, x1, y1, options={})
278
+ append_to_operators("draw", "#{options_to_str(options)} rectangle #{x0},#{y0} #{x1},#{y1}")
279
+ end
280
+
281
+ # draw a rounded rectangle with the given two corners
282
+ # wc and hc are the width and height of the arc
283
+ # A roundRectangle primitive takes the same corner points as a rectangle
284
+ # followed by the width and height of the rounded corners to be removed.
285
+ def draw_round_rectangle(x0, y0, x1, y1, wc, hc, options={})
286
+ append_to_operators("draw", "#{options_to_str(options)} roundRectangle #{x0},#{y0} #{x1},#{y1} #{wc},#{hc}")
287
+ end
288
+
289
+ # The arc primitive is used to inscribe an elliptical segment in to a given rectangle.
290
+ # An arc requires the two corners used for rectangle (see above) followed by
291
+ # the start and end angles of the arc of the segment segment (e.g. 130,30 200,100 45,90).
292
+ # The start and end points produced are then joined with a line segment and the resulting segment of an ellipse is filled.
293
+ def draw_arc(x0, y0, x1, y1, a0, a1, options={})
294
+ append_to_operators("draw", "#{options_to_str(options)} arc #{x0},#{y0} #{x1},#{y1} #{a0},#{a1}")
295
+ end
296
+
297
+ # Use ellipse to draw a partial (or whole) ellipse.
298
+ # Give the center point, the horizontal and vertical "radii"
299
+ # (the semi-axes of the ellipse) and start and end angles in degrees (e.g. 100,100 100,150 0,360).
300
+ def draw_ellipse(x0, y0, rx, ry, a0, a1, options={})
301
+ append_to_operators("draw", "#{options_to_str(options)} ellipse #{x0},#{y0} #{rx},#{ry} #{a0},#{a1}")
302
+ end
303
+
304
+ # The circle primitive makes a disk (filled) or circle (unfilled). Give the center and any point on the perimeter (boundary).
305
+ def draw_circle(x0, y0, x1, y1, options={})
306
+ append_to_operators("draw", "#{options_to_str(options)} circle #{x0},#{y0} #{x1},#{y1}")
307
+ end
308
+
309
+ # The polyline primitive requires three or more points to define their perimeters.
310
+ # A polyline is simply a polygon in which the final point is not stroked to the start point.
311
+ # When unfilled, this is a polygonal line. If the -stroke setting is none (the default), then a polyline is identical to a polygon.
312
+ # points - A single array with each pair forming a coordinate in the form (x, y).
313
+ # e.g. [0,0,100,100,100,0] will draw a polyline between points (0,0)-(100,100)-(100,0)
314
+ def draw_polyline(points, options={})
315
+ append_to_operators("draw", "#{options_to_str(options)} polyline #{points_to_str(points)}")
316
+ end
317
+
318
+ # The polygon primitive requires three or more points to define their perimeters.
319
+ # A polyline is simply a polygon in which the final point is not stroked to the start point.
320
+ # When unfilled, this is a polygonal line. If the -stroke setting is none (the default), then a polyline is identical to a polygon.
321
+ # points - A single array with each pair forming a coordinate in the form (x, y).
322
+ # e.g. [0,0,100,100,100,0] will draw a polygon between points (0,0)-(100,100)-(100,0)
323
+ def draw_polygon(points, options={})
324
+ append_to_operators("draw", "#{options_to_str(options)} polygon #{points_to_str(points)}")
325
+ end
326
+
327
+ # The Bezier primitive creates a spline curve and requires three or points to define its shape.
328
+ # The first and last points are the knots and these points are attained by the curve,
329
+ # while any intermediate coordinates are control points.
330
+ # If two control points are specified, the line between each end knot and its sequentially
331
+ # respective control point determines the tangent direction of the curve at that end.
332
+ # If one control point is specified, the lines from the end knots to the one control point
333
+ # determines the tangent directions of the curve at each end.
334
+ # If more than two control points are specified, then the additional control points
335
+ # act in combination to determine the intermediate shape of the curve.
336
+ # In order to draw complex curves, it is highly recommended either to use the path primitive
337
+ # or to draw multiple four-point bezier segments with the start and end knots of each successive segment repeated.
338
+ def draw_bezier(points, options={})
339
+ append_to_operators("draw", "#{options_to_str(options)} bezier #{points_to_str(points)}")
340
+ end
341
+
342
+ # A path represents an outline of an object, defined in terms of moveto
343
+ # (set a new current point), lineto (draw a straight line), curveto (draw a Bezier curve),
344
+ # arc (elliptical or circular arc) and closepath (close the current shape by drawing a
345
+ # line to the last moveto) elements.
346
+ # Compound paths (i.e., a path with subpaths, each consisting of a single moveto followed by
347
+ # one or more line or curve operations) are possible to allow effects such as donut holes in objects.
348
+ # (See http://www.w3.org/TR/SVG/paths.html)
349
+ def draw_path(path_spec, options={})
350
+ append_to_operators("draw", "#{options_to_str(options)} path #{path_spec}")
351
+ end
352
+
353
+ # Use image to composite an image with another image. Follow the image keyword
354
+ # with the composite operator, image location, image size, and filename
355
+ # You can use 0,0 for the image size, which means to use the actual dimensions found in the image header.
356
+ # Otherwise, it is scaled to the given dimensions. See -compose for a description of the composite operators.
357
+ def draw_image(operator, x0, y0, w, h, image_filename, options={})
358
+ append_to_operators("draw", "#{options_to_str(options)} image #{operator} #{x0},#{y0} #{w},#{h} \"#{image_filename}\"")
359
+ end
360
+
361
+ # Use text to annotate an image with text. Follow the text coordinates with a string.
362
+ def draw_text(x0, y0, text, options={})
363
+ append_to_operators("draw", "#{options_to_str(options)} text #{x0},#{y0} '#{text.gsub('"', '\"').gsub("'", "\\\\'")}'")
364
+ end
365
+
366
+ # saves the current image to the given filename
367
+ def save(output_filename)
368
+ result = QuickMagick.exec3 "convert #{command_line} #{QuickMagick.c output_filename}"
369
+ if @pseudo_image
370
+ # since it's been saved, convert it to normal image (not pseudo)
371
+ initialize(output_filename)
372
+ revert!
373
+ end
374
+ return result
375
+ end
376
+
377
+ alias write save
378
+ alias convert save
379
+
380
+ # saves the current image overwriting the original image file
381
+ def save!
382
+ raise QuickMagick::QuickMagickError, "Cannot mogrify a pseudo image" if @pseudo_image
383
+ result = QuickMagick.exec3 "mogrify #{command_line}"
384
+ # remove all operations to avoid duplicate operations
385
+ revert!
386
+ return result
387
+ end
388
+
389
+ alias write! save!
390
+ alias mogrify! save!
391
+
392
+ def to_blob
393
+ tmp_file = Tempfile.new(QuickMagick::random_string)
394
+ if command_line =~ /-format\s(\S+)\s/
395
+ # use format set up by user
396
+ blob_format = $1
397
+ elsif !@pseudo_image
398
+ # use original image format
399
+ blob_format = self.format
400
+ else
401
+ # default format is jpg
402
+ blob_format = 'jpg'
403
+ end
404
+ save "#{blob_format}:#{tmp_file.path}"
405
+ blob = nil
406
+ File.open(tmp_file.path, 'rb') { |f| blob = f.read}
407
+ blob
408
+ end
409
+
410
+ # image file format
411
+ def format
412
+ image_infoline[1]
413
+ end
414
+
415
+ # columns of image in pixels
416
+ def columns
417
+ image_infoline[2].split('x').first.to_i
418
+ end
419
+
420
+ alias width columns
421
+
422
+ # rows of image in pixels
423
+ def rows
424
+ image_infoline[2].split('x').last.to_i
425
+ end
426
+
427
+ alias height rows
428
+
429
+ # Bit depth
430
+ def bit_depth
431
+ image_infoline[4].to_i
432
+ end
433
+
434
+ # Number of different colors used in this image
435
+ def colors
436
+ image_infoline[6].to_i
437
+ end
438
+
439
+ # returns size of image in bytes
440
+ def size
441
+ File.size?(image_filename)
442
+ end
443
+
444
+ # Reads a pixel from the image.
445
+ # WARNING: This is done through command line which is very slow.
446
+ # It is not recommended at all to use this method for image processing for example.
447
+ def get_pixel(x, y)
448
+ result = QuickMagick.exec3("identify -verbose -crop #{QuickMagick::geometry(1,1,x,y)} #{QuickMagick.c image_filename}[#{@index}]")
449
+ result =~ /Histogram:\s*\d+:\s*\(\s*(\d+),\s*(\d+),\s*(\d+)\)/
450
+ return [$1.to_i, $2.to_i, $3.to_i]
451
+ end
452
+
453
+ # Returns details of the image as found by "identify -verbose" command
454
+ def details
455
+ str_details = QuickMagick.exec3("identify -verbose #{QuickMagick.c image_filename}[#@index]")
456
+ # This is something like breadcrumb for hashes visited at any time
457
+ hash_stack = []
458
+ # Current indentation. Used to infer nesting using indentation
459
+ current_indent = ""
460
+ # Last key inserted in the hash. Used to determine which key is the parent of a nested hash
461
+ last_key = nil
462
+ # Start with top level empty hash
463
+ hash_stack.push({})
464
+ str_details.each_line do |line|
465
+ key, value = line.split(":", 2)
466
+ key_indent = key[/^\s*/]
467
+ if key_indent.length < current_indent.length
468
+ # Must go one level up
469
+ hash_stack.pop
470
+ elsif key_indent.length > current_indent.length
471
+ # Must go one level deeper using last key
472
+ hash_stack.last[last_key] = {}
473
+ hash_stack.push hash_stack.last[last_key]
474
+ end
475
+ current_indent = key_indent
476
+
477
+ key = key.strip
478
+ value = value.strip
479
+ hash_stack.last[key] = value
480
+ last_key = key
481
+ end
482
+ hash_stack.first
483
+ end
484
+
485
+ # displays the current image as animated image
486
+ def animate
487
+ `animate #{command_line}`
488
+ end
489
+
490
+ # displays the current image to the x-windowing system
491
+ def display
492
+ `display #{command_line}`
493
+ end
494
+ end
495
+ end