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


* [知識編はこちら](https://qiita.com/c-flat/items/3dd4ca512632ce97fdca)
* では、実際に連結QRコードを出力してみましょう。

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

# rqrcode を改変する

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

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


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

* 先ず、ヘッダ部の書き込みを行う `QRBitBuffer` に、連結QRコード用のヘッダ書き込みのメソッドを追加します。

```ruby: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` クラスを作成します。

```ruby: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コードのバージョン（=大きさ）は、`option` の `size: xx` というパラメタ名なので注意してください。

```ruby: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,東京都品川区北品川９９ー９９９テストビル９Ｆ
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,ｍＬ
101,2,1,,5
111,2,1,,１日１回夕食後に,
201,2,1,1,7,1124022F2ZZZ,【般】ロラゼパム錠１ｍｇ,1,1,錠
101,3,1,,28
111,3,1,,１日３回毎食後に,
201,3,1,1,7,3136004F2ZZZ,【般】メコバラミン錠０．５ｍｇ,3,1,錠
201,3,2,1,7,2171017F2ZZZ,【般】ニコランジル錠５ｍｇ,3,1,錠
201,3,3,1,7,3962001F1ZZZ,【般】ブホルミン塩酸塩錠５０ｍｇ,3,1,錠
201,3,4,1,7,1124022F1ZZZ,【般】ロラゼパム錠０．５ｍｇ,3,1,錠
201,3,5,1,7,1124026F1ZZZ,【般】トフィソパム錠５０ｍｇ,3,1,錠
201,3,6,1,2,612140503,,3,1,錠
101,4,1,,28
111,4,1,,１日３回毎食後に,
201,4,1,1,7,2344009F2ZZZ,【般】酸化マグネシウム錠３３０ｍｇ,6,1,錠
101,5,3,,1
111,5,1,,足に,
201,5,1,1,7,2619803XAZZZ,【般】消毒用アルコール配合外用液,4,1,ｍＬ
101,6,1,,28
111,6,1,,１日３回毎食後に,
201,6,1,1,7,3136004F2ZZZ,【般】メコバラミン錠０．５ｍｇ,3,1,錠
201,6,2,1,7,2171017F2ZZZ,【般】ニコランジル錠５ｍｇ,3,1,錠
201,6,3,1,7,3962001F1ZZZ,【般】ブホルミン塩酸塩錠５０ｍｇ,3,1,錠
201,6,4,1,7,1124022F1ZZZ,【般】ロラゼパム錠０．５ｍｇ,3,1,錠
201,6,5,1,7,1124026F1ZZZ,【般】トフィソパム錠５０ｍｇ,3,1,錠
201,6,6,1,2,612140503,,3,1,錠
```

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

* 全てのデータのXOR値を算出します。

```ruby
    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 = [誤り訂正レベル](https://www.qrcode.com/about/error_correction.html) -> QRCodeのoptionと同じ
    - adjust_for_sjis = trueを指定時は、ShiftJISの2バイト文字が途中で分割されないようにします。    
    - 知識編で解説しましたが、一部のアプリで読み取った場合に、ShiftJISの2バイト文字が途中で分割されていると文字化けを起こします。このサンプルでは、分割を防止できるようにしています。
 
```ruby
    # 必要に応じて、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コードを分割して生成するメソッド

```ruby
    # 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
```

### 実際に出力する

```ruby

  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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697440/aefe797f-93b7-2fe8-bd7d-c3c76c927670.png) ![qrcode_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697440/54f7a0cd-a932-c8e4-b3f3-7596fa16e11e.png) ![qrcode_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/697440/7fbe107f-3cad-64b1-7d1d-12a18bb91347.png)


## まとめ

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

## 参考文献


* Thanks to [Duncan Robertson](https://github.com/whomwah)
* [rqrcode](https://github.com/whomwah/rqrcode)
* [rqrcode_code](https://github.com/whomwah/rqrcode_core)

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

* [QRコードドットコム](https://www.qrcode.com/)
* [Zxingを使って連結QRコードを読み書きする Part2:連結QRコードの読み取り](https://programresource.net/2013/05/04/2188.html)
* [JAHIS院外処方箋２次元シンボル記録条件規約Ver.1.6](https://www.jahis.jp/standard/detail/id=714)


## 更新履歴

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