axon 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +9 -3
- data/README.rdoc +29 -36
- data/Rakefile +26 -21
- data/TODO.rdoc +1 -6
- data/ext/axon/axon.c +6 -15
- data/ext/axon/extconf.rb +19 -9
- data/ext/axon/interpolation.c +147 -0
- data/ext/axon/jpeg.c +1207 -0
- data/ext/axon/png.c +542 -0
- data/lib/axon.rb +235 -32
- data/lib/axon/cropper.rb +80 -18
- data/lib/axon/fit.rb +69 -19
- data/lib/axon/generators.rb +109 -0
- data/lib/axon/scalers.rb +160 -0
- data/test/helper.rb +151 -6
- data/test/reader_tests.rb +37 -82
- data/test/scaler_tests.rb +102 -0
- data/test/stress_helper.rb +58 -0
- data/test/stress_tests.rb +8 -5
- data/test/test_bilinear_scaler.rb +60 -2
- data/test/test_cropper.rb +68 -1
- data/test/test_fit.rb +35 -0
- data/test/test_generators.rb +21 -0
- data/test/test_image.rb +61 -0
- data/test/test_jpeg_reader.rb +96 -94
- data/test/test_jpeg_writer.rb +95 -8
- data/test/test_nearest_neighbor_scaler.rb +28 -4
- data/test/test_png_reader.rb +12 -8
- data/test/test_png_writer.rb +8 -6
- data/test/writer_tests.rb +129 -111
- metadata +71 -128
- data/.gemtest +0 -0
- data/ext/axon/bilinear_interpolation.c +0 -115
- data/ext/axon/interpolation.h +0 -7
- data/ext/axon/jpeg_common.c +0 -118
- data/ext/axon/jpeg_common.h +0 -37
- data/ext/axon/jpeg_native_writer.c +0 -248
- data/ext/axon/jpeg_reader.c +0 -774
- data/ext/axon/nearest_neighbor_interpolation.c +0 -50
- data/ext/axon/png_common.c +0 -21
- data/ext/axon/png_common.h +0 -18
- data/ext/axon/png_native_writer.c +0 -166
- data/ext/axon/png_reader.c +0 -381
- data/lib/axon/axon.so +0 -0
- data/lib/axon/bilinear_scaler.rb +0 -60
- data/lib/axon/jpeg_writer.rb +0 -41
- data/lib/axon/nearest_neighbor_scaler.rb +0 -39
- data/lib/axon/png_writer.rb +0 -35
- data/lib/axon/scaler.rb +0 -41
- data/lib/axon/solid.rb +0 -23
- data/test/_test_readme.rb +0 -34
- data/test/test_exif.rb +0 -39
- data/test/test_generator.rb +0 -10
- data/test/test_icc.rb +0 -18
- data/test/test_jpeg.rb +0 -9
- data/test/test_png.rb +0 -9
data/lib/axon.rb
CHANGED
@@ -1,45 +1,248 @@
|
|
1
|
-
|
1
|
+
# :main: README.rdoc
|
2
2
|
|
3
|
-
require 'axon/
|
3
|
+
require 'axon/axon'
|
4
4
|
require 'axon/cropper'
|
5
|
-
require 'axon/scaler'
|
6
|
-
require 'axon/nearest_neighbor_scaler'
|
7
|
-
require 'axon/bilinear_scaler'
|
8
5
|
require 'axon/fit'
|
9
|
-
|
10
|
-
require 'axon/
|
11
|
-
require 'axon/png_writer'
|
6
|
+
require 'axon/scalers'
|
7
|
+
require 'axon/generators'
|
12
8
|
|
13
9
|
module Axon
|
14
|
-
VERSION = '0.0
|
10
|
+
VERSION = '0.1.0'
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
12
|
+
# :call-seq:
|
13
|
+
# Axon.jpeg(thing [, markers]) -> image
|
14
|
+
#
|
15
|
+
# Reads a compressed JPEG image from +thing+. +thing+ can be an IO object or
|
16
|
+
# a string of JPEG data.
|
17
|
+
#
|
18
|
+
# +markers+ should be an array of valid JPEG header marker symbols. Valid
|
19
|
+
# symbols are :APP0 through :APP15 and :COM.
|
20
|
+
#
|
21
|
+
# If performance is important and you don't care about any markers, you can
|
22
|
+
# avaoid reading any header markers by supplying an empty array for +markers+.
|
23
|
+
#
|
24
|
+
# When +markers+ is not given, all known JPEG markers will be read.
|
25
|
+
#
|
26
|
+
# io_in = File.open("image.jpg", "r")
|
27
|
+
# image = Axon.jpeg(io_in) # Read JPEG from a StringIO
|
28
|
+
#
|
29
|
+
# jpeg_data = IO.read("image.jpg")
|
30
|
+
# image_2 = Axon.jpeg(jpeg_data) # Read JPEG from image data
|
31
|
+
#
|
32
|
+
# io_in = File.open("image.jpg", "r")
|
33
|
+
# image_3 = Axon.jpeg(io_in, [:APP2]) # Only reads the APP2 marker
|
34
|
+
#
|
35
|
+
def self.jpeg(thing, *args)
|
36
|
+
thing = StringIO.new(thing) unless thing.respond_to?(:read)
|
37
|
+
reader = JPEG::Reader.new(thing, *args)
|
38
|
+
Image.new(reader)
|
39
|
+
end
|
27
40
|
|
28
|
-
|
41
|
+
# :call-seq:
|
42
|
+
# Axon.png(thing) -> image
|
43
|
+
#
|
44
|
+
# Reads a compressed PNG image from +thing+. +thing+ can be an IO object,
|
45
|
+
# the path to a PNG image, or binary PNG data.
|
46
|
+
#
|
47
|
+
# io_in = File.open("image.png", "r")
|
48
|
+
# image = Axon.png(io_in) # Read PNG from a StringIO
|
49
|
+
#
|
50
|
+
# png_data = IO.read("image.png")
|
51
|
+
# image_2 = Axon.png(png_data) # Read PNG from image data
|
52
|
+
#
|
53
|
+
def self.png(thing)
|
54
|
+
thing = StringIO.new(thing) unless thing.respond_to?(:read)
|
55
|
+
reader = PNG::Reader.new(thing)
|
56
|
+
Image.new(reader)
|
29
57
|
end
|
30
58
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
59
|
+
class Image
|
60
|
+
# :call-seq:
|
61
|
+
# Image.new(image_in)
|
62
|
+
#
|
63
|
+
# Wraps +image_in+ in an easy API.
|
64
|
+
#
|
65
|
+
# Rather than calling Image.new directly, you may want to call Axon.jpeg
|
66
|
+
# or Axon.png.
|
67
|
+
#
|
68
|
+
# io_in = File.open("image.jpg", "r")
|
69
|
+
# reader = Axon::JPEG::Reader.new(io_in)
|
70
|
+
# image = Axon::Image.new(reader)
|
71
|
+
#
|
72
|
+
# image.fit(10, 20)
|
73
|
+
# image.height #=> 20 or less
|
74
|
+
# image.width #=> 10 or less
|
75
|
+
#
|
76
|
+
# io_out = File.open("out.jpg", "w")
|
77
|
+
# image.jpg(io_out) # writes a compressed JPEG file
|
78
|
+
#
|
79
|
+
def initialize(source)
|
80
|
+
@source = source
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
# :call-seq:
|
85
|
+
# scale_bilinear(width, height)
|
86
|
+
#
|
87
|
+
# Scales the image using the bilinear interpolation method.
|
88
|
+
#
|
89
|
+
# Bilinear interpolation calculates the color values in the resulting image
|
90
|
+
# by looking at the four nearest pixels for each pixel in the resulting
|
91
|
+
# image.
|
92
|
+
#
|
93
|
+
# This gives a more accurate representation than nearest-neighbor
|
94
|
+
# interpolation, at the expense of slightly blurring the resulting image.
|
95
|
+
#
|
96
|
+
# == Example
|
97
|
+
#
|
98
|
+
# i = Axon::JPEG('test.jpg')
|
99
|
+
# i.scale_bilinear(50, 75)
|
100
|
+
# i.width # => 50
|
101
|
+
# i.height # => 75
|
102
|
+
#
|
103
|
+
def scale_bilinear(*args)
|
104
|
+
@source = BilinearScaler.new(@source, *args)
|
105
|
+
self
|
106
|
+
end
|
107
|
+
|
108
|
+
# :call-seq:
|
109
|
+
# scale_nearest(width, height)
|
110
|
+
#
|
111
|
+
# Scales the image using the nearest-neighbor interpolation method.
|
112
|
+
#
|
113
|
+
# Nearest-neighbor interpolation selects the value of the nearest pixel when
|
114
|
+
# calculating colors in the scaled image.
|
115
|
+
#
|
116
|
+
# == Example
|
117
|
+
#
|
118
|
+
# i = Axon::JPEG('test.jpg')
|
119
|
+
# i.scale_nearest(50, 75)
|
120
|
+
# i.width # => 50
|
121
|
+
# i.height # => 75
|
122
|
+
#
|
123
|
+
def scale_nearest(*args)
|
124
|
+
@source = NearestNeighborScaler.new(@source, *args)
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
# :call-seq:
|
129
|
+
# fit(width, height)
|
130
|
+
#
|
131
|
+
# Scales the image to fit inside given box dimensions while maintaining the
|
132
|
+
# aspect ratio.
|
133
|
+
#
|
134
|
+
# == Example
|
135
|
+
#
|
136
|
+
# i = Axon::JPEG('test.jpg')
|
137
|
+
# i.fit(5, 20)
|
138
|
+
# i.width # => 5
|
139
|
+
# i.height # => 10
|
140
|
+
#
|
141
|
+
def fit(*args)
|
142
|
+
@source = Fit.new(@source, *args)
|
143
|
+
self
|
144
|
+
end
|
145
|
+
|
146
|
+
# :call-seq:
|
147
|
+
# crop(width, height, x_offset = 0, y_offset = 0)
|
148
|
+
#
|
149
|
+
# Crops the image and extracts regions.
|
150
|
+
#
|
151
|
+
# If the region extends beyond the boundaries of the image then the cropped
|
152
|
+
# image will be truncated at the boundaries.
|
153
|
+
#
|
154
|
+
# == Example
|
155
|
+
#
|
156
|
+
# i = Axon::JPEG('test.jpg')
|
157
|
+
# i.crop(50, 75, 10, 20)
|
158
|
+
# c.width # => 50
|
159
|
+
# c.height # => 75
|
160
|
+
#
|
161
|
+
# == Example of Cropping Past the Boundaries
|
162
|
+
#
|
163
|
+
# i = Axon::JPEG('test.jpg')
|
164
|
+
# i.crop(50, 75, 60, 20)
|
165
|
+
# i.width # => 40 # note that this is not 50
|
166
|
+
#
|
167
|
+
def crop(*args)
|
168
|
+
@source = Cropper.new(@source, *args)
|
169
|
+
self
|
170
|
+
end
|
171
|
+
|
172
|
+
# :call-seq:
|
173
|
+
# jpeg(io_out [, options])
|
174
|
+
#
|
175
|
+
# Writes the image to +io_out+ as compressed JPEG data. Returns the number
|
176
|
+
# of bytes written.
|
177
|
+
#
|
178
|
+
# +options+ may contain the following symbols:
|
179
|
+
#
|
180
|
+
# * :bufsize -- the maximum size in bytes of the writes that will be
|
181
|
+
# made to +io_out+
|
182
|
+
# * :quality -- JPEG quality on a 0..100 scale.
|
183
|
+
# * :exif -- Raw exif string that will be saved in the header.
|
184
|
+
# * :icc_profile -- Raw ICC profile string that will be saved in the header.
|
185
|
+
#
|
186
|
+
# == Example
|
187
|
+
#
|
188
|
+
# i = Axon::JPEG('input.jpg')
|
189
|
+
# io_out = File.open('output.jpg', 'w')
|
190
|
+
# i.jpeg(io_out, :quality => 88) # writes the image to output.jpg
|
191
|
+
#
|
192
|
+
def jpeg(*args)
|
193
|
+
JPEG.write(@source, *args)
|
194
|
+
end
|
195
|
+
|
196
|
+
# :call-seq:
|
197
|
+
# png(io_out)
|
198
|
+
#
|
199
|
+
# Writes the image to +io_out+ as compressed PNG data. Returns the number
|
200
|
+
# of bytes written.
|
201
|
+
#
|
202
|
+
# == Example
|
203
|
+
#
|
204
|
+
# i = Axon::JPEG('input.png')
|
205
|
+
# io_out = File.open('output.png', 'w')
|
206
|
+
# i.png(io_out) # writes the image to output.png
|
207
|
+
#
|
208
|
+
def png(*args)
|
209
|
+
PNG.write(@source, *args)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Gets the components in the image.
|
213
|
+
#
|
214
|
+
def components
|
215
|
+
@source.components
|
216
|
+
end
|
217
|
+
|
218
|
+
# Gets the color model of the image.
|
219
|
+
#
|
220
|
+
def color_model
|
221
|
+
@source.color_model
|
41
222
|
end
|
42
223
|
|
43
|
-
|
224
|
+
# Gets the width of the image.
|
225
|
+
#
|
226
|
+
def width
|
227
|
+
@source.width
|
228
|
+
end
|
229
|
+
|
230
|
+
# Gets the height of the image.
|
231
|
+
#
|
232
|
+
def height
|
233
|
+
@source.height
|
234
|
+
end
|
235
|
+
|
236
|
+
# Gets the index of the next line that will be fetched by gets, starting at
|
237
|
+
# 0.
|
238
|
+
def lineno
|
239
|
+
@source.lineno
|
240
|
+
end
|
241
|
+
|
242
|
+
# Gets the next scanline from the image.
|
243
|
+
#
|
244
|
+
def gets
|
245
|
+
@source.gets
|
246
|
+
end
|
44
247
|
end
|
45
248
|
end
|
data/lib/axon/cropper.rb
CHANGED
@@ -1,35 +1,97 @@
|
|
1
1
|
module Axon
|
2
|
+
|
3
|
+
# == An Image Cropper
|
4
|
+
#
|
5
|
+
# Axon::Crop allows you to crop images and extract regions.
|
6
|
+
#
|
7
|
+
# == Example
|
8
|
+
#
|
9
|
+
# image_in = Axon::Solid.new(100, 200)
|
10
|
+
# c = Axon::Crop.new(image_in, 50, 75, 10, 20)
|
11
|
+
# c.width # => 50
|
12
|
+
# c.height # => 75
|
13
|
+
# c.gets # => String
|
14
|
+
#
|
15
|
+
# == Example of Cropping Past the Boundaries of the Original Image
|
16
|
+
#
|
17
|
+
# image_in = Axon::Solid.new(100, 200)
|
18
|
+
# c = Axon::Crop.new(image_in, 50, 75, 60, 20)
|
19
|
+
# c.width # => 40
|
20
|
+
#
|
2
21
|
class Cropper
|
3
|
-
|
4
|
-
|
22
|
+
# The index of the next line that will be fetched by gets, starting at 0.
|
23
|
+
attr_reader :lineno
|
5
24
|
|
6
|
-
|
25
|
+
# :call-seq:
|
26
|
+
# Cropper.new(image_in, width, height, x_offset = 0, y_offset = 0)
|
27
|
+
#
|
28
|
+
# Crops +image_in+ to the size +width+ x +height+. Optionally, +x_offset+
|
29
|
+
# and +y_offset+ can be used to shift the upper left corner of the cropped
|
30
|
+
# area.
|
31
|
+
#
|
32
|
+
# If the cropped image extends beyond the boundaries of +image_in+ then the
|
33
|
+
# cropped image will be truncated at the boundary.
|
34
|
+
#
|
35
|
+
def initialize(source, width, height, x_offset=nil, y_offset=nil)
|
36
|
+
raise ArgumentError if width < 1 || height < 1
|
37
|
+
raise ArgumentError if x_offset && x_offset < 1 || y_offset && y_offset < 1
|
7
38
|
|
8
|
-
|
9
|
-
@image = image
|
39
|
+
@source = source
|
10
40
|
@width = width
|
11
41
|
@height = height
|
12
42
|
@x_offset = x_offset || 0
|
13
43
|
@y_offset = y_offset || 0
|
14
|
-
@
|
15
|
-
@color_model = image.color_model
|
44
|
+
@lineno = 0
|
16
45
|
end
|
17
46
|
|
18
|
-
|
19
|
-
|
20
|
-
|
47
|
+
# Calculates the height of the cropped image.
|
48
|
+
#
|
49
|
+
def height
|
50
|
+
if @y_offset + @height > @source.height
|
51
|
+
[@source.height - @y_offset, 0].max
|
52
|
+
else
|
53
|
+
@height
|
54
|
+
end
|
55
|
+
end
|
21
56
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
57
|
+
# Calculates the width of the cropped image.
|
58
|
+
#
|
59
|
+
def width
|
60
|
+
if @x_offset + @width > @source.width
|
61
|
+
[@source.width - @x_offset, 0].max
|
62
|
+
else
|
63
|
+
@width
|
26
64
|
end
|
27
65
|
end
|
28
|
-
end
|
29
66
|
|
30
|
-
|
31
|
-
|
32
|
-
|
67
|
+
# Gets the components in the cropped image. Same as the components of the
|
68
|
+
# source image.
|
69
|
+
#
|
70
|
+
def components
|
71
|
+
@source.components
|
72
|
+
end
|
73
|
+
|
74
|
+
# Gets the color model of the cropped image. Same as the color model of the
|
75
|
+
# source image.
|
76
|
+
#
|
77
|
+
def color_model
|
78
|
+
@source.color_model
|
79
|
+
end
|
80
|
+
|
81
|
+
# Gets the next scanline from the cropped image.
|
82
|
+
#
|
83
|
+
def gets
|
84
|
+
return nil if @lineno >= height || width < 1 || height < 1
|
85
|
+
|
86
|
+
while @source.lineno < @y_offset
|
87
|
+
break unless @source.gets
|
88
|
+
end
|
89
|
+
|
90
|
+
@lineno += 1
|
91
|
+
|
92
|
+
sl_width = width * components
|
93
|
+
sl_offset = @x_offset * components
|
94
|
+
@source.gets[sl_offset, sl_width]
|
33
95
|
end
|
34
96
|
end
|
35
97
|
end
|
data/lib/axon/fit.rb
CHANGED
@@ -1,43 +1,99 @@
|
|
1
|
+
require 'axon/scalers'
|
2
|
+
|
1
3
|
module Axon
|
2
|
-
class Fit
|
3
|
-
include Image
|
4
|
-
include Enumerable
|
5
4
|
|
5
|
+
# == An Image Box Scaler
|
6
|
+
#
|
7
|
+
# Axon::Fit will scale images to fit inside given box dimensions while
|
8
|
+
# maintaining the aspect ratio.
|
9
|
+
#
|
10
|
+
# == Example
|
11
|
+
#
|
12
|
+
# image_in = Axon::Solid.new(10, 20)
|
13
|
+
# f = Axon::Fit.new(image_in, 5, 20)
|
14
|
+
# f.width # => 5
|
15
|
+
# f.height # => 10
|
16
|
+
#
|
17
|
+
class Fit
|
18
|
+
# :call-seq:
|
19
|
+
# Fit.new(image_in, width, height)
|
20
|
+
#
|
21
|
+
# Fits +image_in+ in the box dimensions given by +width+ and +height+. The
|
22
|
+
# resulting image will not extend beyond the given +width+ or the given
|
23
|
+
# +height+. The resulting image will maintain the aspect ratio of +image_in+
|
24
|
+
# so the resulting image may not completely fill +width+ and +height+.
|
25
|
+
#
|
26
|
+
# The resulting image will match either +width+ or +height+.
|
27
|
+
#
|
6
28
|
def initialize(source, width, height)
|
7
29
|
@source, @fit_width, @fit_height = source, width, height
|
30
|
+
@scaler = nil
|
8
31
|
end
|
9
32
|
|
33
|
+
# Gets the components in the fitted image. Same as the components of the
|
34
|
+
# source image.
|
35
|
+
#
|
10
36
|
def components
|
11
37
|
@source.components
|
12
38
|
end
|
13
39
|
|
40
|
+
# Gets the color model of the fitted image. Same as the color model of the
|
41
|
+
# source image.
|
42
|
+
#
|
14
43
|
def color_model
|
15
44
|
@source.color_model
|
16
45
|
end
|
17
46
|
|
47
|
+
# Gets the width of the fitted image. This will be the given width or less.
|
48
|
+
#
|
18
49
|
def width
|
19
|
-
@
|
50
|
+
@scaler ? @scaler.width : calculate_width
|
20
51
|
end
|
21
52
|
|
53
|
+
# Gets the height of the fitted image. This will be the given height or
|
54
|
+
# less.
|
55
|
+
#
|
22
56
|
def height
|
23
|
-
@
|
57
|
+
@scaler ? @scaler.height : calculate_height
|
58
|
+
end
|
59
|
+
|
60
|
+
# Gets the index of the next line that will be fetched by gets, starting at
|
61
|
+
# 0.
|
62
|
+
#
|
63
|
+
def lineno
|
64
|
+
@scaler ? @scaler.lineno : 0
|
65
|
+
end
|
66
|
+
|
67
|
+
# Gets the next scanline from the fitted image.
|
68
|
+
#
|
69
|
+
def gets
|
70
|
+
@scaler ||= get_scaler
|
71
|
+
@scaler.gets
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def calculate_width
|
77
|
+
(@source.width * calc_fit_ratio).to_i
|
78
|
+
end
|
79
|
+
|
80
|
+
def calculate_height
|
81
|
+
(@source.height * calc_fit_ratio).to_i
|
24
82
|
end
|
25
83
|
|
26
|
-
def
|
84
|
+
def get_scaler
|
27
85
|
r = calc_fit_ratio
|
28
86
|
|
29
87
|
if r > 1
|
30
|
-
|
31
|
-
scaler.each{ |*a| yield(*a) }
|
88
|
+
NearestNeighborScaler.new(@source, width, height)
|
32
89
|
elsif r < 1
|
33
|
-
if r <= 0.5 && @source.kind_of?(
|
90
|
+
if r <= 0.5 && @source.kind_of?(JPEG::Reader)
|
34
91
|
@source.scale_denom = calc_jpeg_pre_shrink(r)
|
35
92
|
r = calc_fit_ratio
|
36
93
|
end
|
37
|
-
|
38
|
-
scaler.each{ |*a| yield(*a) }
|
94
|
+
BilinearScaler.new(@source, width, height)
|
39
95
|
else
|
40
|
-
@source
|
96
|
+
@source
|
41
97
|
end
|
42
98
|
end
|
43
99
|
|
@@ -58,10 +114,4 @@ module Axon
|
|
58
114
|
end
|
59
115
|
end
|
60
116
|
end
|
61
|
-
|
62
|
-
module Image
|
63
|
-
def fit(*args)
|
64
|
-
Fit.new(self, *args)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
117
|
+
end
|