Aztec updates from fyellin

git-svn-id: https://zxing.googlecode.com/svn/trunk@2834 59b500cc-1b3d-0410-9834-0bbf25fbcc57
This commit is contained in:
srowen@gmail.com 2013-07-01 08:58:45 +00:00
parent 7544a08668
commit e8a23d5085
11 changed files with 300 additions and 145 deletions

View file

@ -76,4 +76,11 @@ public enum EncodeHintType {
*/ */
PDF417_DIMENSIONS, PDF417_DIMENSIONS,
/**
* 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 normaol (non-compact) Aztec code
*/
AZTEC_LAYERS,
} }

View file

@ -26,9 +26,9 @@ import com.google.zxing.Result;
import com.google.zxing.ResultMetadataType; import com.google.zxing.ResultMetadataType;
import com.google.zxing.ResultPoint; import com.google.zxing.ResultPoint;
import com.google.zxing.ResultPointCallback; import com.google.zxing.ResultPointCallback;
import com.google.zxing.common.DecoderResult;
import com.google.zxing.aztec.decoder.Decoder; import com.google.zxing.aztec.decoder.Decoder;
import com.google.zxing.aztec.detector.Detector; import com.google.zxing.aztec.detector.Detector;
import com.google.zxing.common.DecoderResult;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -57,8 +57,44 @@ public final class AztecReader implements Reader {
public Result decode(BinaryBitmap image, Map<DecodeHintType,?> hints) public Result decode(BinaryBitmap image, Map<DecodeHintType,?> hints)
throws NotFoundException, FormatException { throws NotFoundException, FormatException {
AztecDetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(); NotFoundException notFoundException = null;
ResultPoint[] points = detectorResult.getPoints(); FormatException formatException = null;
Detector detector = new Detector(image.getBlackMatrix());
ResultPoint[] points = null;
DecoderResult decoderResult = null;
try {
AztecDetectorResult detectorResult = detector.detect(false);
points = detectorResult.getPoints();
decoderResult = new Decoder().decode(detectorResult);
} catch (NotFoundException e) {
notFoundException = e;
} catch (FormatException e) {
formatException = e;
}
if (decoderResult == null) {
try {
AztecDetectorResult detectorResult = detector.detect(true);
points = detectorResult.getPoints();
decoderResult = new Decoder().decode(detectorResult);
} catch (NotFoundException e) {
if (notFoundException != null) {
throw notFoundException;
}
if (formatException != null) {
throw formatException;
}
throw e;
} catch (FormatException e) {
// throw the exception from the non-mirror case, instead
if (notFoundException != null) {
throw notFoundException;
}
if (formatException != null) {
throw formatException;
}
throw e;
}
}
if (hints != null) { if (hints != null) {
ResultPointCallback rpcb = (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); ResultPointCallback rpcb = (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);
@ -69,8 +105,6 @@ public final class AztecReader implements Reader {
} }
} }
DecoderResult decoderResult = new Decoder().decode(detectorResult);
Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.AZTEC); Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.AZTEC);
List<byte[]> byteSegments = decoderResult.getByteSegments(); List<byte[]> byteSegments = decoderResult.getByteSegments();
@ -90,4 +124,4 @@ public final class AztecReader implements Reader {
// do nothing // do nothing
} }
} }

View file

@ -16,9 +16,6 @@
package com.google.zxing.aztec; package com.google.zxing.aztec;
import java.nio.charset.Charset;
import java.util.Map;
import com.google.zxing.BarcodeFormat; import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType; import com.google.zxing.EncodeHintType;
import com.google.zxing.Writer; import com.google.zxing.Writer;
@ -26,37 +23,39 @@ import com.google.zxing.aztec.encoder.AztecCode;
import com.google.zxing.aztec.encoder.Encoder; import com.google.zxing.aztec.encoder.Encoder;
import com.google.zxing.common.BitMatrix; import com.google.zxing.common.BitMatrix;
import java.nio.charset.Charset;
import java.util.Map;
public final class AztecWriter implements Writer { public final class AztecWriter implements Writer {
private static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); private static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");
@Override @Override
public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) { public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) {
return encode(contents, format, width, height, DEFAULT_CHARSET, Encoder.DEFAULT_EC_PERCENT); return encode(contents, format, width, height, null);
} }
@Override @Override
public BitMatrix encode(String contents, BarcodeFormat format, int width, int height, Map<EncodeHintType,?> hints) { public BitMatrix encode(String contents, BarcodeFormat format, int width, int height, Map<EncodeHintType,?> hints) {
String charset = hints == null ? null : (String) hints.get(EncodeHintType.CHARACTER_SET); String charset = hints == null ? null : (String) hints.get(EncodeHintType.CHARACTER_SET);
Number eccPercent = hints == null ? null : (Number) hints.get(EncodeHintType.ERROR_CORRECTION); Number eccPercent = hints == null ? null : (Number) hints.get(EncodeHintType.ERROR_CORRECTION);
Integer layers = hints == null ? null : (Integer)hints.get(EncodeHintType.AZTEC_LAYERS);
return encode(contents, return encode(contents,
format, format,
width, width,
height, height,
charset == null ? DEFAULT_CHARSET : Charset.forName(charset), charset == null ? DEFAULT_CHARSET : Charset.forName(charset),
eccPercent == null ? Encoder.DEFAULT_EC_PERCENT : eccPercent.intValue()); eccPercent == null ? Encoder.DEFAULT_EC_PERCENT : eccPercent.intValue(),
layers == null ? Encoder.DEFAULT_AZTEC_LAYERS : layers.intValue());
} }
private static BitMatrix encode(String contents, private static BitMatrix encode(String contents, BarcodeFormat format,
BarcodeFormat format, int width, int height,
int width, Charset charset, int eccPercent, int layers) {
int height,
Charset charset,
int eccPercent) {
if (format != BarcodeFormat.AZTEC) { if (format != BarcodeFormat.AZTEC) {
throw new IllegalArgumentException("Can only encode AZTEC, but got " + format); throw new IllegalArgumentException("Can only encode AZTEC, but got " + format);
} }
AztecCode aztec = Encoder.encode(contents.getBytes(charset), eccPercent); AztecCode aztec = Encoder.encode(contents.getBytes(charset), eccPercent, layers);
return renderResult(aztec, width, height); return renderResult(aztec, width, height);
} }
@ -84,8 +83,6 @@ public final class AztecWriter implements Writer {
} }
} }
} }
return output; return output;
} }
} }

View file

@ -48,13 +48,17 @@ public final class Detector {
this.image = image; this.image = image;
} }
public AztecDetectorResult detect() throws NotFoundException {
return detect(false);
}
/** /**
* Detects an Aztec Code in an image. * Detects an Aztec Code in an image.
* *
* @return {@link AztecDetectorResult} encapsulating results of detecting an Aztec Code * @return {@link AztecDetectorResult} encapsulating results of detecting an Aztec Code
* @throws NotFoundException if no Aztec Code can be found * @throws NotFoundException if no Aztec Code can be found
*/ */
public AztecDetectorResult detect() throws NotFoundException { public AztecDetectorResult detect(boolean isMirror) throws NotFoundException {
// 1. Get the center of the aztec matrix // 1. Get the center of the aztec matrix
Point pCenter = getMatrixCenter(); Point pCenter = getMatrixCenter();
@ -63,6 +67,12 @@ public final class Detector {
// [topRight, bottomRight, bottomLeft, topLeft] // [topRight, bottomRight, bottomLeft, topLeft]
ResultPoint[] bullsEyeCorners = getBullsEyeCorners(pCenter); ResultPoint[] bullsEyeCorners = getBullsEyeCorners(pCenter);
if (isMirror) {
ResultPoint temp = bullsEyeCorners[0];
bullsEyeCorners[0] = bullsEyeCorners[2];
bullsEyeCorners[2] = temp;
}
// 3. Get the size of the matrix and other parameters from the bull's eye // 3. Get the size of the matrix and other parameters from the bull's eye
extractParameters(bullsEyeCorners); extractParameters(bullsEyeCorners);
@ -135,14 +145,14 @@ public final class Detector {
} }
} }
private int[] expectedCornerBits = { private static final int[] EXPECTED_CORNER_BITS = {
0xee0, // 07340 XXX .XX X.. ... 0xee0, // 07340 XXX .XX X.. ...
0x1dc, // 00734 ... XXX .XX X.. 0x1dc, // 00734 ... XXX .XX X..
0x83b, // 04073 X.. ... XXX .XX 0x83b, // 04073 X.. ... XXX .XX
0x707, // 03407 .XX X.. ... XXX 0x707, // 03407 .XX X.. ... XXX
}; };
private int getRotation(int[] sides, int length) throws NotFoundException { private static int getRotation(int[] sides, int length) throws NotFoundException {
// In a normal pattern, we expect to See // In a normal pattern, we expect to See
// ** .* D A // ** .* D A
// * * // * *
@ -166,7 +176,7 @@ public final class Detector {
// corner. Since the four rotation values have a Hamming distance of 8, we // corner. Since the four rotation values have a Hamming distance of 8, we
// can easily tolerate two errors. // can easily tolerate two errors.
for (int shift = 0; shift < 4; shift++) { for (int shift = 0; shift < 4; shift++) {
if (Integer.bitCount(cornerBits ^ expectedCornerBits[shift]) <= 2) { if (Integer.bitCount(cornerBits ^ EXPECTED_CORNER_BITS[shift]) <= 2) {
return shift; return shift;
} }
} }
@ -343,7 +353,6 @@ public final class Detector {
* *
* @param bullsEyeCorners the array of bull's eye corners * @param bullsEyeCorners the array of bull's eye corners
* @return the array of aztec code corners * @return the array of aztec code corners
* @throws NotFoundException if the corner points do not fit in the image
*/ */
private ResultPoint[] getMatrixCornerPoints(ResultPoint[] bullsEyeCorners) { private ResultPoint[] getMatrixCornerPoints(ResultPoint[] bullsEyeCorners) {
return expandSquare(bullsEyeCorners, 2 * nbCenterLayers, getDimension()); return expandSquare(bullsEyeCorners, 2 * nbCenterLayers, getDimension());

View file

@ -23,11 +23,10 @@ final class BinaryShiftToken extends Token {
private final short binaryShiftStart; private final short binaryShiftStart;
private final short binaryShiftByteCount; private final short binaryShiftByteCount;
BinaryShiftToken(Token previous, BinaryShiftToken(Token previous,
int totalBitCount, int binaryShiftStart,
int binaryShiftStart,
int binaryShiftByteCount) { int binaryShiftByteCount) {
super(previous, totalBitCount); super(previous);
this.binaryShiftStart = (short) binaryShiftStart; this.binaryShiftStart = (short) binaryShiftStart;
this.binaryShiftByteCount = (short) binaryShiftByteCount; this.binaryShiftByteCount = (short) binaryShiftByteCount;
} }
@ -38,7 +37,7 @@ final class BinaryShiftToken extends Token {
if (i == 0 || (i == 31 && binaryShiftByteCount <= 62)) { if (i == 0 || (i == 31 && binaryShiftByteCount <= 62)) {
// We need a header before the first character, and before // We need a header before the first character, and before
// character 31 when the total byte code is <= 62 // character 31 when the total byte code is <= 62
bitArray.appendBits(31, 5); bitArray.appendBits(31, 5); // BINARY_SHIFT
if (binaryShiftByteCount > 62) { if (binaryShiftByteCount > 62) {
bitArray.appendBits(binaryShiftByteCount - 31, 16); bitArray.appendBits(binaryShiftByteCount - 31, 16);
} else if (i == 0) { } else if (i == 0) {
@ -51,7 +50,6 @@ final class BinaryShiftToken extends Token {
} }
bitArray.appendBits(text[binaryShiftStart + i], 8); bitArray.appendBits(text[binaryShiftStart + i], 8);
} }
//assert bitArray.getSize() == getTotalBitCount();
} }
@Override @Override

View file

@ -29,7 +29,9 @@ import com.google.zxing.common.reedsolomon.ReedSolomonEncoder;
public final class Encoder { public final class Encoder {
public static final int DEFAULT_EC_PERCENT = 33; // default minimal percentage of error check words public static final int DEFAULT_EC_PERCENT = 33; // default minimal percentage of error check words
public static final int DEFAULT_AZTEC_LAYERS = 0;
private static final int MAX_NB_BITS = 32; private static final int MAX_NB_BITS = 32;
private static final int MAX_NB_BITS_COMPACT = 4;
private static final int[] WORD_SIZE = { private static final int[] WORD_SIZE = {
4, 6, 6, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 4, 6, 6, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
@ -46,7 +48,7 @@ public final class Encoder {
* @return Aztec symbol matrix with metadata * @return Aztec symbol matrix with metadata
*/ */
public static AztecCode encode(byte[] data) { public static AztecCode encode(byte[] data) {
return encode(data, DEFAULT_EC_PERCENT); return encode(data, DEFAULT_EC_PERCENT, DEFAULT_AZTEC_LAYERS);
} }
/** /**
@ -55,10 +57,10 @@ public final class Encoder {
* @param data input data string * @param data input data string
* @param minECCPercent minimal percentage of error check words (According to ISO/IEC 24778:2008, * @param minECCPercent minimal percentage of error check words (According to ISO/IEC 24778:2008,
* a minimum of 23% + 3 words is recommended) * a minimum of 23% + 3 words is recommended)
* @param userSpecifiedLayers if non-zero, a user-specified value for the number of layers
* @return Aztec symbol matrix with metadata * @return Aztec symbol matrix with metadata
*/ */
public static AztecCode encode(byte[] data, int minECCPercent) { public static AztecCode encode(byte[] data, int minECCPercent, int userSpecifiedLayers) {
// High-level encode // High-level encode
BitArray bits = new HighLevelEncoder(data).encode(); BitArray bits = new HighLevelEncoder(data).encode();
@ -68,50 +70,62 @@ public final class Encoder {
boolean compact; boolean compact;
int layers; int layers;
int totalBitsInLayer; int totalBitsInLayer;
int wordSize = 0; int wordSize;
BitArray stuffedBits = null; BitArray stuffedBits;
// We look at the possible table sizes in the order Compact1, Compact2, Compact3, if (userSpecifiedLayers != DEFAULT_AZTEC_LAYERS) {
// Compact4, Normal4,... Normal(i) for i < 4 isn't typically used since Compact(i+1) compact = userSpecifiedLayers < 0;
// is the same size, but has more data. layers = Math.abs(userSpecifiedLayers);
for (int i = 0; ; i++) { if (layers > (compact ? MAX_NB_BITS_COMPACT : MAX_NB_BITS)) {
if (i > MAX_NB_BITS) { throw new IllegalArgumentException(
throw new IllegalArgumentException("Data too large for an Aztec code"); String.format("Illegal value %s for layers", userSpecifiedLayers));
} }
compact = i <= 3;
layers = compact ? i + 1 : i;
totalBitsInLayer = totalBitsInLayer(layers, compact); totalBitsInLayer = totalBitsInLayer(layers, compact);
if (totalSizeBits > totalBitsInLayer) { wordSize = WORD_SIZE[layers];
continue;
}
// [Re]stuff the bits if this is the first opportunity, or if the
// wordSize has changed
if (wordSize != WORD_SIZE[layers]) {
wordSize = WORD_SIZE[layers];
stuffedBits = stuffBits(bits, wordSize);
}
int usableBitsInLayers = totalBitsInLayer - (totalBitsInLayer % wordSize); int usableBitsInLayers = totalBitsInLayer - (totalBitsInLayer % wordSize);
if (stuffedBits.getSize() + eccBits <= usableBitsInLayers) { stuffedBits = stuffBits(bits, wordSize);
break; if (stuffedBits.getSize() + eccBits > usableBitsInLayers) {
throw new IllegalArgumentException("Data to large for user specified layer");
}
if (compact && stuffedBits.getSize() > wordSize * 64) {
// Compact format only allows 64 data words, though C4 can hold more words than that
throw new IllegalArgumentException("Data to large for user specified layer");
}
} else {
wordSize = 0;
stuffedBits = null;
// We look at the possible table sizes in the order Compact1, Compact2, Compact3,
// Compact4, Normal4,... Normal(i) for i < 4 isn't typically used since Compact(i+1)
// is the same size, but has more data.
for (int i = 0; ; i++) {
if (i > MAX_NB_BITS) {
throw new IllegalArgumentException("Data too large for an Aztec code");
}
compact = i <= 3;
layers = compact ? i + 1 : i;
totalBitsInLayer = totalBitsInLayer(layers, compact);
if (totalSizeBits > totalBitsInLayer) {
continue;
}
// [Re]stuff the bits if this is the first opportunity, or if the
// wordSize has changed
if (wordSize != WORD_SIZE[layers]) {
wordSize = WORD_SIZE[layers];
stuffedBits = stuffBits(bits, wordSize);
}
int usableBitsInLayers = totalBitsInLayer - (totalBitsInLayer % wordSize);
if (compact && stuffedBits.getSize() > wordSize * 64) {
// Compact format only allows 64 data words, though C4 can hold more words than that
continue;
}
if (stuffedBits.getSize() + eccBits <= usableBitsInLayers) {
break;
}
} }
} }
BitArray messageBits = generateCheckWords(stuffedBits, totalBitsInLayer, wordSize);
int messageSizeInWords = stuffedBits.getSize() / wordSize;
// generate check words
ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize));
int totalWordsInLayer = totalBitsInLayer / wordSize;
int[] messageWords = bitsToWords(stuffedBits, wordSize, totalWordsInLayer);
rs.encode(messageWords, totalWordsInLayer - messageSizeInWords);
// convert to bit array and pad in the beginning
int startPad = totalBitsInLayer % wordSize;
BitArray messageBits = new BitArray();
messageBits.appendBits(0, startPad);
for (int messageWord : messageWords) {
messageBits.appendBits(messageWord, wordSize);
}
// generate mode message // generate mode message
int messageSizeInWords = stuffedBits.getSize() / wordSize;
BitArray modeMessage = generateModeMessage(compact, layers, messageSizeInWords); BitArray modeMessage = generateModeMessage(compact, layers, messageSizeInWords);
// allocate symbol // allocate symbol
@ -254,12 +268,13 @@ public final class Encoder {
} }
} }
private static BitArray generateCheckWords(BitArray stuffedBits, int totalBits, int wordSize) { private static BitArray generateCheckWords(BitArray bitArray, int totalBits, int wordSize) {
// stuffedBits is guaranteed to be a multiple of the wordSize, so no padding needed assert bitArray.getSize() % wordSize == 0;
int messageSizeInWords = stuffedBits.getSize() / wordSize; // bitArray is guaranteed to be a multiple of the wordSize, so no padding needed
int messageSizeInWords = bitArray.getSize() / wordSize;
ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize)); ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize));
int totalWords = totalBits / wordSize; int totalWords = totalBits / wordSize;
int[] messageWords = bitsToWords(stuffedBits, wordSize, totalWords); int[] messageWords = bitsToWords(bitArray, wordSize, totalWords);
rs.encode(messageWords, totalWords - messageSizeInWords); rs.encode(messageWords, totalWords - messageSizeInWords);
int startPad = totalBits % wordSize; int startPad = totalBits % wordSize;
BitArray messageBits = new BitArray(); BitArray messageBits = new BitArray();
@ -304,7 +319,6 @@ public final class Encoder {
static BitArray stuffBits(BitArray bits, int wordSize) { static BitArray stuffBits(BitArray bits, int wordSize) {
BitArray out = new BitArray(); BitArray out = new BitArray();
// 1. stuff the bits
int n = bits.getSize(); int n = bits.getSize();
int mask = (1 << wordSize) - 2; int mask = (1 << wordSize) - 2;
for (int i = 0; i < n; i += wordSize) { for (int i = 0; i < n; i += wordSize) {

View file

@ -130,7 +130,7 @@ public final class HighLevelEncoder {
} }
} }
// A map showing the available shift coodes. (The shifts to BINARY are not // A map showing the available shift codes. (The shifts to BINARY are not
// shown // shown
static final int[][] SHIFT_TABLE = new int[6][6]; // mode shift codes, per table static final int[][] SHIFT_TABLE = new int[6][6]; // mode shift codes, per table
static { static {

View file

@ -24,8 +24,8 @@ final class SimpleToken extends Token {
private final short value; private final short value;
private final short bitCount; private final short bitCount;
SimpleToken(Token previous, int totalBitCount, int value, int bitCount) { SimpleToken(Token previous, int value, int bitCount) {
super(previous, totalBitCount); super(previous);
this.value = (short) value; this.value = (short) value;
this.bitCount = (short) bitCount; this.bitCount = (short) bitCount;
} }

View file

@ -20,31 +20,25 @@ import com.google.zxing.common.BitArray;
abstract class Token { abstract class Token {
static final Token EMPTY = new SimpleToken(null, 0, 0, 0); static final Token EMPTY = new SimpleToken(null, 0, 0);
private final Token previous; private final Token previous;
private final int totalBitCount; // For debugging purposes, only
Token(Token previous, int totalBitCount) { Token(Token previous) {
this.previous = previous; this.previous = previous;
this.totalBitCount = totalBitCount;
} }
final Token getPrevious() { final Token getPrevious() {
return previous; return previous;
} }
final int getTotalBitCount() {
return totalBitCount;
}
final Token add(int value, int bitCount) { final Token add(int value, int bitCount) {
return new SimpleToken(this, this.totalBitCount + bitCount, value, bitCount); return new SimpleToken(this, value, bitCount);
} }
final Token addBinaryShift(int start, int byteCount) { final Token addBinaryShift(int start, int byteCount) {
int bitCount = (byteCount * 8) + (byteCount <= 31 ? 10 : byteCount <= 62 ? 20 : 21); int bitCount = (byteCount * 8) + (byteCount <= 31 ? 10 : byteCount <= 62 ? 20 : 21);
return new BinaryShiftToken(this, this.totalBitCount + bitCount, start, byteCount); return new BinaryShiftToken(this, start, byteCount);
} }
abstract void appendTo(BitArray bitArray, byte[] text); abstract void appendTo(BitArray bitArray, byte[] text);

View file

@ -18,19 +18,22 @@ package com.google.zxing.aztec.detector;
import com.google.zxing.NotFoundException; import com.google.zxing.NotFoundException;
import com.google.zxing.aztec.AztecDetectorResult; import com.google.zxing.aztec.AztecDetectorResult;
import com.google.zxing.aztec.decoder.Decoder;
import com.google.zxing.aztec.detector.Detector.Point; import com.google.zxing.aztec.detector.Detector.Point;
import com.google.zxing.aztec.encoder.AztecCode; import com.google.zxing.aztec.encoder.AztecCode;
import com.google.zxing.aztec.encoder.Encoder; import com.google.zxing.aztec.encoder.Encoder;
import com.google.zxing.common.BitMatrix; import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.DecoderResult;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
/** /**
* Tests for the Detector * Tests for the Detector
@ -61,30 +64,46 @@ public final class DetectorTest extends Assert {
// Test that we can tolerate errors in the parameter locator bits // Test that we can tolerate errors in the parameter locator bits
private static void testErrorInParameterLocator(String data) throws Exception { private static void testErrorInParameterLocator(String data) throws Exception {
AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1), 25); AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1), 25, Encoder.DEFAULT_AZTEC_LAYERS);
Random random = new Random(aztec.getMatrix().hashCode()); // pseudo-random, but deterministic
int layers = aztec.getLayers(); int layers = aztec.getLayers();
boolean compact = aztec.isCompact(); boolean compact = aztec.isCompact();
List<Point> orientationPoints = getOrientationPoints(aztec); List<Point> orientationPoints = getOrientationPoints(aztec);
Random random = new Random(aztec.getMatrix().hashCode()); // random, but repeatable for (boolean isMirror : new boolean[] { false, true }) {
for (BitMatrix matrix : getRotations(aztec.getMatrix())) { for (BitMatrix matrix : getRotations(aztec.getMatrix())) {
// Each time through this loop, we reshuffle the corners, to get a different set of errors // Systematically try every possible 1- and 2-bit error.
Collections.shuffle(orientationPoints, random); for (int error1 = 0; error1 < orientationPoints.size(); error1++) {
for (int errors = 1; errors <= 3; errors++) { for (int error2 = error1; error2 < orientationPoints.size(); error2++) {
// Add another error to one of the parameter locator bits BitMatrix copy = isMirror ? transpose(matrix) : clone(matrix);
matrix.flip(orientationPoints.get(errors).getX(), orientationPoints.get(errors).getY()); copy.flip(orientationPoints.get(error1).getX(), orientationPoints.get(error1).getY());
try { if (error2 > error1) {
// The detector can't yet deal with bitmaps in which each square is only 1x1 pixel. // if error2 == error1, we only test a single error
// We zoom it larger. copy.flip(orientationPoints.get(error2).getX(), orientationPoints.get(error2).getY());
AztecDetectorResult r = new Detector(makeLarger(matrix, 3)).detect(); }
if (errors < 3) { // The detector doesn't seem to work when matrix bits are only 1x1. So magnify.
AztecDetectorResult r = new Detector(makeLarger(copy, 3)).detect(isMirror);
assertNotNull(r); assertNotNull(r);
assertEquals(r.getNbLayers(), layers); assertEquals(r.getNbLayers(), layers);
assertEquals(r.isCompact(), compact); assertEquals(r.isCompact(), compact);
} else { DecoderResult res = new Decoder().decode(r);
fail("Should not succeed with more than two errors"); assertEquals(data, res.getText());
} }
} catch (NotFoundException e) { }
assertEquals("Should only fail with three errors", 3, errors); // Try a few random three-bit errors;
for (int i = 0; i < 5; i++) {
BitMatrix copy = clone(matrix);
Set<Integer> errors = new TreeSet<Integer>();
while (errors.size() < 3) {
// Quick and dirty way of getting three distinct integers between 1 and n.
errors.add(random.nextInt(orientationPoints.size()));
}
for (int error : errors) {
copy.flip(orientationPoints.get(error).getX(), orientationPoints.get(error).getY());
}
try {
new Detector(makeLarger(copy, 3)).detect(false);
fail("Should not reach here");
} catch (NotFoundException expected) { }
} }
} }
} }
@ -104,25 +123,55 @@ public final class DetectorTest extends Assert {
return output; return output;
} }
// Returns a list of the four rotations of the BitMatrix. The identity rotation is // Returns a list of the four rotations of the BitMatrix.
// explicitly a copy, so that it can be modified without affecting the original matrix.
private static List<BitMatrix> getRotations(BitMatrix input) { private static List<BitMatrix> getRotations(BitMatrix input) {
BitMatrix matrix0 = input;
BitMatrix matrix90 = rotateRight(input);
BitMatrix matrix180 = rotateRight(matrix90);
BitMatrix matrix270 = rotateRight(matrix180);
return Arrays.asList(matrix0, matrix90, matrix180, matrix270);
}
// Rotates a square BitMatrix to the right by 90 degrees
private static BitMatrix rotateRight(BitMatrix input) {
int width = input.getWidth(); int width = input.getWidth();
BitMatrix matrix0 = new BitMatrix(width); BitMatrix result = new BitMatrix(width);
BitMatrix matrix90 = new BitMatrix(width);
BitMatrix matrix180 = new BitMatrix(width);
BitMatrix matrix270 = new BitMatrix(width);
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
for (int y = 0; y < width; y++) { for (int y = 0; y < width; y++) {
if (input.get(x, y)) { if (input.get(x,y)) {
matrix0.set(x, y); result.set(y, width - x - 1);
matrix90.set(y, width - x - 1);
matrix180.set(width - x - 1, width - y - 1);
matrix270.set(width - y - 1, x);
} }
} }
} }
return Arrays.asList(matrix0, matrix90, matrix180, matrix270); return result;
}
// Returns the transpose of a bit matrix, which is equivalent to rotating the
// matrix to the right, and then flipping it left-to-right
private static BitMatrix transpose(BitMatrix input) {
int width = input.getWidth();
BitMatrix result = new BitMatrix(width);
for (int x = 0; x < width; x++) {
for (int y = 0; y < width; y++) {
if (input.get(x, y)) {
result.set(y, x);
}
}
}
return result;
}
private static BitMatrix clone(BitMatrix input) {
int width = input.getWidth();
BitMatrix result = new BitMatrix(width);
for (int x = 0; x < width; x++) {
for (int y = 0; y < width; y++) {
if (input.get(x,y)) {
result.set(x,y);
}
}
}
return result;
} }
private static List<Point> getOrientationPoints(AztecCode code) { private static List<Point> getOrientationPoints(AztecCode code) {

View file

@ -16,6 +16,19 @@
package com.google.zxing.aztec.encoder; package com.google.zxing.aztec.encoder;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.FormatException;
import com.google.zxing.ResultPoint;
import com.google.zxing.aztec.AztecDetectorResult;
import com.google.zxing.aztec.AztecWriter;
import com.google.zxing.aztec.decoder.Decoder;
import com.google.zxing.common.BitArray;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.DecoderResult;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.EnumMap; import java.util.EnumMap;
@ -23,20 +36,6 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.google.zxing.FormatException;
import com.google.zxing.aztec.AztecWriter;
import org.junit.Assert;
import org.junit.Test;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.ResultPoint;
import com.google.zxing.aztec.AztecDetectorResult;
import com.google.zxing.aztec.decoder.Decoder;
import com.google.zxing.common.BitArray;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.DecoderResult;
/** /**
* Aztec 2D generator unit tests. * Aztec 2D generator unit tests.
* *
@ -139,7 +138,8 @@ public final class EncoderTest extends Assert {
String data = "In ut magna vel mauris malesuada"; String data = "In ut magna vel mauris malesuada";
AztecWriter writer = new AztecWriter(); AztecWriter writer = new AztecWriter();
BitMatrix matrix = writer.encode(data, BarcodeFormat.AZTEC, 0, 0); BitMatrix matrix = writer.encode(data, BarcodeFormat.AZTEC, 0, 0);
AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1), Encoder.DEFAULT_EC_PERCENT); AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1),
Encoder.DEFAULT_EC_PERCENT, Encoder.DEFAULT_AZTEC_LAYERS);
BitMatrix expectedMatrix = aztec.getMatrix(); BitMatrix expectedMatrix = aztec.getMatrix();
assertEquals(matrix, expectedMatrix); assertEquals(matrix, expectedMatrix);
} }
@ -341,16 +341,21 @@ public final class EncoderTest extends Assert {
// Test the output generated by Binary/Switch, particularly near the // Test the output generated by Binary/Switch, particularly near the
// places where the encoding changes: 31, 62, and 2047+31=2078 // places where the encoding changes: 31, 62, and 2047+31=2078
for (int i : new int[] { 1, 2, 3, 10, 29, 30, 31, 32, 33, for (int i : new int[] { 1, 2, 3, 10, 29, 30, 31, 32, 33,
60, 61, 62, 63, 64, 2076, 2077, 2078, 2079, 2080, 3000}) { 60, 61, 62, 63, 64, 2076, 2077, 2078, 2079, 2080, 2100 }) {
// This is the expected length of a binary string of length "i" // This is the expected length of a binary string of length "i"
int expectedLength = (8 * i) + int expectedLength = (8 * i) +
( (i <= 31) ? 10 : (i <= 62) ? 20 : (i <= 2078) ? 21 : 31); ( (i <= 31) ? 10 : (i <= 62) ? 20 : (i <= 2078) ? 21 : 31);
// Verify that we are correct about the length. // Verify that we are correct about the length.
testHighLevelEncodeString(sb.substring(0, i), expectedLength); testHighLevelEncodeString(sb.substring(0, i), expectedLength);
// A lower case letter at the beginning will be merged into binary mode if (i != 1 && i != 32 && i != 2079) {
testHighLevelEncodeString('a' + sb.substring(0, i - 1), expectedLength); // The addition of an 'a' at the beginning or end gets merged into the binary code
// A lower case letter at the end will also be merged into binary mode // in those cases where adding another binary character only adds 8 or 9 bits to the result.
testHighLevelEncodeString(sb.substring(0, i - 1) + 'a', expectedLength); // So we exclude the border cases i=1,32,2079
// A lower case letter at the beginning will be merged into binary mode
testHighLevelEncodeString('a' + sb.substring(0, i - 1), expectedLength);
// A lower case letter at the end will also be merged into binary mode
testHighLevelEncodeString(sb.substring(0, i - 1) + 'a', expectedLength);
}
// A lower case letter at both ends will enough to latch us into LOWER. // A lower case letter at both ends will enough to latch us into LOWER.
testHighLevelEncodeString('a' + sb.substring(0, i) + 'b', expectedLength + 15); testHighLevelEncodeString('a' + sb.substring(0, i) + 'b', expectedLength + 15);
} }
@ -379,10 +384,57 @@ public final class EncoderTest extends Assert {
"...X. XXXXX ..X.. X....... ..X.XXX. ..X..... X......."); "...X. XXXXX ..X.. X....... ..X.XXX. ..X..... X.......");
} }
@Test
public void testUserSpecifiedLayers() throws Exception {
byte[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(LATIN_1);
AztecCode aztec = Encoder.encode(alphabet, 25, -2);
assertEquals(2, aztec.getLayers());
assertTrue(aztec.isCompact());
aztec = Encoder.encode(alphabet, 25, 32);
assertEquals(32, aztec.getLayers());
assertFalse(aztec.isCompact());
try {
Encoder.encode(alphabet, 25, 33);
fail("Encode should have failed. No such thing as 33 layers");
} catch (IllegalArgumentException expected) {}
try {
Encoder.encode(alphabet, 25, -1);
fail("Encode should have failed. Text can't fit in 1-layer compact");
} catch (IllegalArgumentException expected) {}
}
@Test
public void testBorderCompact4Case() throws Exception {
// Compact(4) con hold 608 bits of information, but at most 504 can be data. Rest must
// be error correction
String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
// encodes as 26 * 5 * 4 = 520 bits of data
String alphabet4 = alphabet + alphabet + alphabet + alphabet;
byte[] data = alphabet4.getBytes(LATIN_1);
try {
Encoder.encode(data, 0, -4);
fail("Encode should have failed. Text can't fit in 1-layer compact");
} catch (IllegalArgumentException expected) {}
// If we just try to encode it normally, it will go to a non-compact 4 layer
AztecCode aztecCode = Encoder.encode(data, 0, Encoder.DEFAULT_AZTEC_LAYERS);
assertFalse(aztecCode.isCompact());
assertEquals(4, aztecCode.getLayers());
// But shortening the string to 100 bytes (500 bits of data), compact works fine, even if we
// include more error checking.
aztecCode = Encoder.encode(alphabet4.substring(0, 100).getBytes(LATIN_1), 10, Encoder.DEFAULT_AZTEC_LAYERS);
assertTrue(aztecCode.isCompact());
assertEquals(4, aztecCode.getLayers());
}
// Helper routines // Helper routines
private static void testEncode(String data, boolean compact, int layers, String expected) throws Exception { private static void testEncode(String data, boolean compact, int layers, String expected) throws Exception {
AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1), 33); AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1), 33, Encoder.DEFAULT_AZTEC_LAYERS);
assertEquals("Unexpected symbol format (compact)", compact, aztec.isCompact()); assertEquals("Unexpected symbol format (compact)", compact, aztec.isCompact());
assertEquals("Unexpected nr. of layers", layers, aztec.getLayers()); assertEquals("Unexpected nr. of layers", layers, aztec.getLayers());
BitMatrix matrix = aztec.getMatrix(); BitMatrix matrix = aztec.getMatrix();
@ -390,7 +442,7 @@ public final class EncoderTest extends Assert {
} }
private static void testEncodeDecode(String data, boolean compact, int layers) throws Exception { private static void testEncodeDecode(String data, boolean compact, int layers) throws Exception {
AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1), 25); AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1), 25, Encoder.DEFAULT_AZTEC_LAYERS);
assertEquals("Unexpected symbol format (compact)", compact, aztec.isCompact()); assertEquals("Unexpected symbol format (compact)", compact, aztec.isCompact());
assertEquals("Unexpected nr. of layers", layers, aztec.getLayers()); assertEquals("Unexpected nr. of layers", layers, aztec.getLayers());
BitMatrix matrix = aztec.getMatrix(); BitMatrix matrix = aztec.getMatrix();
@ -422,7 +474,8 @@ public final class EncoderTest extends Assert {
hints.put(EncodeHintType.ERROR_CORRECTION, eccPercent); hints.put(EncodeHintType.ERROR_CORRECTION, eccPercent);
AztecWriter writer = new AztecWriter(); AztecWriter writer = new AztecWriter();
BitMatrix matrix = writer.encode(data, BarcodeFormat.AZTEC, 0, 0, hints); BitMatrix matrix = writer.encode(data, BarcodeFormat.AZTEC, 0, 0, hints);
AztecCode aztec = Encoder.encode(data.getBytes(Charset.forName(charset)), eccPercent); AztecCode aztec = Encoder.encode(data.getBytes(Charset.forName(charset)), eccPercent,
Encoder.DEFAULT_AZTEC_LAYERS);
assertEquals("Unexpected symbol format (compact)", compact, aztec.isCompact()); assertEquals("Unexpected symbol format (compact)", compact, aztec.isCompact());
assertEquals("Unexpected nr. of layers", layers, aztec.getLayers()); assertEquals("Unexpected nr. of layers", layers, aztec.getLayers());
BitMatrix matrix2 = aztec.getMatrix(); BitMatrix matrix2 = aztec.getMatrix();
@ -490,11 +543,11 @@ public final class EncoderTest extends Assert {
assertEquals(s, Decoder.highLevelDecode(toBooleanArray(bits))); assertEquals(s, Decoder.highLevelDecode(toBooleanArray(bits)));
} }
private static void testHighLevelEncodeString(String s, int receivedBits) { private static void testHighLevelEncodeString(String s, int expectedReceivedBits) {
BitArray bits = new HighLevelEncoder(s.getBytes(LATIN_1)).encode(); BitArray bits = new HighLevelEncoder(s.getBytes(LATIN_1)).encode();
int receivedBitCount = bits.toString().replace(" ", "").length(); int receivedBitCount = bits.toString().replace(" ", "").length();
assertEquals("highLevelEncode() failed for input string: " + s, receivedBitCount, receivedBitCount); assertEquals("highLevelEncode() failed for input string: " + s,
expectedReceivedBits, receivedBitCount);
assertEquals(s, Decoder.highLevelDecode(toBooleanArray(bits))); assertEquals(s, Decoder.highLevelDecode(toBooleanArray(bits)));
} }
} }