rusted-coil old blog

はてなダイアリー上で書かれていた旧東方錆恋録 ~Slipping Rusted Magnemite~のデータをそのままインポートしたブログです。リダイレクト先を変える前に気づいたらダイアリーがサービス終了していたので、とりあえずリンク切れを防ぐため公開しています。

QRコード認識/生成ライブラリZXing3.2.2でマスクパターンを取得し完全に同じQRコードを再現

とあるアプリを試しに作ってたら必要になったのでメモ。もしかしたら元のZXingでうまくやればできるかもしれない。
なんか書いてみたらだからどうしたってレベルだったけど一応

概要

  • QRコードではモジュールの偏りを抑えるために最後に8種類のマスクパターンのうち1つをかける

http://www.swetake.com/qrcode/qr5.html

  • この「どのマスクパターンを選択するか」の重み付けが実装によるっぽいので、例えば普通にQRコードを認識→データを保存→そのデータを元にQRコード生成とかやると(エンコードされてるデータは同じでも)違うQRコードが生成される可能性がある。
  • ZXing 3.2.2ではQRCodeReaderクラスを使ったQRコード認識では「どのマスクパターンだったか」を外部から取得できなさそうなので、DecoderResultを拡張してマスクパターンの種類を取得できるようにした。

変更点

core/src/main/java/com/google/zxing/common/DecoderResult.java
public final class DecoderResult {
   //...//

   private Integer erasures;
   private Object other;
   private final int structuredAppendParity;
   private final int structuredAppendSequenceNumber;
 
   //sabikoi added
   private int maskIndex;
   public int getMaskIndex() {
     return maskIndex;
   }
   public void setMaskIndex(int index) {
     maskIndex = index;
   }

   //...//
     this.text = text;
     this.byteSegments = byteSegments;
     this.ecLevel = ecLevel;
     this.structuredAppendParity = saParity;
     this.structuredAppendSequenceNumber = saSequence;
     //sabikoi added
     maskIndex = -1;
   }
 
   public byte[] getRawBytes() {
     return rawBytes;
   }

マスクデータを保存する変数をリザルトのクラスに追加しただけ

core/src/main/java/com/google/zxing/Result.java
public final class Result {
   //...//
   private final byte[] rawBytes;
   private ResultPoint[] resultPoints;
   private final BarcodeFormat format;
   private Map<ResultMetadataType,Object> resultMetadata;
   private final long timestamp;
   //sabikoi added
   private int maskIndex;
   public int getMaskIndex() {
     return maskIndex;
   }
   public void setMaskIndex(int index) {
     maskIndex = index;
   }

   //...//
     this.rawBytes = rawBytes;
     this.resultPoints = resultPoints;
     this.format = format;
     this.resultMetadata = null;
     this.timestamp = timestamp;
     //sabikoi added
     maskIndex = -1;
   }
 

こちらも同様

core/src/main/java/com/google/zxing/qrcode/QRCodeReader.java
  //...//

  @Override
  public final Result decode(BinaryBitmap image, Map<DecodeHintType,?> hints)
      throws NotFoundException, ChecksumException, FormatException {
    //...//

    Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.QR_CODE);
    //sabikoi added
    result.setMaskIndex(decoderResult.getMaskIndex());
    List<byte[]> byteSegments = decoderResult.getByteSegments();
    if (byteSegments != null) {
      result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments);
    }

そして外部から利用できるようにQRCoderReaderが返すResultオブジェクトにセット(内部的には一旦デコーダから帰ってきたDecoderResultを受け取ってResultを作っている)

core/src/main/java/com/google/zxing/qrcode/decoder/Decoder.java
  //...//

  public DecoderResult decode(BitMatrix bits, Map<DecodeHintType,?> hints)
      throws FormatException, ChecksumException {

      //...//

      // Prepare for a mirrored reading.
      parser.mirror();

      DecoderResult result = decode(parser, hints);

      //sabikoi added
      result.setMaskIndex(parser.getMaskIndex());

      // Success! Notify the caller that the code was mirrored.
      result.setOther(new QRCodeDecoderMetaData(true));

      return result;

      //...//

  private DecoderResult decode(BitMatrixParser parser, Map<DecodeHintType,?> hints)
      throws FormatException, ChecksumException {

      //...//
    // Error-correct and copy data blocks together into a stream of bytes
    for (DataBlock dataBlock : dataBlocks) {
      byte[] codewordBytes = dataBlock.getCodewords();
      int numDataCodewords = dataBlock.getNumDataCodewords();
      correctErrors(codewordBytes, numDataCodewords);
      for (int i = 0; i < numDataCodewords; i++) {
        resultBytes[resultOffset++] = codewordBytes[i];
      }
    }

    //sabikoi modified
    DecoderResult res = DecodedBitStreamParser.decode(resultBytes, version, ecLevel, hints);
    res.setMaskIndex(parser.getMaskIndex());
    return res;

    // Decode the contents of that stream of bytes
//    return DecodedBitStreamParser.decode(resultBytes, version, ecLevel, hints);

このままでは間違いなく-1が返ってくるので実際にマスクパターンを処理してるDecoderにリザルトへのセットを追加

core/src/main/java/com/google/zxing/qrcode/decoder/BitMatrixParser.java
final class BitMatrixParser {
  //...//

  /**
   * Revert the mask removal done while reading the code words. The bit matrix should revert to its original state.
   */
  void remask() {
    if (parsedFormatInfo == null) {
      return; // We have no format information, and have no data mask
    }
    DataMask dataMask = DataMask.forReference(parsedFormatInfo.getDataMask());
    int dimension = bitMatrix.getHeight();
    dataMask.unmaskBitMatrix(bitMatrix, dimension);
  }

//sabikoi added
  public int getMaskIndex(){
    if(parsedFormatInfo == null){
      return -1;
    }
    return parsedFormatInfo.getDataMask();
  }

  /**
   * Prepare the parser for a mirrored operation.
   * This flag has effect only on the {@link #readFormatInformation()} and the
   * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the
   * {@link #mirror()} method should be called.
   * 
   * @param mirror Whether to read version and format information mirrored.
   */
  void setMirror(boolean mirror) {
    parsedVersion = null;
    parsedFormatInfo = null;
    this.mirror = mirror;
  }

マスクパターンのインデックスは内部的にはparserに保持されてるので取得できるようにする


これでQRを読み込んだ後にリザルトから以下のように取得可能

  QRCodeReader reader = new QRCodeReader();
  Result result = null;
  result = reader.decode(bitmap);
  int qrMaskIndex = result.getMaskIndex();
core/src/main/java/com/google/zxing/EncodeHintType.java
  /**
   * Specifies the required number of layers for an Aztec code.
   * A negative number (-1, -2, -3, -4) specifies a compact Aztec code.
   * 0 indicates to use the minimum number of layers (the default).
   * A positive number (1, 2, .. 32) specifies a normal (non-compact) Aztec code.
   * (Type {@link Integer}, or {@link String} representation of the integer value).
   */
   AZTEC_LAYERS,

  /**
   * sabikoi added
   */
   MASK_INDEX
}
core/src/main/java/com/google/zxing/qrcode/encoder/Encoder.java
  public static QRCode encode(String content,
                              ErrorCorrectionLevel ecLevel,
                              Map<EncodeHintType,?> hints) throws WriterException {
     //...//

     //  Choose the mask pattern and set to "qrCode".
     int dimension = version.getDimensionForVersion();
     ByteMatrix matrix = new ByteMatrix(dimension, dimension);

     //sabikoi modified
     int maskPattern = (
       (hints != null && hints.containsKey(EncodeHintType.MASK_INDEX)) 
         ? (Integer)hints.get(EncodeHintType.MASK_INDEX) 
         : chooseMaskPattern(finalBits, ecLevel, version, matrix));
     qrCode.setMaskPattern(maskPattern);

     if (hints != null && hints.containsKey(EncodeHintType.CHARACTER_SET)) {
       encoding = hints.get(EncodeHintType.CHARACTER_SET).toString();
     }

生成する時はEncodeHintとして設定できるようにした


使う時はさっき取得したインデックスを保存しておいて指定するだけ

  hints.put(EncodeHintType.ERROR_CORRECTION, qrErrorLevel);
  hints.put(EncodeHintType.MASK_INDEX, qrMaskData);
  QRCodeWriter writer = new QRCodeWriter();
  BitMatrix bitMatrix = writer.encode(new String(qrContent, "ISO_8859_1"), BarcodeFormat.QR_CODE, width, height, hints);

ソースからのAndroidプロジェクトへの組み込み

mavenをインストールしてzxingのルートフォルダでビルド、core-3.2.2-SNAPSHOT.jarとjavase-3.2.2-SNAPSHOT.jarをライブラリとして追加するだけ(多分)

結果