# coding: utf-8 require 'usps_intelligent_barcode/codeword_map' require 'usps_intelligent_barcode/crc' # The namespace for everything in this library. module Imb # This class represents a barcode. class Barcode include Memoizer # @return [BarcodeId] attr_reader :barcode_id # @return [ServiceType] attr_reader :service_type # @return [MailerId] attr_reader :mailer_id # @return [SerialNumber] attr_reader :serial_number # @return [RoutingCode] attr_reader :routing_code # @param # Create a new barcode # # @param barcode_id [String] Nominally a String, but can be # anything that {BarcodeId.coerce} will accept. # @param service_type [String] Nominally a String, but can be # anything that {ServiceType.coerce} will accept. # @param mailer_id [String] Nominally a String, but can be # anything that {MailerId.coerce} will accept. # @param serial_number [String] Nominally a String, but can be # anything that {SerialNumber.coerce} will accept. # @param routing_code [String] Nominally a String, but can be # anything that {RoutingCode.coerce} will accept. def initialize( barcode_id, service_type, mailer_id, serial_number, routing_code ) @barcode_id = BarcodeId.coerce(barcode_id) @service_type = ServiceType.coerce(service_type) @mailer_id = MailerId.coerce(mailer_id) @serial_number = SerialNumber.coerce(serial_number) @routing_code = RoutingCode.coerce(routing_code) validate_components end # Return a string to print using one of the USPS Intelligent Mail # Barcode fonts. Each character of the string will be one of: # * 'T' for a tracking mark (neither ascender nor descender) # * 'A' for an ascender mark # * 'D' for a descender mark # * 'F' for a full mark (both ascender and descender) # @return [String] A string that represents the barcode. def barcode_letters symbols.map(&:letter).join end private # :stopdoc: BAR_MAP = BarMap.new CODEWORD_MAP = CodewordMap.new CRC = Crc.new # :startdoc: def validate_components components.each do |component| component.validate(long_mailer_id?) end end def components [ @routing_code, @barcode_id, @service_type, @mailer_id, @serial_number, ] end def long_mailer_id? @mailer_id.long? end # The components ("fields" in the spec) are turned into a single # number that the spec calls "binary_data"). This is done through # a series of multiplications and additions. See spec. section # 2.2.1 ("Step 1--Conversion of Data Fields into Binary Data"). # # @return [Integer] def binary_data components.inject(0) do |data, component| component.shift_and_add_to(data, long_mailer_id?) end end memoize :binary_data # Compute the "frame check sequence." See spec. section 2.2.2 # ("Step 2--Generation of 11-Bit CRC on Binary Data"). # # @return [Integer] def frame_check_sequence CRC.crc(binary_data) end memoize :frame_check_sequence # Compute the "code words." This is an array of 10 integers # computed from the binary data. See spec. section 2.2.3 ("Step # 3--Conversion from Binary Data to Codewords"). # # @return [Array] 10 "characters." def codewords codewords = [] data = binary_data data, codewords[9] = data.divmod 636 8.downto(0) do |i| data, codewords[i] = data.divmod 1365 end codewords end memoize :codewords # Insert the orientation into the codewords. The spec. doesn't # say much about this, other than to multiply codeword "J" by two. # This will cause the LSB to be zero, which is presumably the # orientation. See spec. section 2.4.4 ("Step 4--Inserting # Additional Information into Codewords"). # # @return [Array] 10 "characters." def codewords_with_orientation_in_character_j result = codewords.dup result[9] *= 2 result end # Insert the most significant bit of the FCS into codeword A. See # spec. section 2.4.4 ("Step 4--Inserting Additional Information # into Codewords"). # # @return [Array] 10 "characters." def codewords_with_fcs_bit_in_character_a result = codewords_with_orientation_in_character_j.dup result[0] += 659 if frame_check_sequence[10] == 1 result end # Convert the codewords to "characters". Each character is a # 13-bit integer; there are 10 of them, labeled "A" through "J" by # the spec. See spec. section 2.2.5 ("Step 5--Conversion from # Codewords to Characters"), para. "A". # # @return [Array] 10 "characters." def characters CODEWORD_MAP.characters(codewords_with_fcs_bit_in_character_a) end # Fold the least-significant 10 bits of the FCS into the # "characters". See spec. section 2.2.5 ("Step 5--Conversion from # Codewords to Characters"), para. "B". # # @return [Array] 10 "characters". def characters_with_fcs_bits_0_through_9 characters.each_with_index.map do |character, i| if frame_check_sequence[i] == 1 character ^ 0b1111111111111 else character end end end # Map the "characters" to symbols. Here is where the barcode is made. # # @return [Array] def symbols BAR_MAP.symbols(characters_with_fcs_bits_0_through_9) end end end