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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +5 -0
- data/.rvmrc.example +1 -0
- data/Gemfile +3 -0
- data/README.md +66 -0
- data/Rakefile +8 -0
- data/examples/extract_qr +18 -0
- data/lib/qrio.rb +15 -0
- data/lib/qrio/finder_pattern_slice.rb +136 -0
- data/lib/qrio/horizontal_match.rb +31 -0
- data/lib/qrio/image_dumper.rb +97 -0
- data/lib/qrio/image_loader/png_image_loader.rb +18 -0
- data/lib/qrio/matrix.rb +72 -0
- data/lib/qrio/neighbor.rb +40 -0
- data/lib/qrio/qr.rb +207 -0
- data/lib/qrio/qr_matrix.rb +403 -0
- data/lib/qrio/region.rb +113 -0
- data/lib/qrio/sampling_grid.rb +152 -0
- data/lib/qrio/version.rb +3 -0
- data/lib/qrio/vertical_match.rb +31 -0
- data/qrio.gemspec +23 -0
- data/test/finder_pattern_slice_test.rb +121 -0
- data/test/fixtures/block_test.qr +25 -0
- data/test/fixtures/finder_pattern1.png +0 -0
- data/test/fixtures/finder_pattern2.png +0 -0
- data/test/fixtures/finder_pattern3.png +0 -0
- data/test/fixtures/finder_pattern4.png +0 -0
- data/test/fixtures/masked0.qr +25 -0
- data/test/fixtures/no_finder_pattern1.png +0 -0
- data/test/fixtures/qrio-codewords.txt +100 -0
- data/test/fixtures/qrio.qr +33 -0
- data/test/horizontal_match_test.rb +34 -0
- data/test/matrix_test.rb +66 -0
- data/test/qr_matrix_test.rb +164 -0
- data/test/qr_test.rb +107 -0
- data/test/region_test.rb +87 -0
- data/test/sampling_grid_test.rb +78 -0
- metadata +111 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
|##|##|##|#| *| $|$$|.$|$_| #|##|##|##|
|
2
|
+
|# | | |#| *|$ |$$|..|_$| #| | | #|
|
3
|
+
|# |##|# |#| *| |__|.$|__| #| #|##| #|
|
4
|
+
|# |##|# |#| *| |_ |.$|_ | #| #|##| #|
|
5
|
+
|# |##|# |#| *|. |$$|_.|$ | #| #|##| #|
|
6
|
+
|# | | |#| *|$.|$$|__| $| #| | | #|
|
7
|
+
|##|##|##|#| %| %| %| %| %| #|##|##|##|
|
8
|
+
| | | | | *|..| |_$| | | | | |
|
9
|
+
|**|**|**|%|**|..| $|_$| $|**|**|**|**|
|
10
|
+
| $|$_| $| |$$| $|.$| $|__|$$|.$|__| |
|
11
|
+
|$ |$_|$ |%|__|$ |$$| |_$| |..|__|$ |
|
12
|
+
|$ |$_|$ | |$_| |..| $|__| |..|$_| |
|
13
|
+
| $|_ | |%|_ |$ |. |$$|_ |$$|_$|$_| |
|
14
|
+
|X |$ |. | |$$|_ | $|. | | |_$| |$$|
|
15
|
+
|XX|$ |$.|%| |$_|$$|..| $|..|__| |__|
|
16
|
+
|XX|$ |$.| |$ |__| |.$| |..|__|$ |__|
|
17
|
+
|XX| $|..|%| $|$_| $|$$| #|##|##| $|__|
|
18
|
+
| | | | | *| $|$_| $|$#| | #|__| $|
|
19
|
+
|##|##|##|#| *|$ |$$| |$#| #| #|__| |
|
20
|
+
|# | | |#| *| |__|$$|$#| | #|$_| |
|
21
|
+
|# |##|# |#| *| $|_ | |$#|##|##|$$| |
|
22
|
+
|# |##|# |#| *|. |$ |. |__|$$|$_| |$_|
|
23
|
+
|# |##|# |#| *|$.|$$|..|__|$.|_$| |__|
|
24
|
+
|# | | |#| *|..| |$$| $| |__| $|__|
|
25
|
+
|##|##|##|#| *|.$| $|..|$$| |__| |__|
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|##|##|##|#| #| |# | |##| #|##|##|##|
|
2
|
+
|# | | |#| #| | #|# |##| #| | | #|
|
3
|
+
|# |##|# |#| #| #| #| | #| #| #|##| #|
|
4
|
+
|# |##|# |#| #|# |# |##|# | #| #|##| #|
|
5
|
+
|# |##|# |#| #| #|# | #|##| #| #|##| #|
|
6
|
+
|# | | |#| #| | #|# |##| #| | | #|
|
7
|
+
|##|##|##|#| #| #| #| #| #| #|##|##|##|
|
8
|
+
| | | | | #|# |# |##|# | | | | |
|
9
|
+
|##|##|##|#|##| #| | | |##|##|##|##|
|
10
|
+
| |##| | | #|##|##|##|# | #|##|# |# |
|
11
|
+
| | | |#| #|##|# | #| | #| #| #|##|
|
12
|
+
|##|##|##| | |# |# |##|# |# |# | |# |
|
13
|
+
|##|# |# |#| #|##| #|# | #|# | |##| #|
|
14
|
+
|##|##| #| | #|# |##|# |# |# |##|# | #|
|
15
|
+
| #| | |#| #|##|# | #| | #| #| #| #|
|
16
|
+
|# |##|##| | |# |# |##|# |# |# | |# |
|
17
|
+
| #|##|# |#| |##| |# | #|##|##| | #|
|
18
|
+
| | | | | #|##| |##| #| | #|# |##|
|
19
|
+
|##|##|##|#| #|##|# | #|##| #| #| #| #|
|
20
|
+
|# | | |#| #|# |# | #| #| | #| |# |
|
21
|
+
|# |##|# |#| #| | #| #|##|##|##|# | #|
|
22
|
+
|# |##|# |#| #|# | |# |# | #| |# | |
|
23
|
+
|# |##|# |#| #|##|# | #| #|##| | #| #|
|
24
|
+
|# | | |#| #|# |# | #|##|# |# |##|# |
|
25
|
+
|##|##|##|#| #| | | #|# | #| #| #| #|
|
Binary file
|
@@ -0,0 +1,100 @@
|
|
1
|
+
01000010
|
2
|
+
11110110
|
3
|
+
11110110
|
4
|
+
11000110
|
5
|
+
00000110
|
6
|
+
01110110
|
7
|
+
11010010
|
8
|
+
11110010
|
9
|
+
10000111
|
10
|
+
10010111
|
11
|
+
11110111
|
12
|
+
11110111
|
13
|
+
01000111
|
14
|
+
01000110
|
15
|
+
00100111
|
16
|
+
00010111
|
17
|
+
01000111
|
18
|
+
10000111
|
19
|
+
01010110
|
20
|
+
00100110
|
21
|
+
00000111
|
22
|
+
01010110
|
23
|
+
00100111
|
24
|
+
10010110
|
25
|
+
00110011
|
26
|
+
00100010
|
27
|
+
10010111
|
28
|
+
11110000
|
29
|
+
10100010
|
30
|
+
11100110
|
31
|
+
00110110
|
32
|
+
11101100
|
33
|
+
11110010
|
34
|
+
00110110
|
35
|
+
11110110
|
36
|
+
00010001
|
37
|
+
01010000
|
38
|
+
01110010
|
39
|
+
00010010
|
40
|
+
00110100
|
41
|
+
00100001
|
42
|
+
11101101
|
43
|
+
10001010
|
44
|
+
00010011
|
45
|
+
10001110
|
46
|
+
11100110
|
47
|
+
11000111
|
48
|
+
00010001
|
49
|
+
11000101
|
50
|
+
10011011
|
51
|
+
10101110
|
52
|
+
00100101
|
53
|
+
11011111
|
54
|
+
00010101
|
55
|
+
10111101
|
56
|
+
11111111
|
57
|
+
10111000
|
58
|
+
00011111
|
59
|
+
00010110
|
60
|
+
01010000
|
61
|
+
01111110
|
62
|
+
10010101
|
63
|
+
10010001
|
64
|
+
10111100
|
65
|
+
01011100
|
66
|
+
10000011
|
67
|
+
00001010
|
68
|
+
11010010
|
69
|
+
01100011
|
70
|
+
11010011
|
71
|
+
11110101
|
72
|
+
11001001
|
73
|
+
00000111
|
74
|
+
01111111
|
75
|
+
11011001
|
76
|
+
01110011
|
77
|
+
00000101
|
78
|
+
10110001
|
79
|
+
00111000
|
80
|
+
10111110
|
81
|
+
10111011
|
82
|
+
01000010
|
83
|
+
10100000
|
84
|
+
10001100
|
85
|
+
11001100
|
86
|
+
11000100
|
87
|
+
11100110
|
88
|
+
00111110
|
89
|
+
00011110
|
90
|
+
11011100
|
91
|
+
10001111
|
92
|
+
01111010
|
93
|
+
11110100
|
94
|
+
00111111
|
95
|
+
10010000
|
96
|
+
10101010
|
97
|
+
00100100
|
98
|
+
11110100
|
99
|
+
01011110
|
100
|
+
11011101
|
@@ -0,0 +1,33 @@
|
|
1
|
+
xxxxxxx x x x x x xxxxxxx
|
2
|
+
x x x xxxxxx xxx x x x
|
3
|
+
x xxx x xx xxx xx xxxx x xxx x
|
4
|
+
x xxx x x x xx x x xx x xxx x
|
5
|
+
x xxx x xx xx x xxxx xx x xxx x
|
6
|
+
x x xxx x xxxx x x
|
7
|
+
xxxxxxx x x x x x x x x x xxxxxxx
|
8
|
+
xxx x x xxx xx
|
9
|
+
xx xx xx x xxx x x xx
|
10
|
+
xx x xx xx x xxxxx x xxx
|
11
|
+
xxx xx xxxxxxxx xx x xxx
|
12
|
+
xxx x x xx xx xxxx xxxxxx
|
13
|
+
x xxx xxx x x xx x x x x x
|
14
|
+
xx x x x xxxxx x x xx x x x
|
15
|
+
x xxx x x xx xx x xx
|
16
|
+
x xxx x x x xx xxxx x x x
|
17
|
+
x xx x x x x x x x xx xx x
|
18
|
+
x xx x x x x xxxx x
|
19
|
+
x x xxxx xxxx xxxx xx xxx xx x
|
20
|
+
x xx xxxx x x xxxx xxxx
|
21
|
+
x x x x xxx x x x x
|
22
|
+
xx x xx x x x xxx xx
|
23
|
+
x x x x x x x x xx xx xxx
|
24
|
+
x x x x xx x x xx xx xx xxx
|
25
|
+
xxxxx x xxx x xx xxxxx
|
26
|
+
xx x x x x xx xx x
|
27
|
+
xxxxxxx xxx xx xxxxxxxx x xx
|
28
|
+
x x xxx xx x x xx xxxx
|
29
|
+
x xxx x xxx xx xx xx xxxxxx x
|
30
|
+
x xxx x xxx x x x x x x
|
31
|
+
x xxx x x x x x x x xx x xx
|
32
|
+
x x x xx x x x xxxx
|
33
|
+
xxxxxxx x x x x x xxxx xx
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative '../lib/qrio'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class TestHorizontalMatch < Test::Unit::TestCase
|
5
|
+
def test_adjacency_detection
|
6
|
+
hmatch1 = Qrio::HorizontalMatch.build(2, 0, 8)
|
7
|
+
hmatch2 = Qrio::HorizontalMatch.build(3, 0, 8)
|
8
|
+
hmatch3 = Qrio::HorizontalMatch.build(3, 5, 13)
|
9
|
+
|
10
|
+
assert hmatch1.origin_matches?(hmatch2)
|
11
|
+
assert hmatch1.terminus_matches?(hmatch2)
|
12
|
+
assert hmatch1.endpoints_match?(hmatch2)
|
13
|
+
assert hmatch1.offset_matches?(hmatch2)
|
14
|
+
assert hmatch1.adjacent?(hmatch2)
|
15
|
+
|
16
|
+
refute hmatch1.adjacent?(hmatch3)
|
17
|
+
|
18
|
+
# slice1 = @s.new(3, 26, 82, 39)
|
19
|
+
# slice2 = @s.new(3, 40, 81, 58)
|
20
|
+
# assert slice1.adjacent?(slice2)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_slice_sorting
|
24
|
+
input = [
|
25
|
+
[2, 0, 8],
|
26
|
+
[3, 1, 9],
|
27
|
+
[4, 0, 8]
|
28
|
+
].map{|c| Qrio::HorizontalMatch.build(*c) }
|
29
|
+
output = input.sort
|
30
|
+
|
31
|
+
assert_equal [0, 1, 0], output.map{|s| s.left }
|
32
|
+
assert_equal [2, 3, 4], output.map{|s| s.top }
|
33
|
+
end
|
34
|
+
end
|
data/test/matrix_test.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative '../lib/qrio'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class TestMatrix < Test::Unit::TestCase
|
5
|
+
def test_random_access
|
6
|
+
matrix = Qrio::Matrix.new([1, 1, 0, 0], 2, 2)
|
7
|
+
assert_equal 2, matrix.rows.length
|
8
|
+
assert_equal 2, matrix.columns.length
|
9
|
+
|
10
|
+
assert_equal [1, 1], matrix.rows[0]
|
11
|
+
assert_equal [0, 0], matrix.rows[1]
|
12
|
+
assert_equal [1, 0], matrix.columns[0]
|
13
|
+
assert_equal [1, 0], matrix.columns[1]
|
14
|
+
|
15
|
+
assert_equal 1, matrix[0, 0]
|
16
|
+
assert_equal 0, matrix[0, 1]
|
17
|
+
assert_equal 1, matrix[1, 0]
|
18
|
+
assert_equal 0, matrix[1, 1]
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_rotation
|
22
|
+
matrix = Qrio::Matrix.new([1, 0, 1, 0, 0, 0, 0, 1, 0], 3, 3)
|
23
|
+
rotated = matrix.rotate
|
24
|
+
|
25
|
+
assert_equal [0, 0, 1], rotated.rows[0]
|
26
|
+
assert_equal [1, 0, 0], rotated.rows[1]
|
27
|
+
assert_equal [0, 0, 1], rotated.rows[2]
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_extraction
|
31
|
+
matrix = Qrio::Matrix.new([
|
32
|
+
1, 0, 0, 0,
|
33
|
+
0, 1, 1, 0,
|
34
|
+
0, 0, 1, 0,
|
35
|
+
0, 0, 0, 1
|
36
|
+
], 4, 4)
|
37
|
+
|
38
|
+
center = matrix.extract(1, 1, 2, 2)
|
39
|
+
assert_equal 2, center.width
|
40
|
+
assert_equal 2, center.height
|
41
|
+
|
42
|
+
assert_equal [1, 1], center.rows[0]
|
43
|
+
assert_equal [0, 1], center.rows[1]
|
44
|
+
|
45
|
+
extracted = matrix.extract(1, 1, 3, 2)
|
46
|
+
assert_equal 3, extracted.width
|
47
|
+
assert_equal 2, extracted.height
|
48
|
+
|
49
|
+
assert_equal [1, 1, 0], extracted.rows[0]
|
50
|
+
assert_equal [0, 1, 0], extracted.rows[1]
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_set_bit
|
54
|
+
bits = Array.new(25, false)
|
55
|
+
matrix = Qrio::Matrix.new(bits, 5, 5)
|
56
|
+
refute matrix[4, 4]
|
57
|
+
refute matrix.rows.last.last
|
58
|
+
refute matrix.columns.last.last
|
59
|
+
|
60
|
+
matrix[4, 4] = true
|
61
|
+
|
62
|
+
assert matrix[4, 4]
|
63
|
+
assert matrix.rows.last.last
|
64
|
+
assert matrix.columns.last.last
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require_relative '../lib/qrio'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class TestQrMatrix < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@bits, @qr = make_qr("block_test")
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_to_s
|
10
|
+
assert_equal bits_to_string(@bits, @qr.width), @qr.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_format_detection
|
14
|
+
@qr[1, 8] = false
|
15
|
+
assert_equal 'M', @qr.error_correction_level
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def test_dimensions
|
20
|
+
assert_equal 25, @qr.width
|
21
|
+
assert_equal 25, @qr.height
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_position_detection
|
25
|
+
assert @qr.in_finder_pattern?(0, 6)
|
26
|
+
assert @qr.in_finder_pattern?(0, 7)
|
27
|
+
assert @qr.in_finder_pattern?(0, 8)
|
28
|
+
refute @qr.in_finder_pattern?(0, 9)
|
29
|
+
|
30
|
+
assert @qr.in_finder_pattern?(6, 0)
|
31
|
+
assert @qr.in_finder_pattern?(7, 0)
|
32
|
+
assert @qr.in_finder_pattern?(8, 0)
|
33
|
+
refute @qr.in_finder_pattern?(9, 0)
|
34
|
+
|
35
|
+
assert @qr.in_finder_pattern?(8, 3)
|
36
|
+
|
37
|
+
assert @qr.in_alignment_line?(14, 6)
|
38
|
+
assert @qr.in_alignment_line?(6, 14)
|
39
|
+
|
40
|
+
|
41
|
+
assert_equal 2, @qr.version
|
42
|
+
assert @qr.in_alignment_pattern?(16, 16)
|
43
|
+
assert @qr.in_alignment_pattern?(18, 18)
|
44
|
+
assert @qr.in_alignment_pattern?(20, 20)
|
45
|
+
|
46
|
+
refute @qr.in_alignment_pattern?(15, 16)
|
47
|
+
refute @qr.in_alignment_pattern?(16, 15)
|
48
|
+
|
49
|
+
refute @qr.in_alignment_pattern?(21, 20)
|
50
|
+
refute @qr.in_alignment_pattern?(20, 21)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_alignment_pattern_detection_by_version
|
54
|
+
# version one has no alignment patterns
|
55
|
+
v1 = Qrio::QrMatrix.new(Array.new(441, false), 21, 21)
|
56
|
+
assert_equal 1, v1.version
|
57
|
+
|
58
|
+
(0...20).each do |c|
|
59
|
+
refute v1.in_alignment_pattern?(c, c)
|
60
|
+
end
|
61
|
+
|
62
|
+
# construct versions 2 - 10 and verify alignment patterns
|
63
|
+
[18, 22, 26, 30, 34, [22, 38], [24, 42], [26, 46], [28, 50]].each_with_index do |ap_centers, index|
|
64
|
+
version = index + 2
|
65
|
+
dimension = version * 4 + 17
|
66
|
+
bits = Array.new(dimension * dimension, false)
|
67
|
+
|
68
|
+
qr = Qrio::QrMatrix.new(bits, dimension, dimension)
|
69
|
+
assert_equal version, qr.version
|
70
|
+
|
71
|
+
qr.draw_alignment_patterns
|
72
|
+
verify_alignment_centers(qr, *ap_centers)
|
73
|
+
qr.raw_bytes.each do |byte|
|
74
|
+
assert_equal 0, byte
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_read_raw_bytes
|
80
|
+
bytes = @qr.raw_bytes
|
81
|
+
bytes.each_with_index do |byte, index|
|
82
|
+
assert_equal index + 1, byte
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_unmask
|
87
|
+
@qr[3,8] = false
|
88
|
+
assert_equal 0, @qr.mask_pattern
|
89
|
+
|
90
|
+
_, @masked = make_qr("masked0")
|
91
|
+
@qr.unmask
|
92
|
+
|
93
|
+
assert_equal @masked.raw_bytes, @qr.raw_bytes
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_text
|
97
|
+
_, qrio = make_qr("qrio")
|
98
|
+
qrio.unmask
|
99
|
+
assert_equal fixture_content("qrio-codewords.txt").split("\n"),
|
100
|
+
qrio.raw_bytes.map{|b| b.to_s(2).rjust(8, '0') }
|
101
|
+
|
102
|
+
assert_equal 4, qrio.version
|
103
|
+
assert_equal 'H', qrio.error_correction_level
|
104
|
+
assert_equal 4, qrio.block_count
|
105
|
+
assert_equal 16, qrio.ecc_bytes_per_block
|
106
|
+
|
107
|
+
assert_equal "https://github.com/rubysolo/qrio", qrio.text
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def verify_alignment_centers(qr, *centers)
|
113
|
+
cols = *centers.dup
|
114
|
+
rows = *centers.dup
|
115
|
+
|
116
|
+
refute qr.in_alignment_pattern? 6, 6
|
117
|
+
refute qr.in_alignment_pattern? qr.width - 7, 8
|
118
|
+
refute qr.in_alignment_pattern? 8, qr.height - 7
|
119
|
+
|
120
|
+
cols.each do |cy|
|
121
|
+
rows.each do |cx|
|
122
|
+
assert qr.in_alignment_pattern?(cx, cy), "(#{ cx }, #{ cy }) should be ap center (version #{ qr.version })"
|
123
|
+
assert qr.in_alignment_pattern?(cx - 2, cy - 2)
|
124
|
+
assert qr.in_alignment_pattern?(cx - 2, cy + 2)
|
125
|
+
assert qr.in_alignment_pattern?(cx + 2, cy - 2)
|
126
|
+
assert qr.in_alignment_pattern?(cx + 2, cy + 2)
|
127
|
+
|
128
|
+
refute qr.in_alignment_pattern?(cx - 3, cy - 2), "(#{ cx - 3}, #{ cy - 2}) should be outside alignment pattern (version #{ qr.version })"
|
129
|
+
refute qr.in_alignment_pattern?(cx - 2, cy - 3)
|
130
|
+
refute qr.in_alignment_pattern?(cx + 3, cy + 2)
|
131
|
+
refute qr.in_alignment_pattern?(cx + 2, cy + 3)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def fixture_content(filename)
|
137
|
+
IO.read(File.expand_path("../fixtures/#{ filename }", __FILE__))
|
138
|
+
end
|
139
|
+
|
140
|
+
def make_qr(which)
|
141
|
+
raw = fixture_content("#{ which }.qr")
|
142
|
+
data = raw.strip.gsub(/\|/, '').split(/\n/)
|
143
|
+
|
144
|
+
width = data.first.length
|
145
|
+
height = data.length
|
146
|
+
off = ' _.,'.split(//)
|
147
|
+
|
148
|
+
bits = data.join('').split(//).map{|s| ! off.include?(s) }
|
149
|
+
[bits, Qrio::QrMatrix.new(bits, width, height)]
|
150
|
+
end
|
151
|
+
|
152
|
+
def bits_to_string(bits, width)
|
153
|
+
chars = bits.map{|b| b ? '#' : ' ' }
|
154
|
+
string = []
|
155
|
+
while row = chars.slice!(0, width)
|
156
|
+
break if row.nil? || row.empty?
|
157
|
+
string << row.join
|
158
|
+
end
|
159
|
+
string << nil
|
160
|
+
|
161
|
+
string.join("\n")
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|