連結QRコードの作成であるが、筆者が連結QRが必要となったとび森のデータでは連結QRは4枚で各コードのサイズは固定であったため、データを元に分割数を決めると言った処理は省略している。本来はライブラリ側で受け取ったデータを適切に分割し、複数枚のQR Bitmapデータを返すべきであるがそこまでの修正は大変なので、筆者がZxingライブラリに行った修正は以下の点だけである。
- 連結QR作成用関数の追加
- ヘッダー部分に連結フラグ、ページ、パリティを追加する
データの分割、ページ数及びパリティはアプリ側であらかじめ準備しておき、Zxingでは単一QRの作成と処理はほぼ同様で、ヘッダー部分だけ連結の内容を組み込めるようにしただけである。
Zxingのソースで修正したのは2箇所。2.0のソースベースなので、最新版では異なる可能性がある。
まずはQRCodeWriter.javaの47行目から78行目に「encode_sa」を追加。
public final class QRCodeWriter implements Writer { private static final int QUIET_ZONE_SIZE = 4; @Override public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) throws WriterException { return encode(contents, format, width, height, null); } public BitMatrix encode_sa(byte page, byte totalpage, int parity, String contents, BarcodeFormat format, int width, int height, Map<EncodeHintType,?> hints) throws WriterException { if (contents.length() == 0) { throw new IllegalArgumentException("Found empty contents"); } if (format != BarcodeFormat.QR_CODE) { throw new IllegalArgumentException("Can only encode QR_CODE, but got " + format); } if (width < 0 || height < 0) { throw new IllegalArgumentException("Requested dimensions are too small: " + width + 'x' + height); } ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L; if (hints != null) { ErrorCorrectionLevel requestedECLevel = (ErrorCorrectionLevel) hints.get(EncodeHintType.ERROR_CORRECTION); if (requestedECLevel != null) { errorCorrectionLevel = requestedECLevel; } } QRCode code = new QRCode(); Encoder.encode_sa(page,totalpage,parity,contents, errorCorrectionLevel, hints, code); return renderResult(code, width, height); } @Override public BitMatrix encode(String contents,
アプリからは上記関数を呼び出す際、ページ番号やパリティ、そのページ用のQRコードデータを渡す。次にEncoder.javaの82行目から150行目に「encode_sa」を追加。
public static void encode(String content, ErrorCorrectionLevel ecLevel, QRCode qrCode) throws WriterException { encode(content, ecLevel, null, qrCode); } //structured arrayed data, NeFa 2013/3/4 //data page split and parity calcuated on caller side public static void encode_sa(byte page, byte totalpage, int parity, String content, ErrorCorrectionLevel ecLevel, Map<EncodeHintType,?> hints, QRCode qrCode) throws WriterException { String encoding = hints == null ? null : (String) hints.get(EncodeHintType.CHARACTER_SET); if (encoding == null) { encoding = DEFAULT_BYTE_MODE_ENCODING; } // Step 1: Choose the mode (encoding). Mode mode = chooseMode(content, encoding); // Step 2: Append "bytes" into "dataBits" in appropriate encoding. BitArray dataBits = new BitArray(); appendBytes(content, mode, dataBits, encoding); // Step 3: Initialize QR code that can contain "dataBits". int numInputBits = dataBits.getSize(); initQRCode(numInputBits, ecLevel, mode, qrCode); // Step 4: Build another bit vector that contains header and data. BitArray headerAndDataBits = new BitArray(); // Step 4.1: Add code page info byte tmpbyte; tmpbyte = 0x3; headerAndDataBits.appendBits(tmpbyte, 4); headerAndDataBits.appendBits(page, 4); headerAndDataBits.appendBits(totalpage, 4); headerAndDataBits.appendBits((byte)((parity&0xf0)>>4), 4); headerAndDataBits.appendBits((byte)(parity&0x0f), 4); // Step 4.5: Append ECI message if applicable if (mode == Mode.BYTE && !DEFAULT_BYTE_MODE_ENCODING.equals(encoding)) { CharacterSetECI eci = CharacterSetECI.getCharacterSetECIByName(encoding); if (eci != null) { appendECI(eci, headerAndDataBits); } } appendModeInfo(mode, headerAndDataBits); int numLetters = mode == Mode.BYTE ? dataBits.getSizeInBytes() : content.length(); appendLengthInfo(numLetters, qrCode.getVersion(), mode, headerAndDataBits); headerAndDataBits.appendBitArray(dataBits); // Step 5: Terminate the bits properly. terminateBits(qrCode.getNumDataBytes(), headerAndDataBits); // Step 6: Interleave data bits with error correction code. BitArray finalBits = new BitArray(); interleaveWithECBytes(headerAndDataBits, qrCode.getNumTotalBytes(), qrCode.getNumDataBytes(), qrCode.getNumRSBlocks(), finalBits); // Step 7: Choose the mask pattern and set to "qrCode". ByteMatrix matrix = new ByteMatrix(qrCode.getMatrixWidth(), qrCode.getMatrixWidth()); qrCode.setMaskPattern(chooseMaskPattern(finalBits, ecLevel, qrCode.getVersion(), matrix)); // Step 8. Build the matrix and set it to "qrCode". MatrixUtil.buildMatrix(finalBits, ecLevel, qrCode.getVersion(), qrCode.getMaskPattern(), matrix); qrCode.setMatrix(matrix); // Step 9. Make sure we have a valid QR Code. if (!qrCode.isValid()) { throw new WriterException("Invalid QR code: " + qrCode.toString()); } } public static void encode(String content,
中身はencode関数のほぼコピーで、Step4.1のヘッダー情報を追加しているくらいである。修正後、Step1で準備しておいたbuildcore.batを実行してcore.jarをリビルド、androidのプロジェクトに組み込み直す。
そして、アプリの呼び出し部。
private byte calcParity(byte[] data){ byte parity; parity = data[0]; for (int i=1;i<data.length;i++) parity = (byte) (parity ^ data[i]);//XOR return parity; } private static final String ENCORD_NAME = "ISO-8859-1"; public Bitmap createBinQRCode(byte[] qrData,int size,int page) { String contents; Bitmap bitmap = null; try { QRCodeWriter writer = new QRCodeWriter(); Hashtable encodeHint = new Hashtable(); encodeHint.put(EncodeHintType.CHARACTER_SET, ENCORD_NAME); encodeHint.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); BitMatrix bitData; byte partbuff[] = new byte[0x21c]; System.arraycopy(qrData,0x21c*page,partbuff,0,0x21c); contents = new String(partbuff, ENCORD_NAME); bitData = writer.encode_sa((byte)(page&0xff),(byte)3,calcParity(qrData),contents, BarcodeFormat.QR_CODE, size, size, encodeHint);
qrDataに全データが入っており、本関数を呼び出す度にpageを指定しQRコードを生成している。とび森のQRコードは1コードあたり0x21cサイズ固定だったため、指定ページにあたるデータの0x21c分のみ抜き出してQRコード化している。
Parityは全連結QRコード共通の為、全データに対するParityを算出。この算出コードを組んだのがかなり前なのでこれで正しいか覚えていないが、おそらくほとんどの連結QRコード読み取りは連結用IDとして扱いParity計算まではしていない気もするので、適当な乱数をParityとして設定しても問題なさそうな気はする。
とりあえず以上で簡単ではあるが筆者がとび森用に連結QRコード読み書き対応を行った内容である。
誰かの参考にあれば幸いである。
参考にしたサイト:
http://wada811.blog.fc2.com/blog-entry-56.html
http://d.hatena.ne.jp/tomorrowkey/20091114/1258206013
http://ahoworld.blog58.fc2.com/?mode=m&no=255
http://wiashia.blog.so-net.ne.jp/2012-05-23
This post is also available in: 英語