-
では、実際に連結QRコードを出力してみましょう。
- 知識編にも書きましたが、今回は処方箋のQRコードで使う
8bit_byte
モードの連結QRコードを生成します。 - 処方箋に書き込むデータの実態は ShiftJISエンコーディングのCSVデータファイルです。(改行=CR/LF・EOFあり)(こちらで定義されています)
- 知識編にも書きましたが、今回は処方箋のQRコードで使う
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コードのバージョン(=大きさ)は、
option
のsize: 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
まとめ
- ヘッダ部に分割に関する情報を記録した上で、分割したデータを与えてそれぞれのQRコードを生成すれば、分割QRコードが生成できる。
- 分割を指定しているが実際のQRコードが1個だけの場合、読み取れないリーダーがあるようなので注意。(知識編を参照)
- リーダーによって挙動が違う場合もあるので、動作検証は実際の機器類を使ってやりましょう。(知識編を参照)
参考文献
- Thanks to Duncan Robertson
- rqrcode
- rqrcode_code
この記事は以下の情報を参考にして執筆しました。
更新履歴
- 2020.08.26
byte_with_connected_encoding_start
で書き込む「連結QRコードを示すMODE」の値を4→3に修正しました。