qrio 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,113 @@
1
+ module Qrio
2
+ # a rectangular matrix of bits
3
+ class Region
4
+ attr_reader :x1, :y1, :x2, :y2, :orientation
5
+
6
+ def initialize(x1, y1, x2, y2)
7
+ @x1 = x1
8
+ @y1 = y1
9
+ @x2 = x2
10
+ @y2 = y2
11
+
12
+ set_orientation
13
+ end
14
+
15
+ def left; x1; end
16
+ def right; x2; end
17
+ def top; y1; end
18
+ def bottom; y2; end
19
+
20
+ def top_left
21
+ [x1, y1]
22
+ end
23
+
24
+ def bottom_right
25
+ [x2, y2]
26
+ end
27
+
28
+ def to_coordinates
29
+ [top_left, bottom_right].flatten
30
+ end
31
+
32
+ def to_point_size
33
+ [top_left, width, height].flatten
34
+ end
35
+
36
+ def to_s
37
+ "R[#{ to_coordinates.join(',') }]"
38
+ end
39
+
40
+ def hash
41
+ to_s.hash
42
+ end
43
+
44
+ def eql?(other)
45
+ self == other
46
+ end
47
+
48
+ def ==(other)
49
+ to_s == other.to_s
50
+ end
51
+
52
+ def width
53
+ x2 - x1 + 1
54
+ end
55
+
56
+ def height
57
+ y2 - y1 + 1
58
+ end
59
+
60
+ def center
61
+ [left + width / 2, top + height / 2]
62
+ end
63
+
64
+ def horizontal?; orientation == :horizontal; end
65
+ def vertical?; orientation == :vertical; end
66
+
67
+ def orientation_matches?(other)
68
+ orientation == other.orientation
69
+ end
70
+
71
+ def union(other)
72
+ self.class.new(
73
+ [left, other.left].min,
74
+ [top, other.top].min,
75
+ [right, other.right].max,
76
+ [bottom, other.bottom].max
77
+ )
78
+ end
79
+
80
+ def translate(xoffset, yoffset)
81
+ self.class.new(
82
+ left - xoffset,
83
+ top - yoffset,
84
+ right - xoffset,
85
+ bottom - yoffset
86
+ )
87
+ end
88
+
89
+ # return a new region that would be the result of rotating
90
+ # a matrix of width x height containing this region
91
+ def rotate(mw, mh)
92
+ self.class.new(
93
+ mh - bottom - 1,
94
+ left,
95
+ mh - top - 1,
96
+ right
97
+ )
98
+ end
99
+
100
+ private
101
+
102
+ def set_orientation
103
+ @orientation = case
104
+ when width > height
105
+ :horizontal
106
+ when height > width
107
+ :vertical
108
+ else
109
+ :square
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,152 @@
1
+ module Qrio
2
+ class SamplingGrid
3
+ attr_reader :origin_corner, :top_right, :bottom_left,
4
+ :orientation, :bounds, :angles,
5
+ :block_width, :block_height, :provisional_version,
6
+ :finder_patterns, :matrix
7
+
8
+ def initialize(matrix, finder_patterns)
9
+ @matrix = matrix
10
+ @finder_patterns = finder_patterns
11
+ @angles = []
12
+
13
+ find_origin_corner
14
+ detect_orientation
15
+ end
16
+
17
+ def find_origin_corner(normalized=false)
18
+ build_finder_pattern_neighbors
19
+
20
+ shared_corners = @finder_patterns.select do |fp|
21
+ fp.neighbors.select(&:right_angle?).count > 1
22
+ end
23
+
24
+ # TODO : handle multiple possible matches
25
+ if @origin_corner = shared_corners.first
26
+ if normalized
27
+ # we have correct orientation, identify fp positions
28
+ @top_right = non_origin_finder_patterns.map(&:destination).detect{|fp| fp.center.last < @matrix.height / 2.0 }
29
+ @bottom_left = non_origin_finder_patterns.map(&:destination).detect{|fp| fp.center.first < @matrix.width / 2.0 }
30
+ else
31
+ set_bounds
32
+ end
33
+ end
34
+ end
35
+
36
+ def set_bounds
37
+ @bounds = @origin_corner.dup
38
+ @bounds.neighbors.select(&:right_angle?).each do |n|
39
+ @bounds = @bounds.union(n.destination)
40
+ end
41
+ end
42
+
43
+ # which way is the QR rotated?
44
+ # 0) normal - shared finder patterns in top left
45
+ # 1) - shared finder patterns in top right
46
+ # 2) - shared finder patterns in bottom right
47
+ # 3) - shared finder patterns in bottom left
48
+ def detect_orientation
49
+ # TODO : handle multiple possible matches
50
+ other_corners = non_origin_finder_patterns
51
+
52
+ dc = other_corners.map(&:distance).inject(0){|s,d| s + d } / 2.0
53
+ threshold = dc / 2.0
54
+
55
+ other_corners = other_corners.map(&:destination)
56
+
57
+ set_block_dimensions(@origin_corner, *other_corners)
58
+ @provisional_version = ((dc / @block_width).round - 10) / 4
59
+
60
+ xs = other_corners.map{|fp| fp.center.first }
61
+ ys = other_corners.map{|fp| fp.center.last }
62
+
63
+ above = ys.select{|y| y < (@origin_corner.center.last - threshold) }
64
+ left = xs.select{|x| x < (@origin_corner.center.first - threshold) }
65
+
66
+ @orientation = if above.any?
67
+ left.any? ? 2 : 3
68
+ else
69
+ left.any? ? 1 : 0
70
+ end
71
+ end
72
+
73
+ def non_origin_finder_patterns
74
+ @origin_corner.neighbors.select(&:right_angle?)[0,2]
75
+ end
76
+
77
+ def build_finder_pattern_neighbors
78
+ @finder_patterns.each do |source|
79
+ @finder_patterns.each do |destination|
80
+ next if source.center == destination.center
81
+ @angles << Neighbor.new(source, destination)
82
+ end
83
+ end
84
+ end
85
+
86
+ def set_block_dimensions(*finder_patterns)
87
+ @block_width = finder_patterns.inject(0){|s,f| s + f.width } / 21.0
88
+ @block_height = finder_patterns.inject(0){|s,f| s + f.height } / 21.0
89
+ end
90
+
91
+ def logical_width
92
+ @matrix.width / @block_width
93
+ end
94
+
95
+ def logical_height
96
+ @matrix.height / @block_height
97
+ end
98
+
99
+ def normalize
100
+ @matrix = @matrix.extract(*@bounds.to_point_size)
101
+ translate(*@bounds.top_left)
102
+
103
+ if @orientation > 0
104
+ (4 - @orientation).times do
105
+ rotate
106
+ end
107
+ end
108
+
109
+ find_origin_corner(true)
110
+ end
111
+
112
+ def translate(x, y)
113
+ @angles = []
114
+ other_corners = non_origin_finder_patterns.map(&:destination)
115
+
116
+ @origin_corner = @origin_corner.translate(x, y)
117
+ translated = [@origin_corner]
118
+ translated += other_corners.map{|c| c.translate(x, y) }
119
+
120
+ @finder_patterns = translated
121
+ end
122
+
123
+ def rotate
124
+ @matrix = @matrix.rotate
125
+ @finder_patterns.map!{|f| f.rotate(@bounds.width, @bounds.height) }
126
+ end
127
+
128
+ def extracted_pixels
129
+ start_x = @origin_corner.center.first - (@block_width * 3.0)
130
+ start_y = @origin_corner.center.last - (@block_height * 3.0)
131
+
132
+ dest_x = @bottom_left.center.first - (@block_width * 3.0)
133
+ dest_y = @top_right.center.last - (@block_height * 3.0)
134
+
135
+ # TODO : take bottom right alignment pattern into consideration
136
+ dy = (dest_y - start_y) / logical_width
137
+ dx = (dest_x - start_x) / logical_height
138
+
139
+ logical_height.round.times do |row_index|
140
+ row_start_x = start_x + row_index * dx
141
+ row_start_y = start_y + (row_index * @block_height)
142
+
143
+ logical_width.round.times do |col_index|
144
+ x = row_start_x + (col_index * @block_width)
145
+ y = row_start_y + (col_index * dy)
146
+
147
+ yield x.round, y.round
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,3 @@
1
+ module Qrio
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ module Qrio
2
+ class VerticalMatch < FinderPatternSlice
3
+ def self.build(offset, origin, terminus)
4
+ new(offset, origin, offset, terminus)
5
+ end
6
+
7
+ def offset; left; end
8
+ def origin; top; end
9
+ def terminus; bottom; end
10
+
11
+ def length
12
+ height
13
+ end
14
+
15
+ def breadth
16
+ width
17
+ end
18
+
19
+ def left_of?(other)
20
+ right < other.left
21
+ end
22
+
23
+ def right_of?(other)
24
+ left > other.right
25
+ end
26
+
27
+ def offset_diff(other)
28
+ left_of?(other) ? other.left - right : left - other.right
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "qrio/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "qrio"
7
+ s.version = Qrio::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Solomon White"]
10
+ s.email = ["rubysolo@gmail.com"]
11
+ s.homepage = "http://github.com/rubysolo/qrio"
12
+ s.licenses = %w(MIT)
13
+ s.summary = %q(QR code decoder)
14
+ s.description = %q(QR code decoder in pure Ruby)
15
+
16
+ s.add_runtime_dependency "chunky_png"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+ end
23
+
@@ -0,0 +1,121 @@
1
+ require_relative '../lib/qrio'
2
+ require 'test/unit'
3
+
4
+ class TestFinderPatternSlice < Test::Unit::TestCase
5
+ def test_ratio_matching
6
+ [
7
+ [ 1, 1, 3, 1, 1],
8
+ [10,10,30,10,10],
9
+ [11, 9,28,10,12],
10
+ [ 8, 8,19, 9, 8],
11
+ [11,12,34,13,12],
12
+ [12,20,46,19,14],
13
+ [10,13,32,14, 8],
14
+ [10,12,34,13, 7],
15
+ [11,13,32,14, 8],
16
+ ].each{|b| assert_matches_ratio b }
17
+
18
+ (9..12).to_a.each do |a|
19
+ (11..13).to_a.each do |b|
20
+ (31..34).to_a.each do |c|
21
+ (13..15).to_a.each do |d|
22
+ (7..9).to_a.each do |e|
23
+ assert_matches_ratio [a, b, c, d, e]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def test_intersection_detection
32
+ slice1 = Qrio::HorizontalMatch.build(27, 16, 62)
33
+ slice1 = slice1.union Qrio::HorizontalMatch.build(45, 16, 62)
34
+
35
+ slice2 = Qrio::VerticalMatch.build(27, 16, 62)
36
+ slice2 = slice2.union Qrio::VerticalMatch.build(44, 16, 62)
37
+
38
+ assert slice1.length_matches?(slice2)
39
+ assert slice1.breadth_matches?(slice2)
40
+ assert slice1.intersects? slice2
41
+
42
+ slice1 = Qrio::HorizontalMatch.build(21, 5, 57)
43
+ slice1 = slice1.union Qrio::HorizontalMatch.build(35, 5, 57)
44
+
45
+ slice2 = Qrio::VerticalMatch.build(22, 3, 54)
46
+ slice2 = slice2.union Qrio::VerticalMatch.build(38, 3, 54)
47
+
48
+ assert slice1.intersects? slice2
49
+ end
50
+
51
+ =begin
52
+ def test_slice_ratio
53
+ correct = [
54
+ [16, 27, 62, 45],
55
+ [27, 16, 44, 62],
56
+ [10, 27, 62, 45],
57
+ [27, 10, 44, 62],
58
+ [ 5, 21, 57, 35]
59
+ ].map{|a| @s.new(*a) }
60
+
61
+ correct.each do |slice|
62
+ assert slice.has_correct_ratio?, slice.ratio
63
+ end
64
+
65
+ incorrect = [
66
+ [16, 27, 62, 28],
67
+ [27, 16, 30, 62],
68
+ ].map{|a| @s.new(*a) }
69
+
70
+ incorrect.each do |slice|
71
+ assert ! slice.has_correct_ratio?, slice.ratio
72
+ end
73
+ end
74
+ =end
75
+
76
+ def test_slice_builder
77
+ slice = Qrio::FinderPatternSlice.build_matching(
78
+ 10, 5,
79
+ [1, 1, 3, 1, 1],
80
+ :horizontal
81
+ )
82
+
83
+ assert slice.is_a?(Qrio::HorizontalMatch)
84
+ assert_equal 10, slice.offset
85
+ assert_equal 5, slice.origin
86
+ assert_equal 11, slice.terminus
87
+ assert_equal 5, slice.left
88
+ assert_equal 11, slice.right
89
+ assert_equal 10, slice.top
90
+ assert_equal 10, slice.bottom
91
+ assert_equal 1, slice.height
92
+ assert_equal 7, slice.width
93
+
94
+ slice = Qrio::FinderPatternSlice.build_matching(
95
+ 23, 15,
96
+ [1, 1, 3, 1, 1],
97
+ :vertical
98
+ )
99
+
100
+ assert slice.is_a?(Qrio::VerticalMatch)
101
+ assert_equal 23, slice.offset
102
+ assert_equal 15, slice.origin
103
+ assert_equal 21, slice.terminus
104
+ assert_equal 23, slice.left
105
+ assert_equal 23, slice.right
106
+ assert_equal 15, slice.top
107
+ assert_equal 21, slice.bottom
108
+ assert_equal 7, slice.height
109
+ assert_equal 1, slice.width
110
+ end
111
+
112
+ private
113
+
114
+ def assert_matches_ratio(widths)
115
+ norm = Qrio::FinderPatternSlice.normalized_ratio(widths)
116
+ norm = norm.map{|n| '%.2f' % n }
117
+ msg = "Expected #{ widths.join('|') } [#{ norm.join('|') }]"
118
+ msg << " to match finder pattern ratio"
119
+ assert Qrio::FinderPatternSlice.matches_ratio?(widths), msg
120
+ end
121
+ end