aseldawy-quick_magick 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +13 -0
- data/README +186 -0
- data/Rakefile +14 -0
- data/lib/quick_magick.rb +176 -0
- data/lib/quick_magick/image.rb +458 -0
- data/lib/quick_magick/image_list.rb +60 -0
- data/quick_magick.gemspec +31 -0
- data/test/9.gif +0 -0
- data/test/badfile.xxx +0 -0
- data/test/image_list_test.rb +114 -0
- data/test/image_test.rb +299 -0
- data/test/multipage.tif +0 -0
- data/test/test_magick.rb +169 -0
- metadata +76 -0
data/Manifest
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
+
|
data/lib/quick_magick.rb
ADDED
@@ -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
|