barcode1dtools 0.9.2

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,252 @@
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/ean13'
9
+
10
+ module Barcode1DTools
11
+
12
+ # Barcode1DTools::EAN_8 - Create pattern for EAN-8 barcodes
13
+
14
+ # The value encoded is a 7-digit number, and a checksum digit will
15
+ # be added. You can add the option # :checksum_included => true
16
+ # when initializing to specify that you have already included a
17
+ # checksum.
18
+ #
19
+ # num = '96385074'
20
+ # bc = Barcode1DTools::EAN8.new(num)
21
+ # pattern = bc.bars
22
+ # rle_pattern = bc.rle
23
+ # width = bc.width
24
+ # check_digit = Barcode1DTools::EAN83.generate_check_digit_for(num)
25
+ #
26
+ # The object created is immutable.
27
+ #
28
+ # There are two formats for the returned pattern (wn format is
29
+ # not available):
30
+ #
31
+ # bars - 1s and 0s specifying black lines and white spaces. Actual
32
+ # characters can be changed from "1" and 0" with options
33
+ # :line_character and :space_character. Each character
34
+ # in the string renders to a single unit width.
35
+ #
36
+ # rle - Run-length-encoded version of the pattern. The first
37
+ # number is always a black line, with subsequent digits
38
+ # alternating between spaces and lines. The digits specify
39
+ # the width of each line or space.
40
+ #
41
+ # The "width" method will tell you the total end-to-end width, in
42
+ # units, of the entire barcode.
43
+ #
44
+ # Unlike some of the other barcodes, e.g. Code 3 of 9, there is no "wnstr" for
45
+ # EAN & UPC style barcodes because the bars and spaces are variable width from
46
+ # 1 to 4 units.
47
+ #
48
+ # A EAN-8 barcode has 3 elements:
49
+ # 1. A 2 or 3 digit "number system" designation
50
+ # 2. A 4 or 5 digit manufacturer's code
51
+ # 3. A single digit checksum
52
+ #
53
+ # Note than an EAN-8 is not analogous to a UPC-E. In particular, there
54
+ # is no way to create an EAN-13 from and EAN-8 and vice versa. The
55
+ # numbers are assigned within each system by a central authority.
56
+ #
57
+ # The bar patterns are the same as EAN-13, with nothing encoded in the
58
+ # parity. All bars on the left use the "odd" parity set.
59
+ #
60
+ # RENDERING
61
+ #
62
+ # When rendered, two sets of four digits are shown at the bottom of the
63
+ # code, aligned with the bottom of the code, and with the middle guard
64
+ # pattern bars extending down between them. A supplemental 2 or 5 may
65
+ # be used.
66
+
67
+ class EAN8 < Barcode1D
68
+
69
+ # patterns to create the bar codes:
70
+ LEFT_PATTERNS = EAN13::LEFT_PATTERNS
71
+ LEFT_PATTERNS_RLE = EAN13::LEFT_PATTERNS_RLE
72
+ RIGHT_PATTERNS = EAN13::RIGHT_PATTERNS
73
+ RIGHT_PATTERNS_RLE = EAN13::RIGHT_PATTERNS_RLE
74
+
75
+ SIDE_GUARD_PATTERN=EAN13::SIDE_GUARD_PATTERN
76
+ MIDDLE_GUARD_PATTERN=EAN13::MIDDLE_GUARD_PATTERN
77
+ SIDE_GUARD_PATTERN_RLE=EAN13::SIDE_GUARD_PATTERN_RLE
78
+ MIDDLE_GUARD_PATTERN_RLE=EAN13::MIDDLE_GUARD_PATTERN_RLE
79
+
80
+ DEFAULT_OPTIONS = {
81
+ :line_character => '1',
82
+ :space_character => '0'
83
+ }
84
+
85
+ # Specific for EAN
86
+ attr_reader :number_system
87
+ attr_reader :product_code
88
+
89
+ class << self
90
+ # returns true or false - must be 7-8 digits
91
+ def can_encode?(value, options = nil)
92
+ if !options
93
+ value.to_s =~ /^\d{7,8}$/
94
+ elsif (options[:checksum_included])
95
+ value.to_s =~ /^\d{8}$/
96
+ else
97
+ value.to_s =~ /^\d{7}$/
98
+ end
99
+ end
100
+
101
+ # Generates check digit given a string to encode. It assumes there
102
+ # is no check digit on the "value".
103
+ def generate_check_digit_for(value)
104
+ raise UnencodableCharactersError unless self.can_encode?(value, :checksum_included => false)
105
+ mult = 1
106
+ value = value.split('').inject(0) { |a,c| mult = 4 - mult ; a + c.to_i * mult }
107
+ (10 - (value % 10)) % 10
108
+ end
109
+
110
+ # validates the check digit given a string - assumes check digit
111
+ # is last digit of string.
112
+ def validate_check_digit_for(value)
113
+ raise UnencodableCharactersError unless self.can_encode?(value, :checksum_included => true)
114
+ md = value.match(/^(\d{7})(\d)$/)
115
+ self.generate_check_digit_for(md[1]) == md[2].to_i
116
+ end
117
+
118
+ # Decode a string representing an rle or bar pattern EAN-13.
119
+ # Note that the string might be backward or forward. This
120
+ # will return an EAN8 object.
121
+ def decode(str)
122
+ if str.length == 67
123
+ # bar pattern
124
+ str = bars_to_rle(str)
125
+ elsif str.length == 43
126
+ # rle
127
+ else
128
+ raise UnencodableCharactersError, "Pattern must be 67 unit bar pattern or 43 character rle."
129
+ end
130
+
131
+ # Check the guard patterns
132
+ unless str[0..2] == SIDE_GUARD_PATTERN_RLE && str[40..42] == SIDE_GUARD_PATTERN_RLE && str[19..23] == MIDDLE_GUARD_PATTERN_RLE
133
+ raise UnencodableCharactersError, "Missing or incorrect guard patterns"
134
+ end
135
+
136
+ # Now I have an rle pattern, simply need to decode
137
+ # according to the LEFT_PATTERNS_RLE, keeping track
138
+ # of the parity for each position.
139
+
140
+ # Set up the decoder
141
+ left_parity_sequence = ''
142
+ left_digits = ''
143
+ right_parity_sequence = ''
144
+ right_digits = ''
145
+ left_initial_offset = SIDE_GUARD_PATTERN_RLE.length
146
+ right_initial_offset = SIDE_GUARD_PATTERN_RLE.length + (4*4) + MIDDLE_GUARD_PATTERN_RLE.length
147
+
148
+ # Decode the left side
149
+ (0..3).each do |left_offset|
150
+ found = false
151
+ digit_rle = str[(left_initial_offset + left_offset*4),4]
152
+ ['o','e'].each do |parity|
153
+ ('0'..'9').each do |digit|
154
+ if LEFT_PATTERNS_RLE[digit][parity] == digit_rle
155
+ left_parity_sequence += parity
156
+ left_digits += digit
157
+ found = true
158
+ break
159
+ end
160
+ end
161
+ end
162
+ raise UndecodableCharactersError, "Invalid sequence: #{digit_rle}" unless found
163
+ end
164
+
165
+ # Decode the right side
166
+ (0..3).each do |right_offset|
167
+ found = false
168
+ digit_rle = str[(right_initial_offset + right_offset*4),4]
169
+ ['o','e'].each do |parity|
170
+ ('0'..'9').each do |digit|
171
+ if LEFT_PATTERNS_RLE[digit][parity] == digit_rle
172
+ right_parity_sequence += parity
173
+ right_digits += digit
174
+ found = true
175
+ break
176
+ end
177
+ end
178
+ end
179
+ raise UndecodableCharactersError, "Invalid sequence: #{digit_rle}" unless found
180
+ end
181
+
182
+ # If left parity sequence is 'eeee', the string is reversed
183
+ if left_parity_sequence == 'eeee'
184
+ left_digits, right_digits, left_parity_sequence = right_digits.reverse, left_digits.reverse, right_parity_sequence.reverse.tr('eo','oe')
185
+ end
186
+
187
+ # Debugging
188
+ #puts "Left digits: #{left_digits} Left parity: #{left_parity_sequence}"
189
+ #puts "Right digits: #{right_digits} Right parity: #{right_parity_sequence}"
190
+
191
+ EAN8.new(left_digits + right_digits, :checksum_included => true)
192
+ end
193
+ end
194
+
195
+ # Options are :line_character, :space_character, and
196
+ # :checksum_included.
197
+ def initialize(value, options = {})
198
+
199
+ @options = DEFAULT_OPTIONS.merge(options)
200
+
201
+ # Can we encode this value?
202
+ raise UnencodableCharactersError unless self.class.can_encode?(value, @options)
203
+
204
+ if @options[:checksum_included]
205
+ @encoded_string = sprintf('%08d', value.to_i)
206
+ raise ChecksumError unless self.class.validate_check_digit_for(@encoded_string)
207
+ md = @encoded_string.match(/^(\d+?)(\d)$/)
208
+ @value, @check_digit = md[1], md[2].to_i
209
+ else
210
+ # need to add a checksum
211
+ @value = sprintf('%07d', value.to_i)
212
+ @check_digit = self.class.generate_check_digit_for(@value)
213
+ @encoded_string = "#{@value}#{@check_digit}"
214
+ end
215
+
216
+ md = @value.match(/^(\d{3})(\d{4})/)
217
+ @number_system, @product_code = md[1], md[2]
218
+ end
219
+
220
+ # not usable with EAN codes
221
+ def wn
222
+ raise NotImplementedError
223
+ end
224
+
225
+ # returns a run-length-encoded string representation
226
+ def rle
227
+ if @rle
228
+ @rle
229
+ else
230
+ md = @encoded_string.match(/^(\d{4})(\d{4})/)
231
+ @rle = gen_rle(md[1], md[2])
232
+ end
233
+ end
234
+
235
+ # returns 1s and 0s (for "black" and "white")
236
+ def bars
237
+ @bars ||= self.class.rle_to_bars(self.rle, @options)
238
+ end
239
+
240
+ # returns the total unit width of the bar code
241
+ def width
242
+ @width ||= rle.split('').inject(0) { |a,c| a + c.to_i }
243
+ end
244
+
245
+ private
246
+
247
+ def gen_rle(left_half, right_half)
248
+ (SIDE_GUARD_PATTERN_RLE + (0..3).collect { |n| LEFT_PATTERNS_RLE[left_half[n,1]]['o'] }.join + MIDDLE_GUARD_PATTERN_RLE + right_half.split('').collect { |c| RIGHT_PATTERNS_RLE[c] }.join + SIDE_GUARD_PATTERN_RLE)
249
+ end
250
+
251
+ end
252
+ end
@@ -0,0 +1,248 @@
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
+ module Barcode1DTools
9
+
10
+ # Barcode1DTools::Interleaved2of5 - Create pattern for Interleaved 2 of 5
11
+ # (also known as I 2/5 or ITF) barcodes. The value encoded is an
12
+ # integer, and a checksum digit will be added. You can add the option
13
+ # :checksum_included => true when initializing to specify that you
14
+ # have already included a checksum, or :skip_checksum => true to
15
+ # specify that no checksum should be added or checked. A ChecksumError
16
+ # will be raised if :checksum_included => true, :skip_checksum is
17
+ # missing or false, and the last digit is invalid as a checksum.
18
+ #
19
+ # I 2/5 can only encode an even number of digits (including the
20
+ # checksum), so a "0" will be prepended if there is an odd number of
21
+ # digits. The 0 has no effect on the checksum. Note that sometimes
22
+ # an odd number of digits is encoded with 5 narrow spaces for the last
23
+ # digit. We do not encode this way but can handle decoding.
24
+ #
25
+ # num = 238982
26
+ # bc = Barcode1DTools::Interleaved2of5.new(num)
27
+ # pattern = bc.bars
28
+ # rle_pattern = bc.rle
29
+ # wn_pattern = bc.wn
30
+ # width = bc.width
31
+ # check_digit = Barcode1DTools::Interleaved2of5.generate_check_digit_for(num)
32
+ #
33
+ # The object created is immutable.
34
+ #
35
+ # Barcode1DTools::Interleaved2of5 creates the patterns that you need to
36
+ # display Interleaved 2 of 5 (also known as I 2/5) barcodes. It can also
37
+ # decode a simple w/n string.
38
+ #
39
+ # I 2/5 barcodes consist of lines and spaces that are either "wide" or
40
+ # "narrow", with "wide" lines or spaces being twice the width of
41
+ # narrow lines or spaces.
42
+ #
43
+ # There are three formats for the returned pattern:
44
+ #
45
+ # bars - 1s and 0s specifying black lines and white spaces. Actual
46
+ # characters can be changed from "1" and 0" with options
47
+ # :line_character and :space_character.
48
+ #
49
+ # rle - Run-length-encoded version of the pattern. The first
50
+ # number is always a black line, with subsequent digits
51
+ # alternating between spaces and lines. The digits specify
52
+ # the width of each line or space.
53
+ #
54
+ # wn - The native format for this barcode type. The string
55
+ # consists of a series of "w" and "n" characters. The first
56
+ # item is always a black line, with subsequent characters
57
+ # alternating between spaces and lines. A "wide" item
58
+ # is twice the width of a "narrow" item.
59
+ #
60
+ # The "width" method will tell you the total end-to-end width, in
61
+ # units, of the entire barcode.
62
+ #
63
+ # In this encoding, pairs of digits are interleaved with each other,
64
+ # so the first digit is the "bars" and the second digit is the
65
+ # "spaces". Each digit consists of 5 sets of wide or narrow bars or
66
+ # spaces, with 2 of the 5 being wide. The pattern can be calculated
67
+ # by considering a "weight" for each position: 1, 2, 4, 7, and 0, with
68
+ # "0" itself being represented by 4 + 7. So, 3 is 1 + 2, or "wwnnn",
69
+ # while 7 is "nnnww" (7 + 0). More information is available on
70
+ # Wikipedia.
71
+
72
+ class Interleaved2of5 < Barcode1D
73
+
74
+ # patterns and such go here
75
+ PATTERNS = {
76
+ 'start' => 'nnnn',
77
+ '0' => 'nnwwn',
78
+ '1' => 'wnnnw',
79
+ '2' => 'nwnnw',
80
+ '3' => 'wwnnn',
81
+ '4' => 'nnwnw',
82
+ '5' => 'wnwnn',
83
+ '6' => 'nwwnn',
84
+ '7' => 'nnnww',
85
+ '8' => 'wnnwn',
86
+ '9' => 'nwnwn',
87
+ 'stop' => 'wnn'
88
+ }
89
+
90
+ WN_RATIO = 2
91
+
92
+ DEFAULT_OPTIONS = {
93
+ :w_character => 'w',
94
+ :n_character => 'n',
95
+ :line_character => '1',
96
+ :space_character => '0',
97
+ :wn_ratio => WN_RATIO
98
+ }
99
+
100
+ class << self
101
+ # returns true or false
102
+ def can_encode?(value)
103
+ value.is_a?(Integer) || value.to_s =~ /^[0-9]+$/
104
+ end
105
+
106
+ # Generates check digit given a string to encode. It assumes there
107
+ # is no check digit on the "value". Note that if value has an even
108
+ # number of digits, a "0" will be prepended for this operation.
109
+ def generate_check_digit_for(value)
110
+ raise UnencodableCharactersError unless self.can_encode?(value)
111
+ mult = 1
112
+ value = value.to_s
113
+ value = "0#{value}" if value.size.even?
114
+ value = value.split('').inject(0) { |a,c| mult = 4 - mult ; a + c.to_i * mult }
115
+ (10 - (value % 10)) % 10
116
+ end
117
+
118
+ # validates the check digit given a string - assumes check digit
119
+ # is last digit of string.
120
+ def validate_check_digit_for(value)
121
+ raise UnencodableCharactersError unless self.can_encode?(value)
122
+ md = value.to_s.match(/^(\d+)(\d)$/)
123
+ self.generate_check_digit_for(md[1]) == md[2].to_i
124
+ end
125
+
126
+ # Decode a string in wn format. This will return an Interleaved2of5
127
+ # object.
128
+ def decode(str, options = {})
129
+ if str =~ /[^wn]/
130
+ raise UnencodableCharactersError, "Pattern must contain only \"w\" and \"n\"."
131
+ end
132
+
133
+ if str.reverse =~ /^#{PATTERNS['start']}.*?#{PATTERNS['stop']}$/
134
+ str.reverse!
135
+ end
136
+
137
+ unless str =~ /^#{PATTERNS['start']}(.*?)#{PATTERNS['stop']}$/
138
+ raise UnencodableCharactersError, "Start/stop pattern is not detected."
139
+ end
140
+
141
+ numeric_pattern = $1
142
+
143
+ unless numeric_pattern.size % 10 == 0
144
+ raise UnencodableCharactersError, "Wrong number of bars."
145
+ end
146
+
147
+ decoded_string = ''
148
+
149
+ numeric_pattern.scan(/(.{10})/).each do |chunk|
150
+ chunk = chunk[0]
151
+
152
+ num1 = chunk[0,1] + chunk[2,1] + chunk[4,1] + chunk[6,1] + chunk[8,1]
153
+ num2 = chunk[1,1] + chunk[3,1] + chunk[5,1] + chunk[7,1] + chunk[9,1]
154
+
155
+ found = false
156
+ ('0'..'9').each do |digit|
157
+ if PATTERNS[digit] == num1
158
+ decoded_string += digit
159
+ found = true
160
+ end
161
+ end
162
+
163
+ raise UndecodableCharactersError, "Invalid sequence: #{num1}" unless found
164
+
165
+ # nnnnn is a sequence sometimes used in the spaces of the last
166
+ # digit to indicate there is no last digit.
167
+ if num2 != 'nnnnn'
168
+ found = false
169
+ ('0'..'9').each do |digit|
170
+ if PATTERNS[digit] == num2
171
+ decoded_string += digit
172
+ found = true
173
+ end
174
+ end
175
+
176
+ raise UndecodableCharactersError, "Invalid sequence: #{num2}" unless found
177
+ end
178
+ end
179
+
180
+ Interleaved2of5.new(decoded_string.to_i, options)
181
+ end
182
+ end
183
+
184
+ # Options are :line_character, :space_character, :w_character,
185
+ # :n_character, :skip_checksum, and :checksum_included.
186
+ def initialize(value, options = {})
187
+
188
+ @options = DEFAULT_OPTIONS.merge(options)
189
+
190
+ # Can we encode this value?
191
+ raise UnencodableCharactersError unless self.class.can_encode?(value)
192
+
193
+ if @options[:skip_checksum]
194
+ @encoded_string = value.to_s
195
+ @value = value.to_i
196
+ @check_digit = nil
197
+ elsif @options[:checksum_included]
198
+ raise ChecksumError unless self.class.validate_check_digit_for(value)
199
+ @encoded_string = value.to_s
200
+ @value = value.to_i / 10
201
+ @check_digit = value % 10
202
+ else
203
+ # need to add a checksum
204
+ @value = value.to_i
205
+ @check_digit = self.class.generate_check_digit_for(@value)
206
+ @encoded_string = "#{@value.to_s}#{@check_digit}"
207
+ end
208
+
209
+ @encoded_string = '0' + @encoded_string if @encoded_string.size.odd?
210
+ end
211
+
212
+ # Returns a string of "w" or "n" ("wide" and "narrow")
213
+ def wn
214
+ @wn ||= wn_str.tr('wn', @options[:w_character].to_s + @options[:n_character].to_s)
215
+ end
216
+
217
+ # returns a run-length-encoded string representation
218
+ def rle
219
+ @rle ||= self.class.wn_to_rle(self.wn, @options)
220
+ end
221
+
222
+ # returns 1s and 0s (for "black" and "white")
223
+ def bars
224
+ @bars ||= self.class.rle_to_bars(self.rle, @options)
225
+ end
226
+
227
+ # returns the total unit width of the bar code
228
+ def width
229
+ @width ||= rle.split('').inject(0) { |a,c| a + c.to_i }
230
+ end
231
+
232
+ private
233
+
234
+ def wn_str
235
+ @wn_str ||=
236
+ PATTERNS['start'] +
237
+ @encoded_string.unpack('A2'*(@encoded_string.size/2)).inject('') { |a,str| a + interleave(str) } +
238
+ PATTERNS['stop']
239
+ end
240
+
241
+ # Requires a two-digit string
242
+ def interleave(two_digits)
243
+ bars_pattern = PATTERNS[two_digits[0,1]]
244
+ spaces_pattern = PATTERNS[two_digits[1,1]]
245
+ (0..4).inject('') { |ret,x| ret + bars_pattern[x,1] + spaces_pattern[x,1] }
246
+ end
247
+ end
248
+ end