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.
Files changed (56) hide show
  1. data/CHANGELOG.rdoc +9 -3
  2. data/README.rdoc +29 -36
  3. data/Rakefile +26 -21
  4. data/TODO.rdoc +1 -6
  5. data/ext/axon/axon.c +6 -15
  6. data/ext/axon/extconf.rb +19 -9
  7. data/ext/axon/interpolation.c +147 -0
  8. data/ext/axon/jpeg.c +1207 -0
  9. data/ext/axon/png.c +542 -0
  10. data/lib/axon.rb +235 -32
  11. data/lib/axon/cropper.rb +80 -18
  12. data/lib/axon/fit.rb +69 -19
  13. data/lib/axon/generators.rb +109 -0
  14. data/lib/axon/scalers.rb +160 -0
  15. data/test/helper.rb +151 -6
  16. data/test/reader_tests.rb +37 -82
  17. data/test/scaler_tests.rb +102 -0
  18. data/test/stress_helper.rb +58 -0
  19. data/test/stress_tests.rb +8 -5
  20. data/test/test_bilinear_scaler.rb +60 -2
  21. data/test/test_cropper.rb +68 -1
  22. data/test/test_fit.rb +35 -0
  23. data/test/test_generators.rb +21 -0
  24. data/test/test_image.rb +61 -0
  25. data/test/test_jpeg_reader.rb +96 -94
  26. data/test/test_jpeg_writer.rb +95 -8
  27. data/test/test_nearest_neighbor_scaler.rb +28 -4
  28. data/test/test_png_reader.rb +12 -8
  29. data/test/test_png_writer.rb +8 -6
  30. data/test/writer_tests.rb +129 -111
  31. metadata +71 -128
  32. data/.gemtest +0 -0
  33. data/ext/axon/bilinear_interpolation.c +0 -115
  34. data/ext/axon/interpolation.h +0 -7
  35. data/ext/axon/jpeg_common.c +0 -118
  36. data/ext/axon/jpeg_common.h +0 -37
  37. data/ext/axon/jpeg_native_writer.c +0 -248
  38. data/ext/axon/jpeg_reader.c +0 -774
  39. data/ext/axon/nearest_neighbor_interpolation.c +0 -50
  40. data/ext/axon/png_common.c +0 -21
  41. data/ext/axon/png_common.h +0 -18
  42. data/ext/axon/png_native_writer.c +0 -166
  43. data/ext/axon/png_reader.c +0 -381
  44. data/lib/axon/axon.so +0 -0
  45. data/lib/axon/bilinear_scaler.rb +0 -60
  46. data/lib/axon/jpeg_writer.rb +0 -41
  47. data/lib/axon/nearest_neighbor_scaler.rb +0 -39
  48. data/lib/axon/png_writer.rb +0 -35
  49. data/lib/axon/scaler.rb +0 -41
  50. data/lib/axon/solid.rb +0 -23
  51. data/test/_test_readme.rb +0 -34
  52. data/test/test_exif.rb +0 -39
  53. data/test/test_generator.rb +0 -10
  54. data/test/test_icc.rb +0 -18
  55. data/test/test_jpeg.rb +0 -9
  56. data/test/test_png.rb +0 -9
data/lib/axon.rb CHANGED
@@ -1,45 +1,248 @@
1
- require 'axon/axon'
1
+ # :main: README.rdoc
2
2
 
3
- require 'axon/solid'
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/jpeg_writer'
11
- require 'axon/png_writer'
6
+ require 'axon/scalers'
7
+ require 'axon/generators'
12
8
 
13
9
  module Axon
14
- VERSION = '0.0.2'
10
+ VERSION = '0.1.0'
15
11
 
16
- def self.JPEG(thing, markers=nil)
17
- if thing.respond_to? :read
18
- io = thing
19
- rewind_after_scanlines = false
20
- elsif thing[0, 1] == "\xFF"
21
- io = StringIO.new(thing)
22
- rewind_after_scanlines = true
23
- else
24
- io = File.open(thing)
25
- rewind_after_scanlines = true
26
- end
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
- JPEGReader.new(io, markers, rewind_after_scanlines)
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
- def self.PNG(thing)
32
- if thing.respond_to? :read
33
- io = thing
34
- rewind_after_scanlines = false
35
- elsif thing[0, 1] == "\x89"
36
- io = StringIO.new(thing)
37
- rewind_after_scanlines = true
38
- else
39
- io = File.open(thing)
40
- rewind_after_scanlines = true
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
- PNGReader.new(io)
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
- include Enumerable
4
- include Image
22
+ # The index of the next line that will be fetched by gets, starting at 0.
23
+ attr_reader :lineno
5
24
 
6
- attr_reader :image, :width, :height, :components, :color_model
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
- def initialize(image, width, height, x_offset=nil, y_offset=nil)
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
- @components = image.components
15
- @color_model = image.color_model
44
+ @lineno = 0
16
45
  end
17
46
 
18
- def each
19
- sl_width = @width * @components
20
- sl_offset = @x_offset * @components
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
- @image.each_with_index do |orig_sl, i|
23
- next if i < @y_offset
24
- yield orig_sl[sl_offset, sl_width]
25
- break if i == @height - 1
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
- module Image
31
- def crop(*args)
32
- Cropper.new(self, *args)
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
- @source.width * calc_fit_ratio
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
- @source.height * calc_fit_ratio
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 each
84
+ def get_scaler
27
85
  r = calc_fit_ratio
28
86
 
29
87
  if r > 1
30
- scaler = NearestNeighborScaler.new(@source, r)
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?(JPEGReader)
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
- scaler = BilinearScaler.new(@source, r)
38
- scaler.each{ |*a| yield(*a) }
94
+ BilinearScaler.new(@source, width, height)
39
95
  else
40
- @source.each{ |*a| yield(*a) }
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