axon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gemtest +0 -0
  2. data/CHANGELOG.rdoc +3 -0
  3. data/README.rdoc +104 -0
  4. data/Rakefile +30 -0
  5. data/TODO.rdoc +12 -0
  6. data/ext/axon/axon.c +20 -0
  7. data/ext/axon/bilinear_interpolation.c +115 -0
  8. data/ext/axon/extconf.rb +21 -0
  9. data/ext/axon/iccjpeg.c +248 -0
  10. data/ext/axon/iccjpeg.h +73 -0
  11. data/ext/axon/interpolation.h +7 -0
  12. data/ext/axon/jpeg_common.c +118 -0
  13. data/ext/axon/jpeg_common.h +37 -0
  14. data/ext/axon/jpeg_native_writer.c +248 -0
  15. data/ext/axon/jpeg_reader.c +774 -0
  16. data/ext/axon/nearest_neighbor_interpolation.c +50 -0
  17. data/ext/axon/png_common.c +21 -0
  18. data/ext/axon/png_common.h +18 -0
  19. data/ext/axon/png_native_writer.c +166 -0
  20. data/ext/axon/png_reader.c +381 -0
  21. data/lib/axon/axon.so +0 -0
  22. data/lib/axon/bilinear_scaler.rb +60 -0
  23. data/lib/axon/cropper.rb +35 -0
  24. data/lib/axon/fit.rb +67 -0
  25. data/lib/axon/jpeg_writer.rb +41 -0
  26. data/lib/axon/nearest_neighbor_scaler.rb +39 -0
  27. data/lib/axon/png_writer.rb +35 -0
  28. data/lib/axon/scaler.rb +41 -0
  29. data/lib/axon/solid.rb +23 -0
  30. data/lib/axon.rb +45 -0
  31. data/test/_test_readme.rb +34 -0
  32. data/test/helper.rb +17 -0
  33. data/test/reader_tests.rb +115 -0
  34. data/test/stress_tests.rb +71 -0
  35. data/test/test_bilinear_scaler.rb +9 -0
  36. data/test/test_cropper.rb +9 -0
  37. data/test/test_exif.rb +39 -0
  38. data/test/test_generator.rb +10 -0
  39. data/test/test_icc.rb +18 -0
  40. data/test/test_jpeg.rb +9 -0
  41. data/test/test_jpeg_reader.rb +109 -0
  42. data/test/test_jpeg_writer.rb +26 -0
  43. data/test/test_nearest_neighbor_scaler.rb +13 -0
  44. data/test/test_png.rb +9 -0
  45. data/test/test_png_reader.rb +15 -0
  46. data/test/test_png_writer.rb +13 -0
  47. data/test/writer_tests.rb +179 -0
  48. metadata +148 -0
@@ -0,0 +1,35 @@
1
+ module Axon
2
+ class Cropper
3
+ include Enumerable
4
+ include Image
5
+
6
+ attr_reader :image, :width, :height, :components, :color_model
7
+
8
+ def initialize(image, width, height, x_offset=nil, y_offset=nil)
9
+ @image = image
10
+ @width = width
11
+ @height = height
12
+ @x_offset = x_offset || 0
13
+ @y_offset = y_offset || 0
14
+ @components = image.components
15
+ @color_model = image.color_model
16
+ end
17
+
18
+ def each
19
+ sl_width = @width * @components
20
+ sl_offset = @x_offset * @components
21
+
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
26
+ end
27
+ end
28
+ end
29
+
30
+ module Image
31
+ def crop(*args)
32
+ Cropper.new(self, *args)
33
+ end
34
+ end
35
+ end
data/lib/axon/fit.rb ADDED
@@ -0,0 +1,67 @@
1
+ module Axon
2
+ class Fit
3
+ include Image
4
+ include Enumerable
5
+
6
+ def initialize(source, width, height)
7
+ @source, @fit_width, @fit_height = source, width, height
8
+ end
9
+
10
+ def components
11
+ @source.components
12
+ end
13
+
14
+ def color_model
15
+ @source.color_model
16
+ end
17
+
18
+ def width
19
+ @source.width * calc_fit_ratio
20
+ end
21
+
22
+ def height
23
+ @source.height * calc_fit_ratio
24
+ end
25
+
26
+ def each
27
+ r = calc_fit_ratio
28
+
29
+ if r > 1
30
+ scaler = NearestNeighborScaler.new(@source, r)
31
+ scaler.each{ |*a| yield(*a) }
32
+ elsif r < 1
33
+ if r <= 0.5 && @source.kind_of?(JPEGReader)
34
+ @source.scale_denom = calc_jpeg_pre_shrink(r)
35
+ r = calc_fit_ratio
36
+ end
37
+ scaler = BilinearScaler.new(@source, r)
38
+ scaler.each{ |*a| yield(*a) }
39
+ else
40
+ @source.each{ |*a| yield(*a) }
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def calc_fit_ratio
47
+ width_ratio = @fit_width.to_f / @source.width
48
+ height_ratio = @fit_height.to_f / @source.height
49
+ [width_ratio, height_ratio].min
50
+ end
51
+
52
+ def calc_jpeg_pre_shrink(r)
53
+ case (1/r)
54
+ when (0...2) then nil
55
+ when (2...4) then 2
56
+ when (4...8) then 4
57
+ else 8
58
+ end
59
+ end
60
+ end
61
+
62
+ module Image
63
+ def fit(*args)
64
+ Fit.new(self, *args)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ require 'stringio'
2
+
3
+ module Axon
4
+ class JPEGWriter
5
+ include JPEGNativeWriter
6
+
7
+ attr_accessor :quality, :icc_profile, :exif
8
+ attr_reader :io, :bufsize, :image
9
+
10
+ def initialize(image)
11
+ @image = image
12
+ @quality = nil
13
+ @icc_profile = nil
14
+ @exif = nil
15
+ end
16
+
17
+ def write(io, bufsize = nil)
18
+ @bufsize = bufsize || 512
19
+ @io = io
20
+ jpeg_native_write
21
+ end
22
+
23
+ def data
24
+ s = StringIO.new
25
+ s.set_encoding 'BINARY' if s.respond_to?(:set_encoding)
26
+
27
+ write(s)
28
+ s.string
29
+ end
30
+ end
31
+
32
+ module Image
33
+ def to_jpeg(*args)
34
+ JPEGWriter.new self, *args
35
+ end
36
+
37
+ def write_jpeg(io)
38
+ JPEGWriter.new(self).write(io)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ module Axon
2
+ class NearestNeighborScaler < Scaler
3
+ include NearestNeighborScaling
4
+ include Image
5
+ include Enumerable
6
+
7
+ def each
8
+ dest_y = 0
9
+ sample_y = 0
10
+
11
+ @image.each_with_index do |scanline, orig_y|
12
+ while sample_y == orig_y
13
+ yield interpolate_scanline(scanline)
14
+ dest_y += 1
15
+ sample_y = (dest_y / @height_ratio).to_i
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def _interpolate_scanline(scanline)
23
+ dest_sl = ''
24
+
25
+ scanline.size.times do |dest_x|
26
+ sample_x = (dest_x / @width_ratio).to_i
27
+ dest_sl << scanline[sample_x * components, components]
28
+ end
29
+
30
+ return dest_sl
31
+ end
32
+ end
33
+
34
+ module Image
35
+ def scale_nearest_neighbor(*args)
36
+ NearestNeighborScaler.new(self, *args)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ require 'stringio'
2
+
3
+ module Axon
4
+ class PNGWriter
5
+ include PNGNativeWriter
6
+
7
+ attr_reader :image
8
+
9
+ def initialize(image)
10
+ @image = image
11
+ end
12
+
13
+ def write(io)
14
+ png_native_write(io)
15
+ end
16
+
17
+ def data
18
+ s = StringIO.new
19
+ s.set_encoding 'BINARY' if s.respond_to?(:set_encoding)
20
+
21
+ write(s)
22
+ s.string
23
+ end
24
+ end
25
+
26
+ module Image
27
+ def to_png
28
+ PNGWriter.new self
29
+ end
30
+
31
+ def write_png(io)
32
+ PNGWriter.new(self).write(io)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ module Axon
2
+ class Scaler
3
+ attr_reader :width, :height
4
+
5
+ def initialize(image, *args)
6
+ @image = image
7
+
8
+ if args.size == 1
9
+ init_ratio(args[0])
10
+ elsif args.size == 2
11
+ init_dims(args[0], args[1])
12
+ else
13
+ raise ArgumentError, "Must give one or two arguments"
14
+ end
15
+ end
16
+
17
+ def components
18
+ @image.components
19
+ end
20
+
21
+ def color_model
22
+ @image.color_model
23
+ end
24
+
25
+ private
26
+
27
+ def init_ratio(ratio)
28
+ @width_ratio = @height_ratio = ratio.to_f
29
+ @width = (@image.width * ratio).floor
30
+ @height = (@image.height * ratio).floor
31
+ end
32
+
33
+ def init_dims(width, height)
34
+ @width_ratio = width.to_f / @image.width
35
+ @width = width
36
+
37
+ @height_ratio = height.to_f / @image.height
38
+ @height = height
39
+ end
40
+ end
41
+ end
data/lib/axon/solid.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Axon
2
+ class Solid
3
+ include Image
4
+ include Enumerable
5
+ attr_reader :width, :height, :color_model, :components
6
+
7
+ def initialize(width, height, color=nil, color_model=nil)
8
+ @width, @height = width, height
9
+ @color = color || "\x00\x00\x00"
10
+ @color_model = color_model || :RGB
11
+ @components = @color.size
12
+ end
13
+
14
+ def each
15
+ sl = @color * width
16
+ height.times { yield sl }
17
+ end
18
+ end
19
+
20
+ module Image
21
+ # empty
22
+ end
23
+ end
data/lib/axon.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'axon/axon'
2
+
3
+ require 'axon/solid'
4
+ require 'axon/cropper'
5
+ require 'axon/scaler'
6
+ require 'axon/nearest_neighbor_scaler'
7
+ require 'axon/bilinear_scaler'
8
+ require 'axon/fit'
9
+
10
+ require 'axon/jpeg_writer'
11
+ require 'axon/png_writer'
12
+
13
+ module Axon
14
+ VERSION = '0.0.1'
15
+
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
27
+
28
+ JPEGReader.new(io, markers, rewind_after_scanlines)
29
+ end
30
+
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
41
+ end
42
+
43
+ PNGReader.new(io)
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ require 'helper'
2
+
3
+ module Axon
4
+ class TestReader < AxonTestCase
5
+ def setup
6
+ data = Solid.new(100, 200, "\x0A\x14\x69").to_jpeg.data
7
+ @io_in = StringIO.new(data)
8
+ @io_out = StringIO.new
9
+ end
10
+
11
+ def test_short_chained_example
12
+
13
+ # Short, chained example. Reads a JPEG from io_in and writes scaled png to
14
+ # io_out.
15
+ Axon.JPEG(@io_in).fit(100, 100).write_png(@io_out)
16
+ end
17
+
18
+ def test_longer_example
19
+ # Even longer example, reads the JPEG header, looks at properties and header
20
+ # values, sets decompression options, scales the image, sets compression
21
+ # options, and writes a JPEG.
22
+ image = Axon.JPEG(@io_in, [:APP2])
23
+
24
+ puts image.width
25
+ puts image.height
26
+ puts image[:APP2]
27
+ image.scale_denom = 4
28
+
29
+ jpeg = image.fit(100, 100).to_jpeg
30
+ jpeg.quality = 88
31
+ jpeg.write(@io_out)
32
+ end
33
+ end
34
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'minitest/autorun'
2
+ require 'axon'
3
+ require 'stringio'
4
+ require 'reader_tests'
5
+ require 'writer_tests'
6
+
7
+ module Axon
8
+ class AxonTestCase < MiniTest::Unit::TestCase
9
+
10
+ # Generate a solid velvet JPEG
11
+ def setup
12
+ @velvet = "\x0A\x14\x69"
13
+ @image = Solid.new 10, 15, @velvet
14
+ @io_out = StringIO.new
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,115 @@
1
+ module Axon
2
+ module ReaderTests
3
+ def test_read_image_from_io
4
+ assert_equal 10, @reader.width
5
+ assert_equal 15, @reader.height
6
+ end
7
+
8
+ def test_read_image_from_string
9
+ assert_equal 10, @reader.width
10
+ assert_equal 15, @reader.height
11
+ end
12
+
13
+ def test_num_components
14
+ assert_equal 3, @reader.components
15
+ end
16
+
17
+ def test_color_model
18
+ assert_equal :RGB, @reader.color_model
19
+ end
20
+
21
+ class DoubleIO < StringIO
22
+ def read(*args)
23
+ s = super
24
+ s[0..20] * 100
25
+ end
26
+ end
27
+
28
+ def test_io_returns_too_much
29
+ f = DoubleIO.new @data
30
+ assert_raises(RuntimeError) { @readerclass.new f }
31
+ end
32
+
33
+ class NilIO
34
+ def read(*args); end
35
+ end
36
+
37
+ def test_io_returns_nil
38
+ assert_raises(RuntimeError) { @readerclass.new NilIO.new }
39
+ end
40
+
41
+ class RaiseIO
42
+ def read(*args)
43
+ raise 'heck'
44
+ end
45
+ end
46
+
47
+ def test_io_raises_exception
48
+ assert_raises(RuntimeError) { @readerclass.new RaiseIO.new }
49
+ end
50
+
51
+ class EmptyStringIO
52
+ def read(*args)
53
+ ""
54
+ end
55
+ end
56
+
57
+ def test_empty_string_io
58
+ assert_raises(RuntimeError) { @readerclass.new EmptyStringIO.new }
59
+ end
60
+
61
+ class ThatsNotAStringIO
62
+ def read(*args)
63
+ :this_should_be_a_string
64
+ end
65
+ end
66
+
67
+ def test_not_a_string_io
68
+ assert_raises(TypeError) { @readerclass.new ThatsNotAStringIO.new }
69
+ end
70
+
71
+ def test_each
72
+ size = @reader.width * @reader.components
73
+
74
+ @reader.each do |scan_line|
75
+ assert_equal size, scan_line.size
76
+ end
77
+ end
78
+
79
+ def test_no_shenanigans_during_each
80
+ @reader.each do |sl|
81
+ assert_raises(RuntimeError) { @reader.each{} }
82
+ break
83
+ end
84
+ end
85
+
86
+ class OneExceptionIO < StringIO
87
+ def initialize(*args)
88
+ @raised_exception = false
89
+ super
90
+ end
91
+
92
+ def read(*args)
93
+ unless @raised_exception
94
+ @raised_exception = true
95
+ raise 'heck'
96
+ end
97
+ super
98
+ end
99
+ end
100
+
101
+ def test_recovers_from_initial_io_exception
102
+ io = OneExceptionIO.new @data
103
+ r = @readerclass.allocate
104
+ assert_raises(RuntimeError) { r.send(:initialize, io) }
105
+ r.send(:initialize, io)
106
+ r.each{ }
107
+ end
108
+
109
+ def test_multiple_each_calls
110
+ @reader.each{ }
111
+ @io_in.rewind
112
+ @reader.each{ }
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,71 @@
1
+ require 'helper'
2
+
3
+ module Axon
4
+ class TestStressCases < AxonTestCase
5
+ def setup
6
+ @velvet = "\x0A\x14\x69"
7
+ @image = Generator::Solid.new 1000, 1500, @velvet
8
+ @jpeg_data = @image.to_jpeg.data
9
+ @readerclass = JPEGReader
10
+ end
11
+
12
+ class RandomRaiseIO
13
+ def initialize(source)
14
+ @io = StringIO.new source
15
+ @die_at = rand(source.size)
16
+ end
17
+
18
+ def read
19
+ if @die_at < 2
20
+ raise 'random death!'
21
+ end
22
+
23
+ read_size = rand @die_at
24
+ @die_at -= read_size
25
+ @io.read(read_size)
26
+ end
27
+ end
28
+
29
+ def test_io_raises_after_random_sized_reads
30
+ loop do
31
+ f = RandomRaiseIO.new(@jpeg_data)
32
+ r = @readerclass.new(f) rescue puts("new failed")
33
+ r.each{} rescue puts('each_sl failed')
34
+ end
35
+ end
36
+
37
+ class CustomIO < StringIO
38
+ def initialize(str, sequence)
39
+ super str
40
+ @sequence = sequence
41
+ end
42
+
43
+ def read
44
+ if !@sequence || @sequence.empty?
45
+ raise "random death!"
46
+ @sequence = nil
47
+ end
48
+ super(@sequence.shift)
49
+ end
50
+ end
51
+
52
+ def test_custom_io_error
53
+ loop do
54
+ f = CustomIO.new(@jpeg_data, [114, 64, 8, 200, 200, 30, 350])
55
+
56
+ r = @readerclass.new(f)
57
+ r.each{ } rescue nil
58
+ f = CustomIO.new(@jpeg_data, [101, 0])
59
+ r = @readerclass.new(f) rescue nil
60
+ end
61
+ end
62
+
63
+ def test_io_returns_nil_after_random_sized_reads
64
+ loop do
65
+ f = SometimesNilIO.new(@data)
66
+ r = @readerclass.new(f)
67
+ r.each{}
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ require 'helper'
2
+
3
+ module Axon
4
+ class TestBilinearScaler < AxonTestCase
5
+ def test_scaling
6
+ @image.scale_bilinear(0.7).write_jpeg(@io_out)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'helper'
2
+
3
+ module Axon
4
+ class TestCropper < AxonTestCase
5
+ def test_crop
6
+ @image.scale_nearest_neighbor(40).crop(320, 123).write_jpeg(@io_out)
7
+ end
8
+ end
9
+ end
data/test/test_exif.rb ADDED
@@ -0,0 +1,39 @@
1
+ require 'helper'
2
+
3
+ module Axon
4
+ class TestExif < AxonTestCase
5
+ def test_exif_roundtrip
6
+ random_data = ""
7
+ (100).times do
8
+ random_data << [rand].pack('d') # 800 bytes random data
9
+ end
10
+
11
+ writer = @image.to_jpeg
12
+ writer.exif = random_data
13
+
14
+ image_with_exif = Axon.JPEG(writer.data)
15
+ assert_equal random_data, image_with_exif.exif
16
+ end
17
+
18
+ def test_exif_with_icc_roundtrip
19
+ random_icc_data = ""
20
+ (2**16).times do # a little larger than one jpeg segment
21
+ random_icc_data << [rand].pack('d') # 8 bytes random data
22
+ end
23
+
24
+ random_exif_data = ""
25
+ (100).times do
26
+ random_exif_data << [rand].pack('d') # 800 bytes random data
27
+ end
28
+
29
+ writer = @image.to_jpeg
30
+ writer.icc_profile = random_icc_data
31
+ writer.exif = random_exif_data
32
+
33
+ image_with_exif_and_icc = Axon.JPEG(writer.data)
34
+
35
+ assert_equal random_exif_data, image_with_exif_and_icc.exif
36
+ assert_equal random_icc_data, image_with_exif_and_icc.icc_profile
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ require "helper"
2
+
3
+ module Axon
4
+ class TestGenerator < AxonTestCase
5
+ def test_new_solid
6
+ g = Solid.new 10, 15, "\x0A\x14\x69"
7
+ g.write_jpeg(@io_out)
8
+ end
9
+ end
10
+ end
data/test/test_icc.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'helper'
2
+
3
+ module Axon
4
+ class TestICC < AxonTestCase
5
+ def test_large_icc_roundtrip
6
+ random_data = ""
7
+ (2**16).times do # a little larger than one jpeg segment
8
+ random_data << [rand].pack('d') # 8 bytes random data
9
+ end
10
+
11
+ writer = @image.to_jpeg
12
+ writer.icc_profile = random_data
13
+
14
+ image_with_icc = Axon.JPEG(writer.data)
15
+ assert_equal random_data, image_with_icc.icc_profile
16
+ end
17
+ end
18
+ end
data/test/test_jpeg.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "helper"
2
+
3
+ module Axon
4
+ class TestJPEG < AxonTestCase
5
+ def test_version
6
+ assert JPEG_LIB_VERSION >= 62
7
+ end
8
+ end
9
+ end