LoginSignup
2
2

More than 3 years have passed since last update.

[Ruby] rqrcodeで連結QRコードを生成する(実践編)

Last updated at Posted at 2020-08-25
  • 知識編はこちら
  • では、実際に連結QRコードを出力してみましょう。

    • 知識編にも書きましたが、今回は処方箋のQRコードで使う 8bit_byte モードの連結QRコードを生成します。
    • 処方箋に書き込むデータの実態は ShiftJISエンコーディングのCSVデータファイルです。(改行=CR/LF・EOFあり)(こちらで定義されています)

rqrcode を改変する

rqrcode のライブラリ構成について

  • rqrcodeは、v1.0.0からライブラリの構成が分割され、生成機能の呼び出しや画像ファイルへの出力クラスを備えるrqrcode と QRコードの生成のcore部分のrqrcode_core の2つに分割されました。
  • 私が実際に分割QRコード出力を利用したプロジェクトでは、rqrcodeをForkしたりPullRequestを出さず、上記の2つのライブラリをSUBSETとして自分のプロジェクトコードに取り込みました。(RSpecでのテストなども自分で追加しました)
  • 実践する場合は、forkリポジトリを作るかSUBSETとして自分のプロジェクトに取り込んでください。

連結QRコードのヘッダ部分を書き込むメソッドを追加する

  • 先ず、ヘッダ部の書き込みを行う QRBitBuffer に、連結QRコード用のヘッダ書き込みのメソッドを追加します。
rqrcode_core/qrcode/qr_bit_buffer.rb
module RQRCodeCore
  class QRBitBuffer
    # 中略
    def byte_with_connected_encoding_start(length, page_number, last_page_number, parity)
      put(3, 4) # 連結QRコードを示すMODE = 0b0011
      put(page_number, 4) # 順序番号(0〜15)
      put(last_page_number, 4) # 最後の順序番号(0〜15)
      put(parity, 8) # パリティ値(全てのデータのXOR値)
      put(QRMODE[:mode_8bit_byte], 4) # 8bit_byte のモード値
      put(length, QRUtil.get_length_in_bits(QRMODE[:mode_8bit_byte], @version)) # データの長さ
    end

連結QRコード用のクラスを作る

  • 8byte_byte モード用に QR8bitByte クラスがありますが、これを連結QRコード用にした QR8BitByteWithConnected クラスを作成します。
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

QRCodeクラスに連結QRコードのoptionを追加する

  • QRコードを生成する QRCode クラスのoptionとして、連結QRコードを出力できる機能を追加します。
    • 尚、連結QRコードの場合、1つのQRコードに格納できるデータのCapacityが2byte短くなりますが、この点は考慮していません。(オリジナルのコードにサイズチェックも入っているので、そこに連結QRコードの場合の条件判定を追加すれば、簡単に実装出来ますね)
    • QRコードのバージョン(=大きさ)は、optionsize: xx というパラメタ名なので注意してください。
rqrcode_core/qrcode/qr_code.rb
module RQRCodeCore
  # 中略
  class QRCode
    attr_reader :modules, :module_count, :version
    # 中略
    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
      # 連結QRコードのoption判定を追加
      with_connected = options[:connected] || false

      # 中略

      # :byte_8bit 以外で連結QRコードを指定した場合にErrorとする(実装は任意です)
      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

      # 中略
      @data_list =
        case mode
        when :mode_number
          QRNumeric.new( @data )
        when :mode_alpha_numk
          QRAlphanumeric.new( @data )
        else
          # 連結QRコードだった場合に、生成するクラスを変更します
          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

連結QRコードを出力する

  • 今回はこのサンプルを、バージョン16の大きさで出力してみましょう。
    • 実際のデータは、ShiftJISエンコーディング(改行=CR/LF EOFあり)です。
JAHIS7
1,1,1234567,13,キータ内科クリニック
2,999-9999,東京都品川区北品川99ー999テストビル9F
3,03-9999-9991,03-9999-9992,
4,1,,内科
5,,,内科医 太郎
11,,患者 花子,カンジャ ハナコ
12,2
13,20000103
21,1
22,01010016
23,記号,番号,2,
51,20200818
81,1,,
81,2,,処方箋用の備考欄です。
101,1,3,,1
111,1,1,,足に,
201,1,1,1,7,2619803XAZZZ,【般】消毒用アルコール配合外用液,4,1,mL
101,2,1,,5
111,2,1,,1日1回夕食後に,
201,2,1,1,7,1124022F2ZZZ,【般】ロラゼパム錠1mg,1,1,錠
101,3,1,,28
111,3,1,,1日3回毎食後に,
201,3,1,1,7,3136004F2ZZZ,【般】メコバラミン錠0.5mg,3,1,錠
201,3,2,1,7,2171017F2ZZZ,【般】ニコランジル錠5mg,3,1,錠
201,3,3,1,7,3962001F1ZZZ,【般】ブホルミン塩酸塩錠50mg,3,1,錠
201,3,4,1,7,1124022F1ZZZ,【般】ロラゼパム錠0.5mg,3,1,錠
201,3,5,1,7,1124026F1ZZZ,【般】トフィソパム錠50mg,3,1,錠
201,3,6,1,2,612140503,,3,1,錠
101,4,1,,28
111,4,1,,1日3回毎食後に,
201,4,1,1,7,2344009F2ZZZ,【般】酸化マグネシウム錠330mg,6,1,錠
101,5,3,,1
111,5,1,,足に,
201,5,1,1,7,2619803XAZZZ,【般】消毒用アルコール配合外用液,4,1,mL
101,6,1,,28
111,6,1,,1日3回毎食後に,
201,6,1,1,7,3136004F2ZZZ,【般】メコバラミン錠0.5mg,3,1,錠
201,6,2,1,7,2171017F2ZZZ,【般】ニコランジル錠5mg,3,1,錠
201,6,3,1,7,3962001F1ZZZ,【般】ブホルミン塩酸塩錠50mg,3,1,錠
201,6,4,1,7,1124022F1ZZZ,【般】ロラゼパム錠0.5mg,3,1,錠
201,6,5,1,7,1124026F1ZZZ,【般】トフィソパム錠50mg,3,1,錠
201,6,6,1,2,612140503,,3,1,錠

パリティを計算するメソッド

  • 全てのデータのXOR値を算出します。
    def create_total_text_parity(data)
      data.each_byte.inject(0) { |parity, b| parity ^ b }
    end

データを分割するメソッド

  • slice_data_for_connected_qrcode メソッドを使って、データを必要数に分割します。
  • option で指定する内容は以下の通りです。
    • size = QRコードのバージョン(サイズ) -> QRCodeのoptionと同じ
    • level = 誤り訂正レベル -> QRCodeのoptionと同じ
    • adjust_for_sjis = trueを指定時は、ShiftJISの2バイト文字が途中で分割されないようにします。
    • 知識編で解説しましたが、一部のアプリで読み取った場合に、ShiftJISの2バイト文字が途中で分割されていると文字化けを起こします。このサンプルでは、分割を防止できるようにしています。
    # 必要に応じて、RQRCodeCore に対するrequireを追加してください。
    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

自動的にQRコードを分割して生成するメソッド

    # 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)
      # 1個のQRコードに収まる場合は、通常のQRコードで生成します。
      return [RQRCode::QRCode.new(data, options)] if fits_in_single_qrcode?(data, options)

      # データを分割
      sliced_text = slice_data_for_connected_qrcode(data, options)
      # パリティを生成
      parity = create_total_text_parity(data)
      # 分割したデータを元に、QRコードを生成する
      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

実際に出力する


  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
  • 3つに分割されたQRコードが生成されました。

    • 並べてレンダリングする場合、クリアエリア(QRコードの周りの余白)の確保も必要ですので、注意しましょう。

    qrcode_0.png qrcode_1.png qrcode_2.png

まとめ

  • ヘッダ部に分割に関する情報を記録した上で、分割したデータを与えてそれぞれのQRコードを生成すれば、分割QRコードが生成できる。
  • 分割を指定しているが実際のQRコードが1個だけの場合、読み取れないリーダーがあるようなので注意。(知識編を参照)
  • リーダーによって挙動が違う場合もあるので、動作検証は実際の機器類を使ってやりましょう。(知識編を参照)

参考文献

この記事は以下の情報を参考にして執筆しました。

更新履歴

  • 2020.08.26 byte_with_connected_encoding_start で書き込む「連結QRコードを示すMODE」の値を4→3に修正しました。
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2