aseldawy-quick_magick 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ README
2
+ Rakefile
3
+ lib/quick_magick/image.rb
4
+ lib/quick_magick/image_list.rb
5
+ lib/quick_magick.rb
6
+ Manifest
7
+ quick_magick.gemspec
8
+ test/image_test.rb
9
+ test/badfile.xxx
10
+ test/multipage.tif
11
+ test/image_list_test.rb
12
+ test/test_magick.rb
13
+ test/9.gif
data/README ADDED
@@ -0,0 +1,186 @@
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 images 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 and has an advantage of allowing you to access single pixels 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 a bit 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 generate 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
+ (new) 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
+ For more information on drawing API visit:
183
+ http://www.imagemagick.org/Usage/draw/
184
+
185
+ Check all command line options of ImageMagick at:
186
+ http://www.imagemagick.org/script/convert.php
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('quick_magick', '0.6.0') do |p|
6
+ p.description = "QuickMagick allows you to access ImageMagick command line functions using Ruby interface."
7
+ p.url = "http://quickmagick.rubyforge.org/"
8
+ p.author = "Ahmed ElDawy"
9
+ p.email = "ahmed.eldawy@badrit.com"
10
+ p.project = "quickmagick"
11
+ end
12
+
13
+ #Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
14
+
@@ -0,0 +1,176 @@
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
+
59
+ class << self
60
+ # Generate a random string of specified length.
61
+ # Used to generate random names for temp files
62
+ def random_string(length=10)
63
+ @@CHARS ||= ("a".."z").to_a + ("1".."9").to_a
64
+ Array.new(length, '').collect{@@CHARS[rand(@@CHARS.size)]}.join
65
+ end
66
+
67
+ # Encodes a geometry string with the given options
68
+ def geometry(width, height=nil, x=nil, y=nil, flag=nil)
69
+ geometry_string = ""
70
+ geometry_string << width.to_s if width
71
+ geometry_string << 'x' << height.to_s if height
72
+ geometry_string << '+' << x.to_s if x
73
+ geometry_string << '+' << y.to_s if y
74
+ geometry_string << flag if flag
75
+ geometry_string
76
+ end
77
+
78
+ # Returns a formatted string for the color with the given components
79
+ # each component could take one of the following values
80
+ # * an integer from 0 to 255
81
+ # * a float from 0.0 to 1.0
82
+ # * a string showing percentage from "0%" to "100%"
83
+ def rgba_color(red, green, blue, alpha=255)
84
+ "#%02x%02x%02x%02x" % [red, green, blue, alpha].collect do |component|
85
+ case component
86
+ when Integer then component
87
+ when Float then Integer(component*255)
88
+ when String then Integer(component.sub('%', '')) * 255 / 100
89
+ end
90
+ end
91
+ end
92
+
93
+ alias rgb_color rgba_color
94
+
95
+ # Returns a formatted string for a gray color with the given level and alpha.
96
+ # level and alpha could take one of the following values
97
+ # * an integer from 0 to 255
98
+ # * a float from 0.0 to 1.0
99
+ # * a string showing percentage from "0%" to "100%"
100
+ def graya_color(level, alpha=255)
101
+ rgba_color(level, level, level, alpha)
102
+ end
103
+
104
+ alias gray_color graya_color
105
+
106
+ # HSL colors are encoding as a triple (hue, saturation, lightness).
107
+ # Hue is represented as an angle of the color circle (i.e. the rainbow represented in a circle).
108
+ # This angle is so typically measured in degrees that the unit is implicit in CSS;
109
+ # syntactically, only a number is given. By definition red=0=360,
110
+ # and the other colors are spread around the circle, so green=120, blue=240, etc.
111
+ # As an angle, it implicitly wraps around such that -120=240 and 480=120, for instance.
112
+ # (Students of trigonometry would say that "coterminal angles are equivalent" here;
113
+ # an angle (theta) can be standardized by computing the equivalent angle, (theta) mod 360.)
114
+ #
115
+ # Saturation and lightness are represented as percentages.
116
+ # 100% is full saturation, and 0% is a shade of grey.
117
+ # 0% lightness is black, 100% lightness is white, and 50% lightness is 'normal'.
118
+ #
119
+ # Hue can take one of the following values:
120
+ # * an integer from 0...360 representing angle in degrees
121
+ # * a float value from 0...2*PI represeting angle in radians
122
+ #
123
+ # saturation, lightness and alpha can take one of the following values:
124
+ # * an integer from 0 to 255
125
+ # * a float from 0.0 to 1.0
126
+ # * a string showing percentage from "0%" to "100%"
127
+ def hsla_color(hue, saturation, lightness, alpha=1.0)
128
+ components = [case hue
129
+ when Integer then hue
130
+ when Float then Integer(hue * 360 / 2 / Math::PI)
131
+ end]
132
+ components += [saturation, lightness].collect do |component|
133
+ case component
134
+ when Integer then (component * 100.0 / 255).round
135
+ when Float then Integer(component*100)
136
+ when String then Integer(component.sub('%', ''))
137
+ end
138
+ end
139
+ components << case alpha
140
+ when Integer then alpha * 100.0 / 255
141
+ when Float then alpha
142
+ when String then Float(alpha.sub('%', '')) / 100.0
143
+ end
144
+ "hsla(%d,%d%%,%d%%,%g)" % components
145
+ end
146
+
147
+ alias hsl_color hsla_color
148
+
149
+ # Escapes possible special chracters in command line by surrounding it with double quotes
150
+ def escape_commandline(str)
151
+ str =~ /^(\w|\s|\.)*$/ ? str : "\"#{str}\""
152
+ end
153
+
154
+ alias c escape_commandline
155
+ end
156
+ end
157
+
158
+ # For backward compatibility with ruby < 1.8.7
159
+ unless "".respond_to? :start_with?
160
+ class String
161
+ def start_with?(x)
162
+ self.index(x) == 0
163
+ end
164
+ end
165
+ end
166
+
167
+ unless "".respond_to? :end_with?
168
+ class String
169
+ def end_with?(x)
170
+ self.index(x) == self.length - 1
171
+ end
172
+ end
173
+ end
174
+
175
+ require 'quick_magick/image'
176
+ require 'quick_magick/image_list'
@@ -0,0 +1,458 @@
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(%Q<"#{filename}">)
20
+ info_lines = info.split(/[\r\n]/)
21
+ images = []
22
+ if info_lines.size == 1
23
+ images << Image.new(filename, info_lines.first)
24
+ else
25
+ info_lines.each_with_index do |info_line, i|
26
+ images << Image.new("#{filename}[#{i.to_s}]", info_line)
27
+ end
28
+ end
29
+ images.each(&proc) if block_given?
30
+ return images
31
+ end
32
+
33
+ alias open read
34
+
35
+ # Creates a new image initially set to gradient
36
+ # Default gradient is linear gradient from black to white
37
+ def gradient(width, height, type=QuickMagick::LinearGradient, color1=nil, color2=nil)
38
+ template_name = type + ":"
39
+ template_name << color1.to_s if color1
40
+ template_name << '-' << color2.to_s if color2
41
+ i = self.new(template_name, nil, true)
42
+ i.size = QuickMagick::geometry(width, height)
43
+ i
44
+ end
45
+
46
+ # Creates an image with solid color
47
+ def solid(width, height, color=nil)
48
+ template_name = QuickMagick::SolidColor+":"
49
+ template_name << color.to_s if color
50
+ i = self.new(template_name, nil, true)
51
+ i.size = QuickMagick::geometry(width, height)
52
+ i
53
+ end
54
+
55
+ # Creates an image from pattern
56
+ def pattern(width, height, pattern)
57
+ raise QuickMagick::QuickMagickError, "Invalid pattern '#{pattern.to_s}'" unless QuickMagick::Patterns.include?(pattern.to_s)
58
+ template_name = "pattern:#{pattern.to_s}"
59
+ i = self.new(template_name, nil, true)
60
+ i.size = QuickMagick::geometry(width, height)
61
+ i
62
+ end
63
+
64
+ # returns info for an image using <code>identify</code> command
65
+ def identify(filename)
66
+ result = `identify #{filename} 2>&1`
67
+ unless $?.success?
68
+ raise QuickMagick::QuickMagickError, "Illegal file \"#{filename}\""
69
+ end
70
+ result
71
+ end
72
+
73
+ end
74
+
75
+ # append the given option, value pair to the settings of the current image
76
+ def append_to_settings(arg, value=nil)
77
+ @arguments << %Q<-#{arg} #{QuickMagick::c value} >
78
+ @last_is_draw = false
79
+ self
80
+ end
81
+
82
+ # Image settings supported by ImageMagick
83
+ IMAGE_SETTINGS_METHODS = %w{
84
+ adjoin affine alpha antialias 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 antialias 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 floodfill 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
+ IMAGE_SETTINGS_METHODS.each do |method|
150
+ if WITH_EQUAL_METHODS.include?(method)
151
+ define_method((method+'=').to_sym) do |arg|
152
+ append_to_settings(method, arg)
153
+ end
154
+ elsif WITH_GEOMETRY_METHODS.include?(method)
155
+ define_method((method).to_sym) do |*args|
156
+ append_to_settings(method, QuickMagick::geometry(*args) )
157
+ end
158
+ else
159
+ define_method(method.to_sym) do |*args|
160
+ append_to_settings(method, args.join(" "))
161
+ end
162
+ end
163
+ end
164
+
165
+ IMAGE_OPERATORS_METHODS.each do |method|
166
+ if WITH_EQUAL_METHODS.include?(method)
167
+ define_method((method+'=').to_sym) do |arg|
168
+ append_to_operators(method, arg )
169
+ end
170
+ elsif WITH_GEOMETRY_METHODS.include?(method)
171
+ define_method((method).to_sym) do |*args|
172
+ append_to_operators(method, QuickMagick::geometry(*args) )
173
+ end
174
+ else
175
+ define_method(method.to_sym) do |*args|
176
+ append_to_operators(method, args.join(" "))
177
+ end
178
+ end
179
+ end
180
+
181
+ # Fills a rectangle with a solid color
182
+ def floodfill(width, height=nil, x=nil, y=nil, flag=nil, color=nil)
183
+ append_to_operators "floodfill", QuickMagick::geometry(width, height, x, y, flag), color
184
+ end
185
+
186
+ # define attribute readers (getters)
187
+ attr_reader :image_filename
188
+ alias original_filename image_filename
189
+
190
+ # constructor
191
+ def initialize(filename, info_line=nil, pseudo_image=false)
192
+ @image_filename = filename
193
+ @pseudo_image = pseudo_image
194
+ if info_line
195
+ @image_infoline = info_line.split
196
+ @image_infoline[0..1] = @image_infoline[0..1].join(' ') while @image_infoline.size > 1 && !@image_infoline[0].start_with?(image_filename)
197
+ end
198
+ @arguments = ""
199
+ end
200
+
201
+ # The command line so far that will be used to convert or save the image
202
+ def command_line
203
+ %Q< "(" #{@arguments} #{QuickMagick::c image_filename} ")" >
204
+ end
205
+
206
+ # An information line about the image obtained using 'identify' command line
207
+ def image_infoline
208
+ return nil if @pseudo_image
209
+ unless @image_infoline
210
+ @image_infoline = QuickMagick::Image::identify(command_line).split
211
+ @image_infoline[0..1] = @image_infoline[0..1].join(' ') while @image_infoline.size > 1 && !@image_infoline[0].start_with?(image_filename)
212
+ end
213
+ @image_infoline
214
+ end
215
+
216
+ # converts options passed to any primitive to a string that can be passed to ImageMagick
217
+ # options allowed are:
218
+ # * rotate degrees
219
+ # * translate dx,dy
220
+ # * scale sx,sy
221
+ # * skewX degrees
222
+ # * skewY degrees
223
+ # * gravity NorthWest, North, NorthEast, West, Center, East, SouthWest, South, or SouthEast
224
+ # * stroke color
225
+ # * fill color
226
+ # The rotate primitive rotates subsequent shape primitives and text primitives about the origin of the main image.
227
+ # If you set the region before the draw command, the origin for transformations is the upper left corner of the region.
228
+ # The translate primitive translates subsequent shape and text primitives.
229
+ # The scale primitive scales them.
230
+ # The skewX and skewY primitives skew them with respect to the origin of the main image or the region.
231
+ # The text gravity primitive only affects the placement of text and does not interact with the other primitives.
232
+ # It is equivalent to using the gravity method, except that it is limited in scope to the draw_text option in which it appears.
233
+ def options_to_str(options)
234
+ options.to_a.flatten.join " "
235
+ end
236
+
237
+ # Converts an array of coordinates to a string that can be passed to polygon, polyline and bezier
238
+ def points_to_str(points)
239
+ raise QuickMagick::QuickMagickError, "Points must be an even number of coordinates" if points.size.odd?
240
+ points_str = ""
241
+ points.each_slice(2) do |point|
242
+ points_str << point.join(",") << " "
243
+ end
244
+ points_str
245
+ end
246
+
247
+ # The shape primitives are drawn in the color specified by the preceding -fill setting.
248
+ # For unfilled shapes, use -fill none.
249
+ # You can optionally control the stroke (the "outline" of a shape) with the -stroke and -strokewidth settings.
250
+
251
+ # draws a point at the given location in pixels
252
+ # A point primitive is specified by a single point in the pixel plane, that is, by an ordered pair
253
+ # of integer coordinates, x,y.
254
+ # (As it involves only a single pixel, a point primitive is not affected by -stroke or -strokewidth.)
255
+ def draw_point(x, y, options={})
256
+ append_to_operators("draw", "#{options_to_str(options)} point #{x},#{y}")
257
+ end
258
+
259
+ # draws a line between the given two points
260
+ # A line primitive requires a start point and end point.
261
+ def draw_line(x0, y0, x1, y1, options={})
262
+ append_to_operators("draw", "#{options_to_str(options)} line #{x0},#{y0} #{x1},#{y1}")
263
+ end
264
+
265
+ # draw a rectangle with the given two corners
266
+ # A rectangle primitive is specified by the pair of points at the upper left and lower right corners.
267
+ def draw_rectangle(x0, y0, x1, y1, options={})
268
+ append_to_operators("draw", "#{options_to_str(options)} rectangle #{x0},#{y0} #{x1},#{y1}")
269
+ end
270
+
271
+ # draw a rounded rectangle with the given two corners
272
+ # wc and hc are the width and height of the arc
273
+ # A roundRectangle primitive takes the same corner points as a rectangle
274
+ # followed by the width and height of the rounded corners to be removed.
275
+ def draw_round_rectangle(x0, y0, x1, y1, wc, hc, options={})
276
+ append_to_operators("draw", "#{options_to_str(options)} roundRectangle #{x0},#{y0} #{x1},#{y1} #{wc},#{hc}")
277
+ end
278
+
279
+ # The arc primitive is used to inscribe an elliptical segment in to a given rectangle.
280
+ # An arc requires the two corners used for rectangle (see above) followed by
281
+ # the start and end angles of the arc of the segment segment (e.g. 130,30 200,100 45,90).
282
+ # The start and end points produced are then joined with a line segment and the resulting segment of an ellipse is filled.
283
+ def draw_arc(x0, y0, x1, y1, a0, a1, options={})
284
+ append_to_operators("draw", "#{options_to_str(options)} arc #{x0},#{y0} #{x1},#{y1} #{a0},#{a1}")
285
+ end
286
+
287
+ # Use ellipse to draw a partial (or whole) ellipse.
288
+ # Give the center point, the horizontal and vertical "radii"
289
+ # (the semi-axes of the ellipse) and start and end angles in degrees (e.g. 100,100 100,150 0,360).
290
+ def draw_ellipse(x0, y0, rx, ry, a0, a1, options={})
291
+ append_to_operators("draw", "#{options_to_str(options)} ellipse #{x0},#{y0} #{rx},#{ry} #{a0},#{a1}")
292
+ end
293
+
294
+ # The circle primitive makes a disk (filled) or circle (unfilled). Give the center and any point on the perimeter (boundary).
295
+ def draw_circle(x0, y0, x1, y1, options={})
296
+ append_to_operators("draw", "#{options_to_str(options)} circle #{x0},#{y0} #{x1},#{y1}")
297
+ end
298
+
299
+ # The polyline primitive requires three or more points to define their perimeters.
300
+ # A polyline is simply a polygon in which the final point is not stroked to the start point.
301
+ # When unfilled, this is a polygonal line. If the -stroke setting is none (the default), then a polyline is identical to a polygon.
302
+ # points - A single array with each pair forming a coordinate in the form (x, y). e.g. [0,0,100,100,100,0] will draw a polyline between points (0,0)-(100,100)-(100,0)
303
+ def draw_polyline(points, options={})
304
+ append_to_operators("draw", "#{options_to_str(options)} polyline #{points_to_str(points)}")
305
+ end
306
+
307
+ # The polygon primitive requires three or more points to define their perimeters.
308
+ # A polyline is simply a polygon in which the final point is not stroked to the start point.
309
+ # When unfilled, this is a polygonal line. If the -stroke setting is none (the default), then a polyline is identical to a polygon.
310
+ # points - A single array with each pair forming a coordinate in the form (x, y). e.g. [0,0,100,100,100,0] will draw a polygon between points (0,0)-(100,100)-(100,0)
311
+ def draw_polygon(points, options={})
312
+ append_to_operators("draw", "#{options_to_str(options)} polygon #{points_to_str(points)}")
313
+ end
314
+
315
+ # The Bezier primitive creates a spline curve and requires three or points to define its shape.
316
+ # The first and last points are the knots and these points are attained by the curve,
317
+ # while any intermediate coordinates are control points.
318
+ # If two control points are specified, the line between each end knot and its sequentially
319
+ # respective control point determines the tangent direction of the curve at that end.
320
+ # If one control point is specified, the lines from the end knots to the one control point
321
+ # determines the tangent directions of the curve at each end.
322
+ # If more than two control points are specified, then the additional control points
323
+ # act in combination to determine the intermediate shape of the curve.
324
+ # In order to draw complex curves, it is highly recommended either to use the path primitive
325
+ # or to draw multiple four-point bezier segments with the start and end knots of each successive segment repeated.
326
+ def draw_bezier(points, options={})
327
+ append_to_operators("draw", "#{options_to_str(options)} bezier #{points_to_str(points)}")
328
+ end
329
+
330
+ # A path represents an outline of an object, defined in terms of moveto
331
+ # (set a new current point), lineto (draw a straight line), curveto (draw a Bezier curve),
332
+ # arc (elliptical or circular arc) and closepath (close the current shape by drawing a
333
+ # line to the last moveto) elements.
334
+ # Compound paths (i.e., a path with subpaths, each consisting of a single moveto followed by
335
+ # one or more line or curve operations) are possible to allow effects such as donut holes in objects.
336
+ # (See http://www.w3.org/TR/SVG/paths.html)
337
+ def draw_path(path_spec, options={})
338
+ append_to_operators("draw", "#{options_to_str(options)} path #{path_spec}")
339
+ end
340
+
341
+ # Use image to composite an image with another image. Follow the image keyword
342
+ # with the composite operator, image location, image size, and filename
343
+ # You can use 0,0 for the image size, which means to use the actual dimensions found in the image header.
344
+ # Otherwise, it is scaled to the given dimensions. See -compose for a description of the composite operators.
345
+ def draw_image(operator, x0, y0, w, h, image_filename, options={})
346
+ append_to_operators("draw", "#{options_to_str(options)} image #{operator} #{x0},#{y0} #{w},#{h} \"#{image_filename}\"")
347
+ end
348
+
349
+ # Use text to annotate an image with text. Follow the text coordinates with a string.
350
+ def draw_text(x0, y0, text, options={})
351
+ append_to_operators("draw", "#{options_to_str(options)} text #{x0},#{y0} '#{text}'")
352
+ end
353
+
354
+ # saves the current image to the given filename
355
+ def save(output_filename)
356
+ result = `convert #{command_line} "#{output_filename}" 2>&1`
357
+ if $?.success?
358
+ if @pseudo_image
359
+ # since it's been saved, convert it to normal image (not pseudo)
360
+ initialize(output_filename)
361
+ revert!
362
+ end
363
+ return result
364
+ else
365
+ error_message = <<-ERROR
366
+ Error executing command: convert #{command_line} "#{output_filename}"
367
+ Result is: #{result}
368
+ ERROR
369
+ raise QuickMagick::QuickMagickError, error_message
370
+ end
371
+ end
372
+
373
+ alias write save
374
+ alias convert save
375
+
376
+ # saves the current image overwriting the original image file
377
+ def save!
378
+ raise QuickMagick::QuickMagickError, "Cannot mogrify a pseudo image" if @pseudo_image
379
+ result = `mogrify #{command_line}`
380
+ if $?.success?
381
+ # remove all operations to avoid duplicate operations
382
+ revert!
383
+ return result
384
+ else
385
+ error_message = <<-ERRORMSG
386
+ Error executing command: mogrify #{command_line}
387
+ Result is: #{result}
388
+ ERRORMSG
389
+ raise QuickMagick::QuickMagickError, error_message
390
+ end
391
+ end
392
+
393
+ alias write! save!
394
+ alias mogrify! save!
395
+
396
+ def to_blob
397
+ tmp_file = Tempfile.new(QuickMagick::random_string)
398
+ if command_line =~ /-format\s(\S+)\s/
399
+ # use format set up by user
400
+ blob_format = $1
401
+ elsif !@pseudo_image
402
+ # use original image format
403
+ blob_format = self.format
404
+ else
405
+ # default format is jpg
406
+ blob_format = 'jpg'
407
+ end
408
+ save "#{blob_format}:#{tmp_file.path}"
409
+ blob = nil
410
+ File.open(tmp_file.path, 'rb') { |f| blob = f.read}
411
+ blob
412
+ end
413
+
414
+ # image file format
415
+ def format
416
+ image_infoline[1]
417
+ end
418
+
419
+ # columns of image in pixels
420
+ def columns
421
+ image_infoline[2].split('x').first.to_i
422
+ end
423
+
424
+ alias width columns
425
+
426
+ # rows of image in pixels
427
+ def rows
428
+ image_infoline[2].split('x').last.to_i
429
+ end
430
+
431
+ alias height rows
432
+
433
+ # Bit depth
434
+ def bit_depth
435
+ image_infoline[4].to_i
436
+ end
437
+
438
+ # Number of different colors used in this image
439
+ def colors
440
+ image_infoline[6].to_i
441
+ end
442
+
443
+ # returns size of image in bytes
444
+ def size
445
+ File.size?(image_filename)
446
+ end
447
+
448
+ # displays the current image as animated image
449
+ def animate
450
+ `animate #{command_line}`
451
+ end
452
+
453
+ # displays the current image to the x-windowing system
454
+ def display
455
+ `display #{command_line}`
456
+ end
457
+ end
458
+ end