barcode1dtools 0.9.2

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