axon 0.0.1

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 (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