From 228f3d20cac2b228c4440b85be884229faf55429 Mon Sep 17 00:00:00 2001 From: "srowen@gmail.com" Date: Tue, 18 Jun 2013 20:40:01 +0000 Subject: [PATCH] AztecWriter improvements from fyellin git-svn-id: https://zxing.googlecode.com/svn/trunk@2822 59b500cc-1b3d-0410-9834-0bbf25fbcc57 --- .../com/google/zxing/aztec/AztecWriter.java | 41 +- .../google/zxing/aztec/decoder/Decoder.java | 377 ++++++------------ .../google/zxing/aztec/detector/Detector.java | 372 ++++++++--------- .../google/zxing/aztec/encoder/Encoder.java | 143 +++---- .../zxing/aztec/detector/DetectorTest.java | 142 +++++++ 5 files changed, 522 insertions(+), 553 deletions(-) create mode 100644 core/test/src/com/google/zxing/aztec/detector/DetectorTest.java diff --git a/core/src/com/google/zxing/aztec/AztecWriter.java b/core/src/com/google/zxing/aztec/AztecWriter.java index 4e304c182..9e89e84c0 100644 --- a/core/src/com/google/zxing/aztec/AztecWriter.java +++ b/core/src/com/google/zxing/aztec/AztecWriter.java @@ -32,7 +32,7 @@ public final class AztecWriter implements Writer { @Override public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) { - return encode(contents, format, DEFAULT_CHARSET, Encoder.DEFAULT_EC_PERCENT); + return encode(contents, format, width, height, DEFAULT_CHARSET, Encoder.DEFAULT_EC_PERCENT); } @Override @@ -41,16 +41,51 @@ public final class AztecWriter implements Writer { Number eccPercent = hints == null ? null : (Number) hints.get(EncodeHintType.ERROR_CORRECTION); return encode(contents, format, + width, + height, charset == null ? DEFAULT_CHARSET : Charset.forName(charset), eccPercent == null ? Encoder.DEFAULT_EC_PERCENT : eccPercent.intValue()); } - private static BitMatrix encode(String contents, BarcodeFormat format, Charset charset, int eccPercent) { + private static BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Charset charset, + int eccPercent) { if (format != BarcodeFormat.AZTEC) { throw new IllegalArgumentException("Can only encode AZTEC, but got " + format); } AztecCode aztec = Encoder.encode(contents.getBytes(charset), eccPercent); - return aztec.getMatrix(); + return renderResult(aztec, width, height); + } + + private static BitMatrix renderResult(AztecCode code, int width, int height) { + BitMatrix input = code.getMatrix(); + if (input == null) { + throw new IllegalStateException(); + } + int inputWidth = input.getWidth(); + int inputHeight = input.getHeight(); + int outputWidth = Math.max(width, inputWidth); + int outputHeight = Math.max(height, inputHeight); + + int multiple = Math.min(outputWidth / inputWidth, outputHeight / inputHeight); + int leftPadding = (outputWidth - (inputWidth * multiple)) / 2; + int topPadding = (outputHeight - (inputHeight * multiple)) / 2; + + BitMatrix output = new BitMatrix(outputWidth, outputHeight); + + for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) { + // Write the contents of this row of the barcode + for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) { + if (input.get(inputX, inputY)) { + output.setRegion(outputX, outputY, multiple, multiple); + } + } + } + + return output; } } diff --git a/core/src/com/google/zxing/aztec/decoder/Decoder.java b/core/src/com/google/zxing/aztec/decoder/Decoder.java index b2293bf7a..2dc1973ed 100644 --- a/core/src/com/google/zxing/aztec/decoder/Decoder.java +++ b/core/src/com/google/zxing/aztec/decoder/Decoder.java @@ -24,6 +24,8 @@ import com.google.zxing.common.reedsolomon.GenericGF; import com.google.zxing.common.reedsolomon.ReedSolomonDecoder; import com.google.zxing.common.reedsolomon.ReedSolomonException; +import java.util.Arrays; + /** *

The main class which implements Aztec Code decoding -- as opposed to locating and extracting * the Aztec Code from an image.

@@ -41,24 +43,6 @@ public final class Decoder { BINARY } - private static final int[] NB_BITS_COMPACT = { - 0, 104, 240, 408, 608 - }; - - private static final int[] NB_BITS = { - 0, 128, 288, 480, 704, 960, 1248, 1568, 1920, 2304, 2720, 3168, 3648, 4160, 4704, 5280, 5888, 6528, - 7200, 7904, 8640, 9408, 10208, 11040, 11904, 12800, 13728, 14688, 15680, 16704, 17760, 18848, 19968 - }; - - private static final int[] NB_DATABLOCK_COMPACT = { - 0, 17, 40, 51, 76 - }; - - private static final int[] NB_DATABLOCK = { - 0, 21, 48, 60, 88, 120, 156, 196, 240, 230, 272, 316, 364, 416, 470, 528, 588, 652, 720, 790, 864, - 940, 1020, 920, 992, 1066, 1144, 1224, 1306, 1392, 1480, 1570, 1664 - }; - private static final String[] UPPER_TABLE = { "CTRL_PS", " ", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "CTRL_LL", "CTRL_ML", "CTRL_DL", "CTRL_BS" @@ -84,148 +68,82 @@ public final class Decoder { "CTRL_PS", " ", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ",", ".", "CTRL_UL", "CTRL_US" }; - private int numCodewords; - private int codewordSize; private AztecDetectorResult ddata; - private int invertedBitCount; public DecoderResult decode(AztecDetectorResult detectorResult) throws FormatException { ddata = detectorResult; BitMatrix matrix = detectorResult.getBits(); - - if (!ddata.isCompact()) { - matrix = removeDashedLines(ddata.getBits()); - } - boolean[] rawbits = extractBits(matrix); - boolean[] correctedBits = correctBits(rawbits); - String result = getEncodedData(correctedBits); - return new DecoderResult(null, result, null, null); } + // This method is used for testing the high-level encoder + public static String highLevelDecode(boolean[] correctedBits) { + return getEncodedData(correctedBits); + } + /** * Gets the string encoded in the aztec code bits * * @return the decoded string - * @throws FormatException if the input is not valid */ - private String getEncodedData(boolean[] correctedBits) throws FormatException { - int endIndex = codewordSize * ddata.getNbDatablocks() - invertedBitCount; - if (endIndex > correctedBits.length) { - throw FormatException.getFormatInstance(); - } - return getEncodedData(correctedBits, endIndex); - } - - // This method is used for testing the high-level encoder - public static String highLevelDecode(boolean[] correctedBits) { - return getEncodedData(correctedBits, correctedBits.length); - } - - private static String getEncodedData(boolean[] correctedBits, int endIndex) { - Table lastTable = Table.UPPER; - Table table = Table.UPPER; - int startIndex = 0; + private static String getEncodedData(boolean[] correctedBits) { + int endIndex = correctedBits.length; + Table latchTable = Table.UPPER; // table most recently latched to + Table shiftTable = Table.UPPER; // table to use for the next read StringBuilder result = new StringBuilder(20); - boolean end = false; - boolean shift = false; - boolean switchShift = false; - boolean binaryShift = false; - - while (!end) { - - if (shift) { - // the table is for the next character only - switchShift = true; - } else { - // save the current table in case next one is a shift - lastTable = table; - } - - int code; - if (binaryShift) { - if (endIndex - startIndex < 5) { + int index = 0; + while (index < endIndex) { + if (shiftTable == Table.BINARY) { + if (endIndex - index < 5) { break; } - - int length = readCode(correctedBits, startIndex, 5); - startIndex += 5; + int length = readCode(correctedBits, index, 5); + index += 5; if (length == 0) { - if (endIndex - startIndex < 11) { + if (endIndex - index < 11) { break; } - - length = readCode(correctedBits, startIndex, 11) + 31; - startIndex += 11; + length = readCode(correctedBits, index, 11) + 31; + index += 11; } for (int charCount = 0; charCount < length; charCount++) { - if (endIndex - startIndex < 8) { - end = true; + if (endIndex - index < 8) { + index = endIndex; // Force outer loop to exit break; } - - code = readCode(correctedBits, startIndex, 8); + int code = readCode(correctedBits, index, 8); result.append((char) code); - startIndex += 8; + index += 8; } - binaryShift = false; + // Go back to whatever mode we had been in + shiftTable = latchTable; } else { - if (table == Table.BINARY) { - if (endIndex - startIndex < 8) { - break; + int size = shiftTable == Table.DIGIT ? 4 : 5; + if (endIndex - index < size) { + break; + } + int code = readCode(correctedBits, index, size); + index += size; + String str = getCharacter(shiftTable, code); + if (str.startsWith("CTRL_")) { + // Table changes + shiftTable = getTable(str.charAt(5)); + if (str.charAt(6) == 'L') { + latchTable = shiftTable; } - code = readCode(correctedBits, startIndex, 8); - startIndex += 8; - - result.append((char) code); - } else { - int size = 5; - - if (table == Table.DIGIT) { - size = 4; - } - - if (endIndex - startIndex < size) { - break; - } - - code = readCode(correctedBits, startIndex, size); - startIndex += size; - - String str = getCharacter(table, code); - if (str.startsWith("CTRL_")) { - // Table changes - table = getTable(str.charAt(5)); - - if (str.charAt(6) == 'S') { - shift = true; - if (str.charAt(5) == 'B') { - binaryShift = true; - } - } - } else { - result.append(str); - } - - + result.append(str); + // Go back to whatever mode we had been in + shiftTable = latchTable; } } - - if (switchShift) { - table = lastTable; - shift = false; - switchShift = false; - } - } return result.toString(); } - /** * gets the table corresponding to the char passed */ @@ -266,7 +184,8 @@ public final class Decoder { case DIGIT: return DIGIT_TABLE[code]; default: - return ""; + // Should not reach here. + throw new IllegalStateException("Bad table"); } } @@ -278,6 +197,7 @@ public final class Decoder { */ private boolean[] correctBits(boolean[] rawbits) throws FormatException { GenericGF gf; + int codewordSize; if (ddata.getNbLayers() <= 2) { codewordSize = 6; @@ -294,30 +214,13 @@ public final class Decoder { } int numDataCodewords = ddata.getNbDatablocks(); - int numECCodewords; - int offset; - - if (ddata.isCompact()) { - offset = NB_BITS_COMPACT[ddata.getNbLayers()] - numCodewords * codewordSize; - numECCodewords = NB_DATABLOCK_COMPACT[ddata.getNbLayers()] - numDataCodewords; - } else { - offset = NB_BITS[ddata.getNbLayers()] - numCodewords * codewordSize; - numECCodewords = NB_DATABLOCK[ddata.getNbLayers()] - numDataCodewords; - } + int numCodewords = rawbits.length / codewordSize; + int offset = rawbits.length % codewordSize; + int numECCodewords = numCodewords - numDataCodewords; int[] dataWords = new int[numCodewords]; - for (int i = 0; i < numCodewords; i++) { - int flag = 1; - for (int j = 1; j <= codewordSize; j++) { - if (rawbits[codewordSize * i + codewordSize - j + offset]) { - dataWords[i] += flag; - } - flag <<= 1; - } - - //if (dataWords[i] >= flag) { - // flag++; - //} + for (int i = 0; i < numCodewords; i++, offset += codewordSize) { + dataWords[i] = readCode(rawbits, offset, codewordSize); } try { @@ -327,48 +230,34 @@ public final class Decoder { throw FormatException.getFormatInstance(); } - offset = 0; - invertedBitCount = 0; - - boolean[] correctedBits = new boolean[numDataCodewords * codewordSize]; + // Now perform the unstuffing operation. + // First, count how many bits are going to be thrown out as stuffing + int mask = (1 << codewordSize) - 1; + int stuffedBits = 0; for (int i = 0; i < numDataCodewords; i++) { - - boolean seriesColor = false; - int seriesCount = 0; - int flag = 1 << (codewordSize - 1); - - for (int j = 0; j < codewordSize; j++) { - - boolean color = (dataWords[i] & flag) == flag; - - if (seriesCount == codewordSize - 1) { - - if (color == seriesColor) { - //bit must be inverted - throw FormatException.getFormatInstance(); - } - - seriesColor = false; - seriesCount = 0; - offset++; - invertedBitCount++; - } else { - - if (seriesColor == color) { - seriesCount++; - } else { - seriesCount = 1; - seriesColor = color; - } - - correctedBits[i * codewordSize + j - offset] = color; - - } - - flag >>>= 1; + int dataWord = dataWords[i]; + if (dataWord == 0 || dataWord == mask) { + throw FormatException.getFormatInstance(); + } else if (dataWord == 1 || dataWord == mask - 1) { + stuffedBits++; } } - + // Now, actually unpack the bits and remove the stuffing + boolean[] correctedBits = new boolean[numDataCodewords * codewordSize - stuffedBits]; + int index = 0; + for (int i = 0; i < numDataCodewords; i++) { + int dataWord = dataWords[i]; + if (dataWord == 1 || dataWord == mask - 1) { + // next codewordSize-1 bits are all zeros or all ones + Arrays.fill(correctedBits, index, index + codewordSize - 1, dataWord > 1); + index += codewordSize - 1; + } else { + for (int bit = codewordSize - 1; bit >= 0; --bit) { + correctedBits[index++] = (dataWord & (1 << bit)) != 0; + } + } + } + assert index == correctedBits.length; return correctedBits; } @@ -376,104 +265,72 @@ public final class Decoder { * Gets the array of bits from an Aztec Code matrix * * @return the array of bits - * @throws FormatException if the matrix is not a valid aztec code */ - private boolean[] extractBits(BitMatrix matrix) throws FormatException { + boolean[] extractBits(BitMatrix matrix) { + boolean compact = ddata.isCompact(); + int layers = ddata.getNbLayers(); + int baseMatrixSize = compact ? 11 + layers * 4 : 14 + layers * 4; // not including alignment lines + int[] alignmentMap = new int[baseMatrixSize]; + boolean[] rawbits = new boolean[totalBitsInLayer(layers, compact)]; - boolean[] rawbits; - if (ddata.isCompact()) { - if (ddata.getNbLayers() > NB_BITS_COMPACT.length) { - throw FormatException.getFormatInstance(); + if (compact) { + for (int i = 0; i < alignmentMap.length; i++) { + alignmentMap[i] = i; } - rawbits = new boolean[NB_BITS_COMPACT[ddata.getNbLayers()]]; - numCodewords = NB_DATABLOCK_COMPACT[ddata.getNbLayers()]; } else { - if (ddata.getNbLayers() > NB_BITS.length) { - throw FormatException.getFormatInstance(); + int matrixSize = baseMatrixSize + 1 + 2 * ((baseMatrixSize / 2 - 1) / 15); + int origCenter = baseMatrixSize / 2; + int center = matrixSize / 2; + for (int i = 0; i < origCenter; i++) { + int newOffset = i + i / 15; + alignmentMap[origCenter - i - 1] = center - newOffset - 1; + alignmentMap[origCenter + i] = center + newOffset + 1; } - rawbits = new boolean[NB_BITS[ddata.getNbLayers()]]; - numCodewords = NB_DATABLOCK[ddata.getNbLayers()]; } - - int layer = ddata.getNbLayers(); - int size = matrix.getHeight(); - int rawbitsOffset = 0; - int matrixOffset = 0; - - while (layer != 0) { - - int flip = 0; - for (int i = 0; i < 2 * size - 4; i++) { - rawbits[rawbitsOffset + i] = matrix.get(matrixOffset + flip, matrixOffset + i / 2); - rawbits[rawbitsOffset + 2 * size - 4 + i] = matrix.get(matrixOffset + i / 2, matrixOffset + size - 1 - flip); - flip = (flip + 1) % 2; + for (int i = 0, rowOffset = 0; i < layers; i++) { + int rowSize = compact ? (layers - i) * 4 + 9 : (layers - i) * 4 + 12; + // The top-left most point of this layer is (not including alignment lines) + int low = i * 2; + // The bottom-right most point of this layer is (not including alignment lines) + int high = baseMatrixSize - 1 - low; + // We pull bits from the two 2 x rowSize columns and two rowSize x 2 rows + for (int j = 0; j < rowSize; j++) { + int columnOffset = j * 2; + for (int k = 0; k < 2; k++) { + // left column + rawbits[rowOffset + columnOffset + k] = + matrix.get(alignmentMap[low + k], alignmentMap[low + j]); + // bottom row + rawbits[rowOffset + 2 * rowSize + columnOffset + k] = + matrix.get(alignmentMap[low + j], alignmentMap[high - k]); + // right column + rawbits[rowOffset + 4 * rowSize + columnOffset + k] = + matrix.get(alignmentMap[high - k], alignmentMap[high - j]); + // top row + rawbits[rowOffset + 6 * rowSize + columnOffset + k] = + matrix.get(alignmentMap[high - j], alignmentMap[low + k]); + } } - - flip = 0; - for (int i = 2 * size + 1; i > 5; i--) { - rawbits[rawbitsOffset + 4 * size - 8 + (2 * size - i) + 1] = - matrix.get(matrixOffset + size - 1 - flip, matrixOffset + i / 2 - 1); - rawbits[rawbitsOffset + 6 * size - 12 + (2 * size - i) + 1] = - matrix.get(matrixOffset + i / 2 - 1, matrixOffset + flip); - flip = (flip + 1) % 2; - } - - matrixOffset += 2; - rawbitsOffset += 8 * size - 16; - layer--; - size -= 4; + rowOffset += rowSize * 8; } - return rawbits; } - /** - * Transforms an Aztec code matrix by removing the control dashed lines - */ - private static BitMatrix removeDashedLines(BitMatrix matrix) { - int nbDashed = 1 + 2 * ((matrix.getWidth() - 1) / 2 / 16); - BitMatrix newMatrix = new BitMatrix(matrix.getWidth() - nbDashed, matrix.getHeight() - nbDashed); - - int nx = 0; - - for (int x = 0; x < matrix.getWidth(); x++) { - - if ((matrix.getWidth() / 2 - x) % 16 == 0) { - continue; - } - - int ny = 0; - for (int y = 0; y < matrix.getHeight(); y++) { - - if ((matrix.getWidth() / 2 - y) % 16 == 0) { - continue; - } - - if (matrix.get(x, y)) { - newMatrix.set(nx, ny); - } - ny++; - } - nx++; - } - - return newMatrix; - } - /** * Reads a code of given length and at given index in an array of bits */ private static int readCode(boolean[] rawbits, int startIndex, int length) { int res = 0; - for (int i = startIndex; i < startIndex + length; i++) { res <<= 1; if (rawbits[i]) { res++; } } - return res; } + private static int totalBitsInLayer(int layers, boolean compact) { + return ((compact ? 88 : 112) + 16 * layers) * layers; + } } diff --git a/core/src/com/google/zxing/aztec/detector/Detector.java b/core/src/com/google/zxing/aztec/detector/Detector.java index fc4403760..8cbe9bb72 100644 --- a/core/src/com/google/zxing/aztec/detector/Detector.java +++ b/core/src/com/google/zxing/aztec/detector/Detector.java @@ -67,9 +67,11 @@ public final class Detector { extractParameters(bullsEyeCorners); // 4. Sample the grid - BitMatrix bits = sampleGrid(image, - bullsEyeCorners[shift%4], bullsEyeCorners[(shift+1)%4], - bullsEyeCorners[(shift+2)%4], bullsEyeCorners[(shift+3)%4]); + BitMatrix bits = sampleGrid(image, + bullsEyeCorners[shift % 4], + bullsEyeCorners[(shift + 1) % 4], + bullsEyeCorners[(shift + 2) % 4], + bullsEyeCorners[(shift + 3) % 4]); // 5. Get the corners of the matrix. ResultPoint[] corners = getMatrixCornerPoints(bullsEyeCorners); @@ -84,89 +86,91 @@ public final class Detector { * @throws NotFoundException in case of too many errors or invalid parameters */ private void extractParameters(ResultPoint[] bullsEyeCorners) throws NotFoundException { - if (!isValid(bullsEyeCorners[0]) || !isValid(bullsEyeCorners[1]) || + if (!isValid(bullsEyeCorners[0]) || !isValid(bullsEyeCorners[1]) || !isValid(bullsEyeCorners[2]) || !isValid(bullsEyeCorners[3])) { - throw NotFoundException.getNotFoundInstance(); - } - int twoCenterLayers = 2 * nbCenterLayers; - // Get the bits around the bull's eye - boolean[] resab = sampleLine(bullsEyeCorners[0], bullsEyeCorners[1], twoCenterLayers+1); - boolean[] resbc = sampleLine(bullsEyeCorners[1], bullsEyeCorners[2], twoCenterLayers+1); - boolean[] rescd = sampleLine(bullsEyeCorners[2], bullsEyeCorners[3], twoCenterLayers+1); - boolean[] resda = sampleLine(bullsEyeCorners[3], bullsEyeCorners[0], twoCenterLayers+1); - - // Determine the orientation of the matrix - if (resab[0] && resab[twoCenterLayers]) { - shift = 0; - } else if (resbc[0] && resbc[twoCenterLayers]) { - shift = 1; - } else if (rescd[0] && rescd[twoCenterLayers]) { - shift = 2; - } else if (resda[0] && resda[twoCenterLayers]) { - shift = 3; - } else { throw NotFoundException.getNotFoundInstance(); } - - //d a - // - //c b - - // Flatten the bits in a single array - boolean[] parameterData; - boolean[] shiftedParameterData; - if (compact) { - shiftedParameterData = new boolean[28]; - for (int i = 0; i < 7; i++) { - shiftedParameterData[i] = resab[2+i]; - shiftedParameterData[i+7] = resbc[2+i]; - shiftedParameterData[i+14] = rescd[2+i]; - shiftedParameterData[i+21] = resda[2+i]; - } - - parameterData = new boolean[28]; - for (int i = 0; i < 28; i++) { - parameterData[i] = shiftedParameterData[(i+shift*7)%28]; - } - } else { - shiftedParameterData = new boolean[40]; - for (int i = 0; i < 11; i++) { - if (i < 5) { - shiftedParameterData[i] = resab[2+i]; - shiftedParameterData[i+10] = resbc[2+i]; - shiftedParameterData[i+20] = rescd[2+i]; - shiftedParameterData[i+30] = resda[2+i]; - } - if (i > 5) { - shiftedParameterData[i-1] = resab[2+i]; - shiftedParameterData[i+9] = resbc[2+i]; - shiftedParameterData[i+19] = rescd[2+i]; - shiftedParameterData[i+29] = resda[2+i]; - } - } - - parameterData = new boolean[40]; - for (int i = 0; i < 40; i++) { - parameterData[i] = shiftedParameterData[(i+shift*10)%40]; + int length = 2 * nbCenterLayers; + // Get the bits around the bull's eye + int[] sides = { + sampleLine(bullsEyeCorners[0], bullsEyeCorners[1], length), // Right side + sampleLine(bullsEyeCorners[1], bullsEyeCorners[2], length), // Bottom + sampleLine(bullsEyeCorners[2], bullsEyeCorners[3], length), // Left side + sampleLine(bullsEyeCorners[3], bullsEyeCorners[0], length) // Top + }; + + // bullsEyeCorners[shift] is the corner of the bulls'eye that has three + // orientation marks. + // sides[shift] is the row/column that goes from the corner with three + // orientation marks to the corner with two. + shift = getRotation(sides, length); + + // Flatten the parameter bits into a single 28- or 40-bit long + long parameterData = 0; + for (int i = 0; i < 4; i++) { + int side = sides[(shift + i) % 4]; + if (compact) { + // Each side of the form ..XXXXXXX. where Xs are parameter data + parameterData <<= 7; + parameterData += (side >> 1) & 0x7F; + } else { + // Each side of the form ..XXXXX.XXXXX. where Xs are parameter data + parameterData <<= 10; + parameterData += ((side >> 2) & (0x1f << 5)) + ((side >> 1) & 0x1F); } } - // corrects the error using RS algorithm - correctParameterData(parameterData, compact); + // Corrects parameter data using RS. Returns just the data portion + // without the error correction. + int correctedData = getCorrectedParameterData(parameterData, compact); - // gets the parameters from the bit array - getParameters(parameterData); + if (compact) { + // 8 bits: 2 bits layers and 6 bits data blocks + nbLayers = (correctedData >> 6) + 1; + nbDataBlocks = (correctedData & 0x3F) + 1; + } else { + // 16 bits: 5 bits layers and 11 bits data blocks + nbLayers = (correctedData >> 11) + 1; + nbDataBlocks = (correctedData & 0x7FF) + 1; + } } - /** - * Gets the Aztec code corners from the bull's eye corners and the parameters. - * - * @param bullsEyeCorners the array of bull's eye 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) throws NotFoundException { - return expandSquare(bullsEyeCorners, 2 * nbCenterLayers, getDimension()); + private int[] expectedCornerBits = { + 0xee0, // 07340 XXX .XX X.. ... + 0x1dc, // 00734 ... XXX .XX X.. + 0x83b, // 04073 X.. ... XXX .XX + 0x707, // 03407 .XX X.. ... XXX + }; + + private int getRotation(int[] sides, int length) throws NotFoundException { + // In a normal pattern, we expect to See + // ** .* D A + // * * + // + // . * + // .. .. C B + // + // Grab the 3 bits from each of the sides the form the locator pattern and concatenate + // into a 12-bit integer. Start with the bit at A + int cornerBits = 0; + for (int side : sides) { + // XX......X where X's are orientation marks + int t = ((side >> (length - 2)) << 1) + (side & 1); + cornerBits = (cornerBits << 3) + t; + } + // Mov the bottom bit to the top, so that the three bits of the locator pattern at A are + // together. cornerBits is now: + // 3 orientation bits at A || 3 orientation bits at B || ... || 3 orientation bits at D + cornerBits = ((cornerBits & 1) << 11) + (cornerBits >> 1); + // The result shift indicates which element of BullsEyeCorners[] goes into the top-left + // corner. Since the four rotation values have a Hamming distance of 8, we + // can easily tolerate two errors. + for (int shift = 0; shift < 4; shift++) { + if (Integer.bitCount(cornerBits ^ expectedCornerBits[shift]) <= 2) { + return shift; + } + } + throw NotFoundException.getNotFoundInstance(); } /** @@ -176,8 +180,7 @@ public final class Detector { * @param compact true if this is a compact Aztec code * @throws NotFoundException if the array contains too many errors */ - private static void correctParameterData(boolean[] parameterData, boolean compact) throws NotFoundException { - + private static int getCorrectedParameterData(long parameterData, boolean compact) throws NotFoundException { int numCodewords; int numDataCodewords; @@ -191,32 +194,22 @@ public final class Detector { int numECCodewords = numCodewords - numDataCodewords; int[] parameterWords = new int[numCodewords]; - - int codewordSize = 4; - for (int i = 0; i < numCodewords; i++) { - int flag = 1; - for (int j = 1; j <= codewordSize; j++) { - if (parameterData[codewordSize*i + codewordSize - j]) { - parameterWords[i] += flag; - } - flag <<= 1; - } + for (int i = numCodewords - 1; i >= 0; --i) { + parameterWords[i] = (int) parameterData & 0xF; + parameterData >>= 4; } - try { ReedSolomonDecoder rsDecoder = new ReedSolomonDecoder(GenericGF.AZTEC_PARAM); rsDecoder.decode(parameterWords, numECCodewords); } catch (ReedSolomonException ignored) { throw NotFoundException.getNotFoundInstance(); } - - for (int i = 0; i < numDataCodewords; i ++) { - int flag = 1; - for (int j = 1; j <= codewordSize; j++) { - parameterData[i*codewordSize+codewordSize-j] = (parameterWords[i] & flag) == flag; - flag <<= 1; - } + // Toss the error correction. Just return the data as an integer + int result = 0; + for (int i = 0; i < numDataCodewords; i++) { + result = (result << 4) + parameterWords[i]; } + return result; } /** @@ -247,9 +240,9 @@ public final class Detector { // //c b - if (nbCenterLayers>2) { - float q = distance(poutd, pouta)*nbCenterLayers/(distance(pind, pina)*(nbCenterLayers+2)); - if ( q < 0.75 || q > 1.25 || !isWhiteOrBlackRectangle(pouta, poutb, poutc, poutd)) { + if (nbCenterLayers > 2) { + float q = distance(poutd, pouta) * nbCenterLayers / (distance(pind, pina) * (nbCenterLayers + 2)); + if (q < 0.75 || q > 1.25 || !isWhiteOrBlackRectangle(pouta, poutb, poutc, poutd)) { break; } } @@ -266,7 +259,7 @@ public final class Detector { throw NotFoundException.getNotFoundInstance(); } - compact = nbCenterLayers==5; + compact = nbCenterLayers == 5; // Expand the square by .5 pixel in each direction so that we're on the border // between the white square and the black square @@ -307,12 +300,12 @@ public final class Detector { // This exception can be in case the initial rectangle is white // In that case, surely in the bull's eye, we try to expand the rectangle. - int cx = image.getWidth()/2; - int cy = image.getHeight()/2; - pointA = getFirstDifferent(new Point(cx+7, cy-7), false, 1, -1).toResultPoint(); - pointB = getFirstDifferent(new Point(cx+7, cy+7), false, 1, 1).toResultPoint(); - pointC = getFirstDifferent(new Point(cx-7, cy+7), false, -1, 1).toResultPoint(); - pointD = getFirstDifferent(new Point(cx-7, cy-7), false, -1, -1).toResultPoint(); + int cx = image.getWidth() / 2; + int cy = image.getHeight() / 2; + pointA = getFirstDifferent(new Point(cx + 7, cy - 7), false, 1, -1).toResultPoint(); + pointB = getFirstDifferent(new Point(cx + 7, cy + 7), false, 1, 1).toResultPoint(); + pointC = getFirstDifferent(new Point(cx - 7, cy + 7), false, -1, 1).toResultPoint(); + pointD = getFirstDifferent(new Point(cx - 7, cy - 7), false, -1, -1).toResultPoint(); } @@ -332,10 +325,10 @@ public final class Detector { } catch (NotFoundException e) { // This exception can be in case the initial rectangle is white // In that case we try to expand the rectangle. - pointA = getFirstDifferent(new Point(cx+7, cy-7), false, 1, -1).toResultPoint(); - pointB = getFirstDifferent(new Point(cx+7, cy+7), false, 1, 1).toResultPoint(); - pointC = getFirstDifferent(new Point(cx-7, cy+7), false, -1, 1).toResultPoint(); - pointD = getFirstDifferent(new Point(cx-7, cy-7), false, -1, -1).toResultPoint(); + pointA = getFirstDifferent(new Point(cx + 7, cy - 7), false, 1, -1).toResultPoint(); + pointB = getFirstDifferent(new Point(cx + 7, cy + 7), false, 1, 1).toResultPoint(); + pointC = getFirstDifferent(new Point(cx - 7, cy + 7), false, -1, 1).toResultPoint(); + pointD = getFirstDifferent(new Point(cx - 7, cy - 7), false, -1, -1).toResultPoint(); } // Recompute the center of the rectangle @@ -345,6 +338,17 @@ public final class Detector { return new Point(cx, cy); } + /** + * Gets the Aztec code corners from the bull's eye corners and the parameters. + * + * @param bullsEyeCorners the array of bull's eye 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) { + return expandSquare(bullsEyeCorners, 2 * nbCenterLayers, getDimension()); + } + /** * Creates a BitMatrix by sampling the provided image. * topLeft, topRight, bottomRight, and bottomLeft are the centers of the squares on the @@ -359,87 +363,50 @@ public final class Detector { GridSampler sampler = GridSampler.getInstance(); int dimension = getDimension(); - float low = dimension/2.0f - nbCenterLayers; - float high = dimension/2.0f + nbCenterLayers; + float low = dimension / 2.0f - nbCenterLayers; + float high = dimension / 2.0f + nbCenterLayers; return sampler.sampleGrid(image, - dimension, - dimension, - low, low, // topleft - high, low, // topright - high, high, // bottomright - low, high, // bottomleft - topLeft.getX(), topLeft.getY(), - topRight.getX(), topRight.getY(), - bottomRight.getX(), bottomRight.getY(), - bottomLeft.getX(), bottomLeft.getY()); - } - - /** - * Sets number of layers and number of data blocks from parameter bits - */ - private void getParameters(boolean[] parameterData) { - - int nbBitsForNbLayers; - int nbBitsForNbDatablocks; - - if (compact) { - nbBitsForNbLayers = 2; - nbBitsForNbDatablocks = 6; - } else { - nbBitsForNbLayers = 5; - nbBitsForNbDatablocks = 11; - } - - for (int i = 0; i < nbBitsForNbLayers; i++) { - nbLayers <<= 1; - if (parameterData[i]) { - nbLayers++; - } - } - - for (int i = nbBitsForNbLayers; i < nbBitsForNbLayers + nbBitsForNbDatablocks; i++) { - nbDataBlocks <<= 1; - if (parameterData[i]) { - nbDataBlocks++; - } - } - - nbLayers++; - nbDataBlocks++; + dimension, + dimension, + low, low, // topleft + high, low, // topright + high, high, // bottomright + low, high, // bottomleft + topLeft.getX(), topLeft.getY(), + topRight.getX(), topRight.getY(), + bottomRight.getX(), bottomRight.getY(), + bottomLeft.getX(), bottomLeft.getY()); } /** - * Samples a line + * Samples a line. * - * @param p1 first point - * @param p2 second point + * @param p1 start point (inclusive) + * @param p2 end point (exclusive) * @param size number of bits - * @return the array of bits + * @return the array of bits as an int (first bit is high-order bit of result) */ - private boolean[] sampleLine(ResultPoint p1, ResultPoint p2, int size) { - - boolean[] res = new boolean[size]; - float d = distance(p1,p2); - float moduleSize = d/(size-1); - float dx = moduleSize*(p2.getX() - p1.getX())/d; - float dy = moduleSize*(p2.getY() - p1.getY())/d; + private int sampleLine(ResultPoint p1, ResultPoint p2, int size) { + int result = 0; + float d = distance(p1, p2); + float moduleSize = d / size; float px = p1.getX(); float py = p1.getY(); - + float dx = moduleSize * (p2.getX() - p1.getX()) / d; + float dy = moduleSize * (p2.getY() - p1.getY()) / d; for (int i = 0; i < size; i++) { - res[i] = image.get(MathUtils.round(px), MathUtils.round(py)); - px += dx; - py += dy; + if (image.get(MathUtils.round(px + i * dx), MathUtils.round(py + i * dy))) { + result |= 1 << (size - i - 1); + } } - - return res; + return result; } /** * @return true if the border of the rectangle passed in parameter is compound of white points only - * or black points only + * or black points only */ private boolean isWhiteOrBlackRectangle(Point p1, Point p2, @@ -448,10 +415,10 @@ public final class Detector { int corr = 3; - p1 = new Point(p1.getX() -corr, p1.getY() +corr); - p2 = new Point(p2.getX() -corr, p2.getY() -corr); - p3 = new Point(p3.getX() +corr, p3.getY() -corr); - p4 = new Point(p4.getX() +corr, p4.getY() +corr); + p1 = new Point(p1.getX() - corr, p1.getY() + corr); + p2 = new Point(p2.getX() - corr, p2.getY() - corr); + p3 = new Point(p3.getX() + corr, p3.getY() - corr); + p4 = new Point(p4.getX() + corr, p4.getY() + corr); int cInit = getColor(p4, p1); @@ -483,9 +450,9 @@ public final class Detector { * @return 1 if segment more than 90% black, -1 if segment is more than 90% white, 0 else */ private int getColor(Point p1, Point p2) { - float d = distance(p1,p2); - float dx = (p2.getX() - p1.getX())/d; - float dy = (p2.getY() - p1.getY())/d; + float d = distance(p1, p2); + float dx = (p2.getX() - p1.getX()) / d; + float dy = (p2.getY() - p1.getY()) / d; int error = 0; float px = p1.getX(); @@ -494,8 +461,8 @@ public final class Detector { boolean colorModel = image.get(p1.getX(), p1.getY()); for (int i = 0; i < d; i++) { - px+=dx; - py+=dy; + px += dx; + py += dy; if (image.get(MathUtils.round(px), MathUtils.round(py)) != colorModel) { error++; } @@ -514,40 +481,39 @@ public final class Detector { * Gets the coordinate of the first point with a different color in the given direction */ private Point getFirstDifferent(Point init, boolean color, int dx, int dy) { - int x = init.getX() +dx; - int y = init.getY() +dy; + int x = init.getX() + dx; + int y = init.getY() + dy; - while(isValid(x,y) && image.get(x,y) == color) { - x+=dx; - y+=dy; + while (isValid(x, y) && image.get(x, y) == color) { + x += dx; + y += dy; } - x-=dx; - y-=dy; + x -= dx; + y -= dy; - while(isValid(x,y) && image.get(x, y) == color) { - x+=dx; + while (isValid(x, y) && image.get(x, y) == color) { + x += dx; } - x-=dx; + x -= dx; - while(isValid(x,y) && image.get(x, y) == color) { - y+=dy; + while (isValid(x, y) && image.get(x, y) == color) { + y += dy; } - y-=dy; + y -= dy; - return new Point(x,y); + return new Point(x, y); } /** * Expand the square represented by the corner points by pushing out equally in all directions * - * @param cornerPoints the corners of the square, which has the bull's eye at its center + * @param cornerPoints the corners of the square, which has the bull's eye at its center * @param oldSide the original length of the side of the square in the target bit matrix * @param newSide the new length of the size of the square in the target bit matrix * @return the corners of the expanded square */ - private ResultPoint[] expandSquare(ResultPoint[] cornerPoints, float oldSide, float newSide) - throws NotFoundException { + private static ResultPoint[] expandSquare(ResultPoint[] cornerPoints, float oldSide, float newSide) { float ratio = newSide / (2 * oldSide); float dx = cornerPoints[0].getX() - cornerPoints[2].getX(); float dy = cornerPoints[0].getY() - cornerPoints[2].getY(); @@ -564,7 +530,7 @@ public final class Detector { ResultPoint result1 = new ResultPoint(centerx + ratio * dx, centery + ratio * dy); ResultPoint result3 = new ResultPoint(centerx - ratio * dx, centery - ratio * dy); - return new ResultPoint[] { result0, result1, result2, result3 }; + return new ResultPoint[]{result0, result1, result2, result3}; } private boolean isValid(int x, int y) { @@ -592,7 +558,7 @@ public final class Detector { if (nbLayers <= 4) { return 4 * nbLayers + 15; } - return 4 * nbLayers + 2 * ((nbLayers-4)/8 + 1) + 15; + return 4 * nbLayers + 2 * ((nbLayers - 4) / 8 + 1) + 15; } static final class Point { diff --git a/core/src/com/google/zxing/aztec/encoder/Encoder.java b/core/src/com/google/zxing/aztec/encoder/Encoder.java index 9d18618bc..acbe7c736 100644 --- a/core/src/com/google/zxing/aztec/encoder/Encoder.java +++ b/core/src/com/google/zxing/aztec/encoder/Encoder.java @@ -29,20 +29,8 @@ import com.google.zxing.common.reedsolomon.ReedSolomonEncoder; public final class Encoder { public static final int DEFAULT_EC_PERCENT = 33; // default minimal percentage of error check words - private static final int[] NB_BITS; // total bits per compact symbol for a given number of layers - private static final int[] NB_BITS_COMPACT; // total bits per full symbol for a given number of layers + private static final int MAX_NB_BITS = 32; - static { - NB_BITS_COMPACT = new int[5]; - for (int i = 1; i < NB_BITS_COMPACT.length; i++) { - NB_BITS_COMPACT[i] = (88 + 16 * i) * i; - } - NB_BITS = new int[33]; - for (int i = 1; i < NB_BITS.length; i++) { - NB_BITS[i] = (112 + 16 * i) * i; - } - } - 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, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 @@ -65,8 +53,8 @@ public final class Encoder { * Encodes the given binary content as an Aztec symbol * * @param data input data string - * @param minECCPercent minimal percentange of error check words (According to ISO/IEC 24778:2008, - * a minimum of 23% + 3 words is recommended) + * @param minECCPercent minimal percentage of error check words (According to ISO/IEC 24778:2008, + * a minimum of 23% + 3 words is recommended) * @return Aztec symbol matrix with metadata */ public static AztecCode encode(byte[] data, int minECCPercent) { @@ -77,56 +65,46 @@ public final class Encoder { // stuff bits and choose symbol size int eccBits = bits.getSize() * minECCPercent / 100 + 11; int totalSizeBits = bits.getSize() + eccBits; + boolean compact; int layers; + int totalBitsInLayer; int wordSize = 0; - int totalSymbolBits = 0; BitArray stuffedBits = null; - for (layers = 1; layers < NB_BITS_COMPACT.length; layers++) { - if (NB_BITS_COMPACT[layers] >= totalSizeBits) { - if (wordSize != WORD_SIZE[layers]) { - wordSize = WORD_SIZE[layers]; - stuffedBits = stuffBits(bits, wordSize); - } - totalSymbolBits = NB_BITS_COMPACT[layers]; - if (stuffedBits.getSize() + eccBits <= NB_BITS_COMPACT[layers]) { - break; - } + // 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"); } - } - boolean compact = true; - if (layers == NB_BITS_COMPACT.length) { - compact = false; - for (layers = 1; layers < NB_BITS.length; layers++) { - if (NB_BITS[layers] >= totalSizeBits) { - if (wordSize != WORD_SIZE[layers]) { - wordSize = WORD_SIZE[layers]; - stuffedBits = stuffBits(bits, wordSize); - } - totalSymbolBits = NB_BITS[layers]; - if (stuffedBits.getSize() + eccBits <= NB_BITS[layers]) { - break; - } - } + 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 (stuffedBits.getSize() + eccBits <= usableBitsInLayers) { + break; } - } - if (layers == NB_BITS.length) { - throw new IllegalArgumentException("Data too large for an Aztec code"); } - // pad the end - int messageSizeInWords = (stuffedBits.getSize() + wordSize - 1) / wordSize; - for (int i = messageSizeInWords * wordSize - stuffedBits.getSize(); i > 0; i--) { - stuffedBits.appendBit(true); - } + int messageSizeInWords = stuffedBits.getSize() / wordSize; // generate check words ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize)); - int totalSizeInFullWords = totalSymbolBits / wordSize; - int[] messageWords = bitsToWords(stuffedBits, wordSize, totalSizeInFullWords); - rs.encode(messageWords, totalSizeInFullWords - messageSizeInWords); + 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 = totalSymbolBits % wordSize; + int startPad = totalBitsInLayer % wordSize; BitArray messageBits = new BitArray(); messageBits.appendBits(0, startPad); for (int messageWord : messageWords) { @@ -158,7 +136,7 @@ public final class Encoder { } BitMatrix matrix = new BitMatrix(matrixSize); - // draw mode and data bits + // draw data bits for (int i = 0, rowOffset = 0; i < layers; i++) { int rowSize = compact ? (layers - i) * 4 + 9 : (layers - i) * 4 + 12; for (int j = 0; j < rowSize; j++) { @@ -180,6 +158,8 @@ public final class Encoder { } rowOffset += rowSize * 8; } + + // draw mode message drawModeMessage(matrix, compact, matrixSize, modeMessage); // draw alignment marks @@ -238,49 +218,50 @@ public final class Encoder { } private static void drawModeMessage(BitMatrix matrix, boolean compact, int matrixSize, BitArray modeMessage) { + int center = matrixSize / 2; if (compact) { for (int i = 0; i < 7; i++) { + int offset = center - 3 + i; if (modeMessage.get(i)) { - matrix.set(matrixSize / 2 - 3 + i, matrixSize / 2 - 5); + matrix.set(offset, center - 5); } if (modeMessage.get(i + 7)) { - matrix.set(matrixSize / 2 + 5, matrixSize / 2 - 3 + i); + matrix.set(center + 5, offset); } if (modeMessage.get(20 - i)) { - matrix.set(matrixSize / 2 - 3 + i, matrixSize / 2 + 5); + matrix.set(offset, center + 5); } if (modeMessage.get(27 - i)) { - matrix.set(matrixSize / 2 - 5, matrixSize / 2 - 3 + i); + matrix.set(center - 5, offset); } } } else { for (int i = 0; i < 10; i++) { + int offset = center - 5 + i + i / 5; if (modeMessage.get(i)) { - matrix.set(matrixSize / 2 - 5 + i + i / 5, matrixSize / 2 - 7); + matrix.set(offset, center - 7); } if (modeMessage.get(i + 10)) { - matrix.set(matrixSize / 2 + 7, matrixSize / 2 - 5 + i + i / 5); + matrix.set(center + 7, offset); } if (modeMessage.get(29 - i)) { - matrix.set(matrixSize / 2 - 5 + i + i / 5, matrixSize / 2 + 7); + matrix.set(offset, center + 7); } if (modeMessage.get(39 - i)) { - matrix.set(matrixSize / 2 - 7, matrixSize / 2 - 5 + i + i / 5); + matrix.set(center - 7, offset); } } } } - private static BitArray generateCheckWords(BitArray stuffedBits, int totalSymbolBits, int wordSize) { - int messageSizeInWords = (stuffedBits.getSize() + wordSize - 1) / wordSize; - for (int i = messageSizeInWords * wordSize - stuffedBits.getSize(); i > 0; i--) { - stuffedBits.appendBit(true); - } + private static BitArray generateCheckWords(BitArray stuffedBits, int totalBits, int wordSize) { + // stuffedBits is guaranteed to be a multiple of the wordSize, so no padding needed + int messageSizeInWords = stuffedBits.getSize() / wordSize; ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize)); - int totalSizeInFullWords = totalSymbolBits / wordSize; - int[] messageWords = bitsToWords(stuffedBits, wordSize, totalSizeInFullWords); - rs.encode(messageWords, totalSizeInFullWords - messageSizeInWords); - int startPad = totalSymbolBits % wordSize; + int totalWords = totalBits / wordSize; + int[] messageWords = bitsToWords(stuffedBits, wordSize, totalWords); + rs.encode(messageWords, totalWords - messageSizeInWords); + int startPad = totalBits % wordSize; BitArray messageBits = new BitArray(); messageBits.appendBits(0, startPad); for (int messageWord : messageWords) { @@ -343,22 +324,10 @@ public final class Encoder { out.appendBits(word, wordSize); } } - - // 2. pad last word to wordSize - n = out.getSize(); - int remainder = n % wordSize; - if (remainder != 0) { - int j = 1; - for (int i = 0; i < remainder; i++) { - if (!out.get(n - 1 - i)) { - j = 0; - } - } - for (int i = remainder; i < wordSize - 1; i++) { - out.appendBit(true); - } - out.appendBit(j == 0); - } return out; } + + private static int totalBitsInLayer(int layers, boolean compact) { + return ((compact ? 88 : 112) + 16 * layers) * layers; + } } diff --git a/core/test/src/com/google/zxing/aztec/detector/DetectorTest.java b/core/test/src/com/google/zxing/aztec/detector/DetectorTest.java new file mode 100644 index 000000000..d4982c672 --- /dev/null +++ b/core/test/src/com/google/zxing/aztec/detector/DetectorTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.aztec.AztecDetectorResult; +import com.google.zxing.aztec.detector.Detector.Point; +import com.google.zxing.aztec.encoder.AztecCode; +import com.google.zxing.aztec.encoder.Encoder; +import com.google.zxing.common.BitMatrix; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +/** + * Tests for the Detector + * + * @author Frank Yellin + */ +public final class DetectorTest extends Assert { + + private static final Charset LATIN_1 = Charset.forName("ISO-8859-1"); + + @Test + public void testErrorInParameterLocatorZeroZero() throws Exception { + // Layers=1, CodeWords=1. So the parameter info and its Reed-Solomon info + // will be completely zero! + testErrorInParameterLocator("X"); + } + + @Test + public void testErrorInParameterLocatorCompact() throws Exception { + testErrorInParameterLocator("This is an example Aztec symbol for Wikipedia."); + } + + @Test + public void testErrorInParameterLocatorNotCompact() throws Exception { + String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYabcdefghijklmnopqrstuvwxyz"; + testErrorInParameterLocator(alphabet + alphabet + alphabet); + } + + // Test that we can tolerate errors in the parameter locator bits + private static void testErrorInParameterLocator(String data) throws Exception { + AztecCode aztec = Encoder.encode(data.getBytes(LATIN_1), 25); + int layers = aztec.getLayers(); + boolean compact = aztec.isCompact(); + List orientationPoints = getOrientationPoints(aztec); + Random random = new Random(aztec.getMatrix().hashCode()); // random, but repeatable + for (BitMatrix matrix : getRotations(aztec.getMatrix())) { + // Each time through this loop, we reshuffle the corners, to get a different set of errors + Collections.shuffle(orientationPoints, random); + for (int errors = 1; errors <= 3; errors++) { + // Add another error to one of the parameter locator bits + matrix.flip(orientationPoints.get(errors).getX(), orientationPoints.get(errors).getY()); + try { + // The detector can't yet deal with bitmaps in which each square is only 1x1 pixel. + // We zoom it larger. + AztecDetectorResult r = new Detector(makeLarger(matrix, 3)).detect(); + if (errors < 3) { + assertNotNull(r); + assertEquals(r.getNbLayers(), layers); + assertEquals(r.isCompact(), compact); + } else { + fail("Should not succeed with more than two errors"); + } + } catch (NotFoundException e) { + assertEquals("Should only fail with three errors", 3, errors); + } + } + } + } + + // Zooms a bit matrix so that each bit is factor x factor + private static BitMatrix makeLarger(BitMatrix input, int factor) { + int width = input.getWidth(); + BitMatrix output = new BitMatrix(width * factor); + for (int inputY = 0; inputY < width; inputY++) { + for (int inputX = 0; inputX < width; inputX++) { + if (input.get(inputX, inputY)) { + output.setRegion(inputX * factor, inputY * factor, factor, factor); + } + } + } + return output; + } + + // Returns a list of the four rotations of the BitMatrix. The identity rotation is + // explicitly a copy, so that it can be modified without affecting the original matrix. + private static List getRotations(BitMatrix input) { + int width = input.getWidth(); + BitMatrix matrix0 = 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 y = 0; y < width; y++) { + if (input.get(x, y)) { + matrix0.set(x, y); + 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); + } + + private static List getOrientationPoints(AztecCode code) { + int center = code.getMatrix().getWidth() / 2; + int offset = code.isCompact() ? 5 : 7; + List result = new ArrayList(); + for (int xSign = -1; xSign <= 1; xSign += 2) { + for (int ySign = -1; ySign <= 1; ySign += 2) { + result.add(new Point(center + xSign * offset, center + ySign * offset)); + result.add(new Point(center + xSign * (offset - 1), center + ySign * offset)); + result.add(new Point(center + xSign * offset, center + ySign * (offset - 1))); + } + } + return result; + } + +}