barcode1dtools 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,246 @@
1
+ #--
2
+ # Copyright 2012 Michael Chaney Consulting Corporation
3
+ #
4
+ # Released under the terms of the MIT License or the GNU
5
+ # General Public License, v. 2
6
+ #++
7
+
8
+ require 'barcode1dtools/upc_a'
9
+
10
+ module Barcode1DTools
11
+
12
+ # Barcode1DTools::UPC_Supplemental_5 - Create pattern for UPC
13
+ # Supplemental 5 barcodes
14
+ #
15
+ # The value encoded is an 5-digit integer, and a checksum digit
16
+ # will be added. You can add the option :checksum_included => true
17
+ # when initializing to specify that you have already included a
18
+ # checksum. The bar patterns are the same as the left
19
+ # half of a standard UPC-A.
20
+ #
21
+ # num = '53999' # book price is US$39.99
22
+ # bc = Barcode1DTools::UPC_Supplemental_5.new(num)
23
+ # pattern = bc.bars
24
+ # rle_pattern = bc.rle
25
+ # bc.price # returns the "price" part as 4 digits
26
+ # bc.currency # returns the first digit currency code
27
+ # width = bc.width
28
+ # check_digit = Barcode1DTools::UPC_Supplemental_5.generate_check_digit_for(num)
29
+ #
30
+ # This type of barcode consists of 5 digits, and a check digit
31
+ # (a modulus 10 of the sum of the digits with weights of 3 and
32
+ # 9) that is encoded in the "parity" of the two barcode
33
+ # digits. It is positioned to the right of a UPC-A or EAN-13
34
+ # to create a "Bookland" code.
35
+ #
36
+ # The two are scanned together, and typically the scanner will
37
+ # return the five digits of the supplemental barcode
38
+ # immediately following the check digit from the main barcode.
39
+ # You will likely need to use the Barcode::UPC_A or
40
+ # Barcode::EAN_13 module in addition to this one to create the
41
+ # full code.
42
+ #
43
+ # The 5-digit supplement is generally used on literature, and
44
+ # represents a type-indicator digit followed by a 4-digit
45
+ # MSRP. The type is "0" for British Pound units, "5" for US
46
+ # Dollar units, and 9 for extra information. A code of
47
+ # "90000" means "no MSRP", "99991" indicates a complimentary
48
+ # copy, "99990" is used to mark used books (by college
49
+ # bookstores), and "90001" through "98999" are used internally
50
+ # by some publishers.
51
+ #
52
+ #== Rendering
53
+ #
54
+ # The 5-digit supplement is positioned to the right of the
55
+ # main UPC code, and the human-readable digits are usually
56
+ # printed above the supplemental barcode.
57
+ #
58
+ # A UPC-A is generally rendered at one inch across, then
59
+ # there's a 1/8th inch gap, then the supplemental. A UPC-A is
60
+ # 95 units wide, so the gap is 24 units wide. The 5-digit
61
+ # supplemental barcode is 47 units wide, essentially half an
62
+ # inch at this scale. Note that regardless of scale, the gap
63
+ # should be at least the smaller of 1/8th inch or 10 units.
64
+
65
+
66
+ class UPC_Supplemental_5 < Barcode1D
67
+
68
+ LEFT_PATTERNS = UPC_A::LEFT_PATTERNS
69
+ LEFT_PATTERNS_RLE = UPC_A::LEFT_PATTERNS_RLE
70
+
71
+ # parity patterns, essentially binary counting where "e" is "1"
72
+ # and "o" is "0"
73
+ PARITY_PATTERNS = {
74
+ '0' => 'eeooo',
75
+ '1' => 'eoeoo',
76
+ '2' => 'eooeo',
77
+ '3' => 'eoooe',
78
+ '4' => 'oeeoo',
79
+ '5' => 'ooeeo',
80
+ '6' => 'oooee',
81
+ '7' => 'oeoeo',
82
+ '8' => 'oeooe',
83
+ '9' => 'ooeoe'
84
+ };
85
+
86
+ LEFT_GUARD_PATTERN = '1011'
87
+ MIDDLE_GUARD_PATTERN = '01'
88
+ LEFT_GUARD_PATTERN_RLE = '112'
89
+ MIDDLE_GUARD_PATTERN_RLE = '11'
90
+
91
+ DEFAULT_OPTIONS = {
92
+ :line_character => '1',
93
+ :space_character => '0'
94
+ }
95
+
96
+ attr_reader :currency_code
97
+ attr_reader :price
98
+
99
+ class << self
100
+ # Returns true or false - must be 5 or 6 digits. This
101
+ # also handles the case where the leading 0 is added.
102
+ def can_encode?(value, options = nil)
103
+ if !options
104
+ value.to_s =~ /^\d{5,6}$/
105
+ elsif (options[:checksum_included])
106
+ value.to_s =~ /^\d{6}$/
107
+ else
108
+ value.to_s =~ /^\d{5}$/
109
+ end
110
+ end
111
+
112
+ # Generates check digit given a string to encode. It assumes there
113
+ # is no check digit on the "value".
114
+ def generate_check_digit_for(value)
115
+ mult = 9 # alternates 3 and 9
116
+ sprintf('%05d',value.to_i).reverse.chars.inject(0) { |a,c| mult = 12 - mult; a + c.to_i * mult } % 10
117
+ end
118
+
119
+ # validates the check digit given a string - assumes check digit
120
+ # is last digit of string.
121
+ def validate_check_digit_for(value)
122
+ raise UnencodableCharactersError unless self.can_encode?(value, :checksum_included => true)
123
+ md = value.match(/^(\d{5})(\d)$/)
124
+ self.generate_check_digit_for(md[1]) == md[2].to_i
125
+ end
126
+
127
+ def decode(str)
128
+ if str.length == 47
129
+ # bar pattern
130
+ str = bars_to_rle(str)
131
+ elsif str.length == 31 && str =~ /^[1-9]+$/
132
+ # rle
133
+ else
134
+ raise UnencodableCharactersError, "Pattern must be 47 unit bar pattern or 31 character rle."
135
+ end
136
+
137
+ # This string is "aaabbbb(ccdddd)" where "aaa" is the left
138
+ # guard pattern, "bbbb" is the first digit, "cc" is the
139
+ # intra-digit guard pattern, and "dddd" is the second
140
+ # digit. (ccdddd) occurs 4 times.
141
+
142
+ # See if the string is reversed
143
+ if str[28..30] == LEFT_GUARD_PATTERN_RLE.reverse && [7,13,19,25].all? { |x| str[29-x,2] == MIDDLE_GUARD_PATTERN_RLE.reverse }
144
+ str.reverse!
145
+ end
146
+
147
+ # Check the guard patterns
148
+ unless (str[0..2] == LEFT_GUARD_PATTERN_RLE && [7,13,19,25].all? { |x| str[x,2] == MIDDLE_GUARD_PATTERN_RLE.reverse })
149
+ raise UnencodableCharactersError, "Missing or incorrect guard patterns"
150
+ end
151
+
152
+ parity_sequence = ''
153
+ digits = ''
154
+ left_initial_offset = LEFT_GUARD_PATTERN_RLE.length
155
+
156
+ # Decode
157
+ (0..4).each do |left_offset|
158
+ found = false
159
+ digit_rle = str[(left_initial_offset + left_offset*6),4]
160
+ ['o','e'].each do |parity|
161
+ ('0'..'9').each do |digit|
162
+ if LEFT_PATTERNS_RLE[digit][parity] == digit_rle
163
+ parity_sequence += parity
164
+ digits += digit
165
+ found = true
166
+ break
167
+ end
168
+ end
169
+ end
170
+ raise UndecodableCharactersError, "Invalid sequence: #{digit_rle}" unless found
171
+ end
172
+
173
+ # Now, find the parity digit
174
+ parity_digit = nil
175
+ ('0'..'9').each do |x|
176
+ if PARITY_PATTERNS[x] == parity_sequence
177
+ parity_digit = x
178
+ break
179
+ end
180
+ end
181
+
182
+ raise UndecodableCharactersError, "Weird parity: #{parity_sequence}" unless parity_digit
183
+
184
+ UPC_Supplemental_5.new(digits + parity_digit, :checksum_included => true)
185
+ end
186
+
187
+ end
188
+
189
+ # Options are :line_character, :space_character, and
190
+ # :checksum_included.
191
+ def initialize(value, options = {})
192
+
193
+ @options = DEFAULT_OPTIONS.merge(options)
194
+
195
+ # Can we encode this value?
196
+ raise UnencodableCharactersError unless self.class.can_encode?(value, @options)
197
+
198
+ if @options[:checksum_included]
199
+ @encoded_string = value.to_s
200
+ raise ChecksumError unless self.class.validate_check_digit_for(@encoded_string)
201
+ md = @encoded_string.match(/^(\d+?)(\d)$/)
202
+ @value, @check_digit = md[1], md[2].to_i
203
+ else
204
+ # need to add a checksum
205
+ @value = value.to_s
206
+ @check_digit = self.class.generate_check_digit_for(@value)
207
+ @encoded_string = sprintf('%05d%1d',@value.to_i,@check_digit)
208
+ end
209
+
210
+ md = @value.match(/^(\d)(\d{4})/)
211
+ @currency_code, @price = md[1], md[2]
212
+ end
213
+
214
+ # not usable with EAN-style codes
215
+ def wn
216
+ raise NotImplementedError
217
+ end
218
+
219
+ # returns a run-length-encoded string representation
220
+ def rle
221
+ if @rle
222
+ @rle
223
+ else
224
+ md = @encoded_string.match(/(\d{5})(\d)$/)
225
+ @rle = gen_rle(md[1], md[2])
226
+ end
227
+ end
228
+
229
+ # returns 1s and 0s (for "black" and "white")
230
+ def bars
231
+ @bars ||= self.class.rle_to_bars(self.rle, @options)
232
+ end
233
+
234
+ # returns the total unit width of the bar code
235
+ def width
236
+ @width ||= rle.split('').inject(0) { |a,c| a + c.to_i }
237
+ end
238
+
239
+ private
240
+
241
+ def gen_rle(payload, parity_digit)
242
+ LEFT_GUARD_PATTERN_RLE + (0..4).collect { |n| LEFT_PATTERNS_RLE[payload[n,1]][PARITY_PATTERNS[parity_digit][n,1]] }.join(MIDDLE_GUARD_PATTERN_RLE)
243
+ end
244
+
245
+ end
246
+ end
@@ -0,0 +1,56 @@
1
+ #--
2
+ # Copyright 2012 Michael Chaney Consulting Corporation
3
+ #
4
+ # Released under the terms of the MIT License or the GNU
5
+ # General Public License, v. 2
6
+ #++
7
+
8
+ require 'test/unit'
9
+ require 'barcode1dtools'
10
+
11
+ class Barcode1DToolsTest < Test::Unit::TestCase
12
+ def setup
13
+ @options = {}
14
+ end
15
+
16
+ def teardown
17
+ end
18
+
19
+ def test_rle_to_bars
20
+ assert_equal '111001', Barcode1DTools::Barcode1D.rle_to_bars('321', @options)
21
+ assert_equal '10011100001', Barcode1DTools::Barcode1D.rle_to_bars('12341', @options)
22
+ end
23
+
24
+ def test_bars_to_rle
25
+ assert_equal '321', Barcode1DTools::Barcode1D.bars_to_rle('111001', @options)
26
+ assert_equal '12341', Barcode1DTools::Barcode1D.bars_to_rle('10011100001', @options)
27
+ end
28
+
29
+ def test_back_and_forth
30
+ random_rle = (0..19).collect { |x| (rand * 4 + 1).to_int.to_s }.join
31
+ random_bars = '1' + (0..((rand(25)+5)*2)).collect { |x| (x.odd? ? '1' : '0')*(rand(5)+1) }.join + '1'
32
+ random_wn = (0..19).collect { |x| rand < 0.5 ? 'w' : 'n' }.join
33
+ assert_equal random_rle, Barcode1DTools::Barcode1D.bars_to_rle(Barcode1DTools::Barcode1D.rle_to_bars(random_rle, @options), @options)
34
+ assert_equal random_bars, Barcode1DTools::Barcode1D.rle_to_bars(Barcode1DTools::Barcode1D.bars_to_rle(random_bars, @options), @options)
35
+ assert_equal random_wn, Barcode1DTools::Barcode1D.rle_to_wn(Barcode1DTools::Barcode1D.wn_to_rle(random_wn, @options), @options)
36
+ end
37
+
38
+ def test_bar_pair
39
+ assert_equal '01', Barcode1DTools::Barcode1D.bar_pair
40
+ assert_equal '56', Barcode1DTools::Barcode1D.bar_pair({ :space_character => 5, :line_character => 6 })
41
+ end
42
+
43
+ def test_wn_to_rle
44
+ assert_equal '12121211', Barcode1DTools::Barcode1D.wn_to_rle('nwnwnwnn')
45
+ end
46
+
47
+ def test_rle_to_wn
48
+ assert_equal 'wnwnwnww', Barcode1DTools::Barcode1D.rle_to_wn('21212122')
49
+ assert_equal 'wnwnwnww', Barcode1DTools::Barcode1D.rle_to_wn('31313133')
50
+ end
51
+
52
+ def test_wn_pair
53
+ assert_equal 'wn', Barcode1DTools::Barcode1D.wn_pair
54
+ assert_equal '65', Barcode1DTools::Barcode1D.wn_pair({ :n_character => '5', :w_character => '6' })
55
+ end
56
+ end
@@ -0,0 +1,102 @@
1
+ #--
2
+ # Copyright 2012 Michael Chaney Consulting Corporation
3
+ #
4
+ # Released under the terms of the MIT License or the GNU
5
+ # General Public License, v. 2
6
+ #++
7
+
8
+ require 'test/unit'
9
+ require 'barcode1dtools'
10
+
11
+ class Barcode1DToolsEAN13Test < Test::Unit::TestCase
12
+ def setup
13
+ end
14
+
15
+ def teardown
16
+ end
17
+
18
+ # Creates a random EAN-13 sans checksum
19
+ def random_12_digit_number
20
+ (0..11).collect { |x| ((rand * 10).to_i % 10).to_s }.join
21
+ end
22
+
23
+ def test_checksum_generation
24
+ assert_equal 7, Barcode1DTools::EAN13.generate_check_digit_for('007820601001')
25
+ end
26
+
27
+ def test_checksum_validation
28
+ assert Barcode1DTools::EAN13.validate_check_digit_for('0884088516338')
29
+ end
30
+
31
+ def test_attr_readers
32
+ ean = Barcode1DTools::EAN13.new('088408851633', :checksum_included => false)
33
+ assert_equal 8, ean.check_digit
34
+ assert_equal '088408851633', ean.value
35
+ assert_equal '0884088516338', ean.encoded_string
36
+ assert_equal '08', ean.number_system
37
+ assert_equal '84088', ean.manufacturers_code
38
+ assert_equal '51633', ean.product_code
39
+ end
40
+
41
+ def test_value_fixup
42
+ ean = Barcode1DTools::EAN13.new('088408851633', :checksum_included => false)
43
+ assert_equal 8, ean.check_digit
44
+ assert_equal '088408851633', ean.value
45
+ assert_equal '0884088516338', ean.encoded_string
46
+ end
47
+
48
+ def test_checksum_error
49
+ # proper checksum is 8
50
+ assert_raise(Barcode1DTools::ChecksumError) { Barcode1DTools::EAN13.new('0884088516331', :checksum_included => true) }
51
+ end
52
+
53
+ def test_value_length_errors
54
+ # One digit too short
55
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('01234567890', :checksum_included => false) }
56
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('012345678901', :checksum_included => true) }
57
+ # One digit too long
58
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('0123456789012', :checksum_included => false) }
59
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('01234567890123', :checksum_included => true) }
60
+ end
61
+
62
+ def test_bad_character_errors
63
+ # Characters that cannot be encoded
64
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.new('thisisnotgood', :checksum_included => false) }
65
+ end
66
+
67
+ def test_width
68
+ ean = Barcode1DTools::EAN13.new('0041343005796', :checksum_included => true)
69
+ assert_equal 95, ean.width
70
+ end
71
+
72
+ def test_barcode_generation
73
+ ean = Barcode1DTools::EAN13.new('0012676510226', :checksum_included => true)
74
+ assert_equal "10100011010011001001001101011110111011010111101010100111011001101110010110110011011001010000101", ean.bars
75
+ assert_equal "11132112221212211141312111411111123122213211212221221114111", ean.rle
76
+ end
77
+
78
+ def test_wn_raises_error
79
+ ean = Barcode1DTools::EAN13.new('0012676510226', :checksum_included => true)
80
+ assert_raise(Barcode1DTools::NotImplementedError) { ean.wn }
81
+ end
82
+
83
+ def test_decode_error
84
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('x') }
85
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('x'*60) }
86
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('x'*96) }
87
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('x'*94) }
88
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN13.decode('111000011111000011111') }
89
+ end
90
+
91
+ def test_decoding
92
+ random_ean_num=random_12_digit_number
93
+ ean = Barcode1DTools::EAN13.new(random_ean_num)
94
+ ean2 = Barcode1DTools::EAN13.decode(ean.bars)
95
+ assert_equal ean.value, ean2.value
96
+ ean3 = Barcode1DTools::EAN13.decode(ean.rle)
97
+ assert_equal ean.value, ean3.value
98
+ # Should also work in reverse
99
+ ean4 = Barcode1DTools::EAN13.decode(ean.bars.reverse)
100
+ assert_equal ean.value, ean4.value
101
+ end
102
+ end
@@ -0,0 +1,95 @@
1
+ #--
2
+ # Copyright 2012 Michael Chaney Consulting Corporation
3
+ #
4
+ # Released under the terms of the MIT License or the GNU
5
+ # General Public License, v. 2
6
+ #++
7
+
8
+ require 'test/unit'
9
+ require 'barcode1dtools'
10
+
11
+ class Barcode1DToolsEAN8Test < Test::Unit::TestCase
12
+ def setup
13
+ end
14
+
15
+ def teardown
16
+ end
17
+
18
+ # Creates a random EAN-8 sans checksum
19
+ def random_7_digit_number
20
+ sprintf('%07d',rand(10000000))
21
+ end
22
+
23
+ def test_checksum_generation
24
+ assert_equal 4, Barcode1DTools::EAN8.generate_check_digit_for('9638507')
25
+ end
26
+
27
+ def test_checksum_validation
28
+ assert Barcode1DTools::EAN8.validate_check_digit_for('96385074')
29
+ end
30
+
31
+ def test_attr_readers
32
+ ean = Barcode1DTools::EAN8.new('9638507', :checksum_included => false)
33
+ assert_equal 4, ean.check_digit
34
+ assert_equal '9638507', ean.value
35
+ assert_equal '96385074', ean.encoded_string
36
+ assert_equal '963', ean.number_system
37
+ assert_equal '8507', ean.product_code
38
+ end
39
+
40
+ def test_checksum_error
41
+ # proper checksum is 4
42
+ assert_raise(Barcode1DTools::ChecksumError) { Barcode1DTools::EAN8.new('96385070', :checksum_included => true) }
43
+ end
44
+
45
+ def test_value_length_errors
46
+ # One digit too short
47
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('123456', :checksum_included => false) }
48
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('1234567', :checksum_included => true) }
49
+ # One digit too long
50
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('12345678', :checksum_included => false) }
51
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('123456789', :checksum_included => true) }
52
+ end
53
+
54
+ def test_bad_character_errors
55
+ # Characters that cannot be encoded
56
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.new('thisisnotgood', :checksum_included => false) }
57
+ end
58
+
59
+ def test_width
60
+ ean = Barcode1DTools::EAN8.new(random_7_digit_number)
61
+ assert_equal 67, ean.width
62
+ end
63
+
64
+ def test_barcode_generation
65
+ ean = Barcode1DTools::EAN8.new('96385074', :checksum_included => true)
66
+ assert_equal "1010001011010111101111010110111010101001110111001010001001011100101", ean.bars
67
+
68
+ assert_equal "1113112111414111213111111231321113121132111", ean.rle
69
+ end
70
+
71
+ def test_wn_raises_error
72
+ ean = Barcode1DTools::EAN8.new(random_7_digit_number)
73
+ assert_raise(Barcode1DTools::NotImplementedError) { ean.wn }
74
+ end
75
+
76
+ def test_decode_error
77
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('x') }
78
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('x'*60) }
79
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('x'*96) }
80
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('x'*94) }
81
+ assert_raise(Barcode1DTools::UnencodableCharactersError) { Barcode1DTools::EAN8.decode('111000011111000011111') }
82
+ end
83
+
84
+ def test_decoding
85
+ random_ean_num=random_7_digit_number
86
+ ean = Barcode1DTools::EAN8.new(random_ean_num)
87
+ ean2 = Barcode1DTools::EAN8.decode(ean.bars)
88
+ assert_equal ean.value, ean2.value
89
+ ean3 = Barcode1DTools::EAN8.decode(ean.rle)
90
+ assert_equal ean.value, ean3.value
91
+ # Should also work in reverse
92
+ ean4 = Barcode1DTools::EAN8.decode(ean.bars.reverse)
93
+ assert_equal ean.value, ean4.value
94
+ end
95
+ end