qrio 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.
@@ -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