[Ruby] Generate a concatenated QR code with rqrcode (Practice)

--As I wrote in the knowledge section, this time I will generate a concatenated QR code in the 8bit_byte mode used in the QR code of the prescription. --The actual data to be written in the prescription is a CSV data file with Shift JIS encoding. (Line break = CR / LF / EOF available) (Defined at here)

Modify rqrcode

About the library structure of rqrcode

Add a method to write the header part of the concatenated QR code

rqrcode_core/qrcode/qr_bit_buffer.rb


module RQRCodeCore
  class QRBitBuffer
    #Omission
    def byte_with_connected_encoding_start(length, page_number, last_page_number, parity)
      put(3, 4) #MODE indicating the concatenated QR code= 0b0011
      put(page_number, 4) #Sequence number(0〜15)
      put(last_page_number, 4) #Last sequence number(0〜15)
      put(parity, 8) #Parity value(XOR value of all data)
      put(QRMODE[:mode_8bit_byte], 4) # 8bit_byte mode value
      put(length, QRUtil.get_length_in_bits(QRMODE[:mode_8bit_byte], @version)) #Data length
    end

Create a class for concatenated QR code

rqrcode_core/qrcode/qr_8bit_byte_with_connected.rb


# frozen_string_literal: true

module RQRCodeCore
  class QR8BitByteWithConnected
    attr_reader :mode, :page_number, :last_page_number, :parity

    def initialize(data, page_number, last_page_number, parity)
      @mode = QRMODE[:mode_8bit_byte]
      @data = data
      @page_number = page_number
      @last_page_number = last_page_number
      @parity = parity
    end

    def get_length
      @data.bytesize
    end

    def write(buffer)
      buffer.byte_with_connected_encoding_start(get_length, @page_number, @last_page_number, @parity)
      @data.each_byte do |b|
        buffer.put(b, 8)
      end
    end
  end
end

Add concatenated QR code option to QRCode class

rqrcode_core/qrcode/qr_code.rb


module RQRCodeCore
  #Omission
  class QRCode
    attr_reader :modules, :module_count, :version
    #Omission
    def initialize( string, *args )
      if !string.is_a? String
        raise QRCodeArgumentError, "The passed data is #{string.class}, not String"
      end

      options               = extract_options!(args)
      level                 = (options[:level] || :h).to_sym
      #Added option judgment for concatenated QR code
      with_connected = options[:connected] || false

      #Omission

      # :byte_Error if a concatenated QR code is specified other than 8bit(Implementation is optional)
      if with_connected && mode != QRMODE_NAME[:byte_8bit]
        raise QRCodeArgumentError, 'Argument error.(Connected QRCode is byte_8bit mode only)'
      end

      max_size_array        = QRMAXDIGITS[level][mode]
      size                  = options[:size] || smallest_size_for(string, max_size_array)

      if size > QRUtil.max_size
        raise QRCodeArgumentError, "Given size greater than maximum possible size of #{QRUtil.max_size}"
      end

      #Omission
      @data_list =
        case mode
        when :mode_number
          QRNumeric.new( @data )
        when :mode_alpha_numk
          QRAlphanumeric.new( @data )
        else
          #If it is a concatenated QR code, change the generated class
          if with_connected
            page_number = options[:page_number]
            last_page_number = options[:last_page_number]
            parity = options[:parity]
            QR8BitByteWithConnected.new(@data, page_number, last_page_number, parity)
          else
            QR8bitByte.new(@data)
          end
        end

      @data_cache           = nil
      self.make
    end

Output the concatenated QR code

JAHIS7
1,1,1234567,13,Kita Internal Medicine Clinic
2,999-9999,9F, 99-999 Test Building, Kitashinagawa, Shinagawa-ku, Tokyo
3,03-9999-9991,03-9999-9992,
4,1,,Internal medicine
5,,,Physician Taro
11,,Patient Hanako,Kanja Hanako
12,2
13,20000103
21,1
22,01010016
23,symbol,number,2,
51,20200818
81,1,,
81,2,,This is a remarks column for prescriptions.
101,1,3,,1
111,1,1,,On the feet,
201,1,1,1,7,2619803XAZZZ,[General] External liquid containing alcohol for disinfection,4,1,mL
101,2,1,,5
111,2,1,,Once a day after dinner,
201,2,1,1,7,1124022F2ZZZ,[General] Lorazepam Tablets 1 mg,1,1,Lock
101,3,1,,28
111,3,1,,3 times a day after every meal,
201,3,1,1,7,3136004F2ZZZ,[General] Mecobalamin Tablets 0.5 mg,3,1,Lock
201,3,2,1,7,2171017F2ZZZ,[General] Nicorandil Tablets 5 mg,3,1,Lock
201,3,3,1,7,3962001F1ZZZ,[General] Buformin Hydrochloride Tablets 50 mg,3,1,Lock
201,3,4,1,7,1124022F1ZZZ,[General] Lorazepam Tablets 0.5 mg,3,1,Lock
201,3,5,1,7,1124026F1ZZZ,[General] Tofisopam Tablets 50 mg,3,1,Lock
201,3,6,1,2,612140503,,3,1,Lock
101,4,1,,28
111,4,1,,3 times a day after every meal,
201,4,1,1,7,2344009F2ZZZ,[General] Magnesium oxide tablets 330 mg,6,1,Lock
101,5,3,,1
111,5,1,,On the feet,
201,5,1,1,7,2619803XAZZZ,[General] External liquid containing alcohol for disinfection,4,1,mL
101,6,1,,28
111,6,1,,3 times a day after every meal,
201,6,1,1,7,3136004F2ZZZ,[General] Mecobalamin Tablets 0.5 mg,3,1,Lock
201,6,2,1,7,2171017F2ZZZ,[General] Nicorandil Tablets 5 mg,3,1,Lock
201,6,3,1,7,3962001F1ZZZ,[General] Buformin Hydrochloride Tablets 50 mg,3,1,Lock
201,6,4,1,7,1124022F1ZZZ,[General] Lorazepam Tablets 0.5 mg,3,1,Lock
201,6,5,1,7,1124026F1ZZZ,[General] Tofisopam Tablets 50 mg,3,1,Lock
201,6,6,1,2,612140503,,3,1,Lock

Method to calculate parity

    def create_total_text_parity(data)
      data.each_byte.inject(0) { |parity, b| parity ^ b }
    end

Method to split data

    #Add a require for RQRCodeCore if needed.
    LIMIT_OF_CONNECTED_QRCODE_LENGTH = 16
    CAPACITY_GAP_FOR_CONNECTED = 2

    def slice_data_for_connected_qrcode(data, options)
      capacity = binary_qrcode_capacity_from(options[:level], options[:size]) - CAPACITY_GAP_FOR_CONNECTED

      sliced =
        if options[:adjust_for_sjis]
          slice_sjis_data(data, capacity: capacity)
        else
          slice_data(data, capacity: capacity)
        end

      fail 'data for connect QRCode too many length.' if sliced.length > LIMIT_OF_CONNECTED_QRCODE_LENGTH
      sliced
    end

    def binary_qrcode_capacity_from(level, size)
      RQRCodeCore::QRMAXDIGITS[level][:mode_8bit_byte][size - 1]
    end

    def slice_sjis_data(sjis_data, capacity:)
      is_first_char = ->(char) { (char >= 129 && char <= 159) || (char >= 224 && char <= 239) }

      bytes = sjis_data.each_byte
      sliced_list = []
      while bytes.present?
        next_bytes = bytes.take(capacity)
        if is_first_char.call(next_bytes.last) && next_bytes.length != bytes
          next_bytes = bytes.take(capacity - 1)
        end

        sliced_list << next_bytes.pack('c*')
        bytes = bytes.drop(next_bytes.length)
      end

      sliced_list
    end

    def slice_data(data, capacity:)
      data.each_byte.each_slice(capacity).map { |sliced| sliced.pack('c*') }
    end

Method to automatically divide and generate QR code

    # Generate connected QRCode. (binary mode only)
    #
    #   # data   - the string you wish to encode
    #   # args
    #   #   size   - the size of the qrcode (default 4)
    #   #   level  - the error correction level
    #   #   adjust_for_sjis - true: no split sjis multi byte character
    #
    # qrcode_list = RQRCode::ConnectedQRCodeUtil.generate_binary_connected_qrcodes('hello world', size: 1, level: :m)
    def generate_binary_connected_qrcodes(data, *args)
      options = extract_options!(args)
      #If it fits in one QR code, it will be generated with a normal QR code.
      return [RQRCode::QRCode.new(data, options)] if fits_in_single_qrcode?(data, options)

      #Split data
      sliced_text = slice_data_for_connected_qrcode(data, options)
      #Generate parity
      parity = create_total_text_parity(data)
      #Generate a QR code based on the divided data
      sliced_text.map.with_index do |text, number|
        extend_args = { page_number: number, last_page_number: sliced_text.length - 1, connected: true, parity: parity }
        RQRCode::QRCode.new(text, options.merge(extend_args))
      end
    end

    def extract_options!(arr)
      arr.last.is_a?(::Hash) ? arr.pop : {}
    end

    def fits_in_single_qrcode?(data, options)
      data.each_byte.to_a.length <= binary_qrcode_capacity_from(options[:level], options[:size])
    end

Actual output


  QRCODE_SIZE = 16 # Size of QRCODE
  QRCODE_LEVEL = :l # Error correction level (L = 7%)
  MODULE_PIXEL_SIZE = 2 # Size of Pixel

  def create_qrcode_png_files_from(csv_data)
    qrcodes = RQRCode::ConnectedQRCodeUtil.generate_binary_connected_qrcodes(
      csv_data,
      size: QRCODE_SIZE,
      level: QRCODE_LEVEL,
      adjust_for_sjis: true
    )
    qrcodes.map.with_index do |qrcode, index|
      file_path = File.join('tmp', "qrcode_#{index}.png ")
      qrcode.as_png(module_px_size: MODULE_PIXEL_SIZE, file: file_path)
    end
  end

Summary

References

This article was written with reference to the following information.

Change log

Recommended Posts

[Ruby] Generate a concatenated QR code with rqrcode (Practice)
[Ruby] Generate a concatenated QR code with rqrcode (Knowledge)
Format Ruby with VS Code
Read and generate QR code [Android]
Make a typing game with ruby
Generate Dart client code with Rails + apipie
Let's make a smart home with Ruby!
I made a risky die with Ruby
Extract a part of a string with Ruby
A memorandum to clean up the code Ruby
Creating a browser automation tool with Ruby + Selenium
I built a Code Pipeline with AWS CDK.
[Ruby] How to generate a random alphabet string
Try debugging a Java program with VS Code
I made a portfolio with Ruby On Rails
Build a Java development environment with VS Code