qrio 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|