quick_magick_hooopo 0.8.1

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.
@@ -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