blob: 704c7db0d0dbe6bc7e2cd8e7ac9c62d444b804e6 [file] [log] [blame]
/*
* 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.pdf417.decoder;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
import com.google.zxing.NotFoundException;
import com.google.zxing.ResultPoint;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.DecoderResult;
import com.google.zxing.common.detector.MathUtils;
import com.google.zxing.pdf417.PDF417Common;
import com.google.zxing.pdf417.decoder.ec.ErrorCorrection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Formatter;
import java.util.List;
/**
* @author Guenther Grau
*/
public final class PDF417ScanningDecoder {
private static final int CODEWORD_SKEW_SIZE = 2;
private static final int MAX_ERRORS = 3;
private static final int MAX_EC_CODEWORDS = 512;
private static final ErrorCorrection errorCorrection = new ErrorCorrection();
private PDF417ScanningDecoder() {
}
// TODO don't pass in minCodewordWidth and maxCodewordWidth, pass in barcode columns for start and stop pattern
// columns. That way width can be deducted from the pattern column.
// This approach also allows to detect more details about the barcode, e.g. if a bar type (white or black) is wider
// than it should be. This can happen if the scanner used a bad blackpoint.
public static DecoderResult decode(BitMatrix image,
ResultPoint imageTopLeft,
ResultPoint imageBottomLeft,
ResultPoint imageTopRight,
ResultPoint imageBottomRight,
int minCodewordWidth,
int maxCodewordWidth) throws NotFoundException, FormatException, ChecksumException {
BoundingBox boundingBox = new BoundingBox(image, imageTopLeft, imageBottomLeft, imageTopRight, imageBottomRight);
DetectionResultRowIndicatorColumn leftRowIndicatorColumn = null;
DetectionResultRowIndicatorColumn rightRowIndicatorColumn = null;
DetectionResult detectionResult = null;
for (int i = 0; i < 2; i++) {
if (imageTopLeft != null) {
leftRowIndicatorColumn = getRowIndicatorColumn(image, boundingBox, imageTopLeft, true, minCodewordWidth,
maxCodewordWidth);
}
if (imageTopRight != null) {
rightRowIndicatorColumn = getRowIndicatorColumn(image, boundingBox, imageTopRight, false, minCodewordWidth,
maxCodewordWidth);
}
detectionResult = merge(leftRowIndicatorColumn, rightRowIndicatorColumn);
if (detectionResult == null) {
throw NotFoundException.getNotFoundInstance();
}
if (i == 0 && detectionResult.getBoundingBox() != null &&
(detectionResult.getBoundingBox().getMinY() < boundingBox.getMinY() || detectionResult.getBoundingBox()
.getMaxY() > boundingBox.getMaxY())) {
boundingBox = detectionResult.getBoundingBox();
} else {
detectionResult.setBoundingBox(boundingBox);
break;
}
}
int maxBarcodeColumn = detectionResult.getBarcodeColumnCount() + 1;
detectionResult.setDetectionResultColumn(0, leftRowIndicatorColumn);
detectionResult.setDetectionResultColumn(maxBarcodeColumn, rightRowIndicatorColumn);
boolean leftToRight = leftRowIndicatorColumn != null;
for (int barcodeColumnCount = 1; barcodeColumnCount <= maxBarcodeColumn; barcodeColumnCount++) {
int barcodeColumn = leftToRight ? barcodeColumnCount : maxBarcodeColumn - barcodeColumnCount;
if (detectionResult.getDetectionResultColumn(barcodeColumn) != null) {
// This will be the case for the opposite row indicator column, which doesn't need to be decoded again.
continue;
}
DetectionResultColumn detectionResultColumn;
if (barcodeColumn == 0 || barcodeColumn == maxBarcodeColumn) {
detectionResultColumn = new DetectionResultRowIndicatorColumn(boundingBox, barcodeColumn == 0);
} else {
detectionResultColumn = new DetectionResultColumn(boundingBox);
}
detectionResult.setDetectionResultColumn(barcodeColumn, detectionResultColumn);
int startColumn = -1;
int previousStartColumn = startColumn;
// TODO start at a row for which we know the start position, then detect upwards and downwards from there.
for (int imageRow = boundingBox.getMinY(); imageRow <= boundingBox.getMaxY(); imageRow++) {
startColumn = getStartColumn(detectionResult, barcodeColumn, imageRow, leftToRight);
if (startColumn < 0 || startColumn > boundingBox.getMaxX()) {
if (previousStartColumn == -1) {
continue;
}
startColumn = previousStartColumn;
}
Codeword codeword = detectCodeword(image, boundingBox.getMinX(), boundingBox.getMaxX(), leftToRight,
startColumn, imageRow, minCodewordWidth, maxCodewordWidth);
if (codeword != null) {
detectionResultColumn.setCodeword(imageRow, codeword);
previousStartColumn = startColumn;
minCodewordWidth = Math.min(minCodewordWidth, codeword.getWidth());
maxCodewordWidth = Math.max(maxCodewordWidth, codeword.getWidth());
}
}
}
return createDecoderResult(detectionResult);
}
private static DetectionResult merge(DetectionResultRowIndicatorColumn leftRowIndicatorColumn,
DetectionResultRowIndicatorColumn rightRowIndicatorColumn)
throws NotFoundException {
if (leftRowIndicatorColumn == null && rightRowIndicatorColumn == null) {
return null;
}
BarcodeMetadata barcodeMetadata = getBarcodeMetadata(leftRowIndicatorColumn, rightRowIndicatorColumn);
if (barcodeMetadata == null) {
return null;
}
BoundingBox boundingBox = BoundingBox.merge(adjustBoundingBox(leftRowIndicatorColumn),
adjustBoundingBox(rightRowIndicatorColumn));
return new DetectionResult(barcodeMetadata, boundingBox);
}
private static BoundingBox adjustBoundingBox(DetectionResultRowIndicatorColumn rowIndicatorColumn)
throws NotFoundException {
if (rowIndicatorColumn == null) {
return null;
}
int[] rowHeights = rowIndicatorColumn.getRowHeights();
if (rowHeights == null) {
return null;
}
int maxRowHeight = getMax(rowHeights);
int missingStartRows = 0;
for (int rowHeight : rowHeights) {
missingStartRows += maxRowHeight - rowHeight;
if (rowHeight > 0) {
break;
}
}
Codeword[] codewords = rowIndicatorColumn.getCodewords();
for (int row = 0; missingStartRows > 0 && codewords[row] == null; row++) {
missingStartRows--;
}
int missingEndRows = 0;
for (int row = rowHeights.length - 1; row >= 0; row--) {
missingEndRows += maxRowHeight - rowHeights[row];
if (rowHeights[row] > 0) {
break;
}
}
for (int row = codewords.length - 1; missingEndRows > 0 && codewords[row] == null; row--) {
missingEndRows--;
}
return rowIndicatorColumn.getBoundingBox().addMissingRows(missingStartRows, missingEndRows,
rowIndicatorColumn.isLeft());
}
private static int getMax(int[] values) {
int maxValue = -1;
for (int value : values) {
maxValue = Math.max(maxValue, value);
}
return maxValue;
}
private static BarcodeMetadata getBarcodeMetadata(DetectionResultRowIndicatorColumn leftRowIndicatorColumn,
DetectionResultRowIndicatorColumn rightRowIndicatorColumn) {
BarcodeMetadata leftBarcodeMetadata;
if (leftRowIndicatorColumn == null ||
(leftBarcodeMetadata = leftRowIndicatorColumn.getBarcodeMetadata()) == null) {
return rightRowIndicatorColumn == null ? null : rightRowIndicatorColumn.getBarcodeMetadata();
}
BarcodeMetadata rightBarcodeMetadata;
if (rightRowIndicatorColumn == null ||
(rightBarcodeMetadata = rightRowIndicatorColumn.getBarcodeMetadata()) == null) {
return leftBarcodeMetadata;
}
if (leftBarcodeMetadata.getColumnCount() != rightBarcodeMetadata.getColumnCount() &&
leftBarcodeMetadata.getErrorCorrectionLevel() != rightBarcodeMetadata.getErrorCorrectionLevel() &&
leftBarcodeMetadata.getRowCount() != rightBarcodeMetadata.getRowCount()) {
return null;
}
return leftBarcodeMetadata;
}
private static DetectionResultRowIndicatorColumn getRowIndicatorColumn(BitMatrix image,
BoundingBox boundingBox,
ResultPoint startPoint,
boolean leftToRight,
int minCodewordWidth,
int maxCodewordWidth) {
DetectionResultRowIndicatorColumn rowIndicatorColumn = new DetectionResultRowIndicatorColumn(boundingBox,
leftToRight);
for (int i = 0; i < 2; i++) {
int increment = i == 0 ? 1 : -1;
int startColumn = (int) startPoint.getX();
for (int imageRow = (int) startPoint.getY(); imageRow <= boundingBox.getMaxY() &&
imageRow >= boundingBox.getMinY(); imageRow += increment) {
Codeword codeword = detectCodeword(image, 0, image.getWidth(), leftToRight, startColumn, imageRow,
minCodewordWidth, maxCodewordWidth);
if (codeword != null) {
rowIndicatorColumn.setCodeword(imageRow, codeword);
if (leftToRight) {
startColumn = codeword.getStartX();
} else {
startColumn = codeword.getEndX();
}
}
}
}
return rowIndicatorColumn;
}
private static void adjustCodewordCount(DetectionResult detectionResult, BarcodeValue[][] barcodeMatrix)
throws NotFoundException {
BarcodeValue barcodeMatrix01 = barcodeMatrix[0][1];
int[] numberOfCodewords = barcodeMatrix01.getValue();
int calculatedNumberOfCodewords = detectionResult.getBarcodeColumnCount() *
detectionResult.getBarcodeRowCount() -
getNumberOfECCodeWords(detectionResult.getBarcodeECLevel());
if (numberOfCodewords.length == 0) {
if (calculatedNumberOfCodewords < 1 || calculatedNumberOfCodewords > PDF417Common.MAX_CODEWORDS_IN_BARCODE) {
throw NotFoundException.getNotFoundInstance();
}
barcodeMatrix01.setValue(calculatedNumberOfCodewords);
} else if (numberOfCodewords[0] != calculatedNumberOfCodewords) {
// The calculated one is more reliable as it is derived from the row indicator columns
barcodeMatrix01.setValue(calculatedNumberOfCodewords);
}
}
private static DecoderResult createDecoderResult(DetectionResult detectionResult) throws FormatException,
ChecksumException, NotFoundException {
BarcodeValue[][] barcodeMatrix = createBarcodeMatrix(detectionResult);
adjustCodewordCount(detectionResult, barcodeMatrix);
Collection<Integer> erasures = new ArrayList<>();
int[] codewords = new int[detectionResult.getBarcodeRowCount() * detectionResult.getBarcodeColumnCount()];
List<int[]> ambiguousIndexValuesList = new ArrayList<>();
List<Integer> ambiguousIndexesList = new ArrayList<>();
for (int row = 0; row < detectionResult.getBarcodeRowCount(); row++) {
for (int column = 0; column < detectionResult.getBarcodeColumnCount(); column++) {
int[] values = barcodeMatrix[row][column + 1].getValue();
int codewordIndex = row * detectionResult.getBarcodeColumnCount() + column;
if (values.length == 0) {
erasures.add(codewordIndex);
} else if (values.length == 1) {
codewords[codewordIndex] = values[0];
} else {
ambiguousIndexesList.add(codewordIndex);
ambiguousIndexValuesList.add(values);
}
}
}
int[][] ambiguousIndexValues = new int[ambiguousIndexValuesList.size()][];
for (int i = 0; i < ambiguousIndexValues.length; i++) {
ambiguousIndexValues[i] = ambiguousIndexValuesList.get(i);
}
return createDecoderResultFromAmbiguousValues(detectionResult.getBarcodeECLevel(), codewords,
PDF417Common.toIntArray(erasures), PDF417Common.toIntArray(ambiguousIndexesList), ambiguousIndexValues);
}
/**
* This method deals with the fact, that the decoding process doesn't always yield a single most likely value. The
* current error correction implementation doesn't deal with erasures very well, so it's better to provide a value
* for these ambiguous codewords instead of treating it as an erasure. The problem is that we don't know which of
* the ambiguous values to choose. We try decode using the first value, and if that fails, we use another of the
* ambiguous values and try to decode again. This usually only happens on very hard to read and decode barcodes,
* so decoding the normal barcodes is not affected by this.
*
* @param erasureArray contains the indexes of erasures
* @param ambiguousIndexes array with the indexes that have more than one most likely value
* @param ambiguousIndexValues two dimensional array that contains the ambiguous values. The first dimension must
* be the same length as the ambiguousIndexes array
*/
private static DecoderResult createDecoderResultFromAmbiguousValues(int ecLevel,
int[] codewords,
int[] erasureArray,
int[] ambiguousIndexes,
int[][] ambiguousIndexValues)
throws FormatException, ChecksumException {
int[] ambiguousIndexCount = new int[ambiguousIndexes.length];
int tries = 100;
while (tries-- > 0) {
for (int i = 0; i < ambiguousIndexCount.length; i++) {
codewords[ambiguousIndexes[i]] = ambiguousIndexValues[i][ambiguousIndexCount[i]];
}
try {
return decodeCodewords(codewords, ecLevel, erasureArray);
} catch (ChecksumException ignored) {
//
}
if (ambiguousIndexCount.length == 0) {
throw ChecksumException.getChecksumInstance();
}
for (int i = 0; i < ambiguousIndexCount.length; i++) {
if (ambiguousIndexCount[i] < ambiguousIndexValues[i].length - 1) {
ambiguousIndexCount[i]++;
break;
} else {
ambiguousIndexCount[i] = 0;
if (i == ambiguousIndexCount.length - 1) {
throw ChecksumException.getChecksumInstance();
}
}
}
}
throw ChecksumException.getChecksumInstance();
}
private static BarcodeValue[][] createBarcodeMatrix(DetectionResult detectionResult) {
BarcodeValue[][] barcodeMatrix =
new BarcodeValue[detectionResult.getBarcodeRowCount()][detectionResult.getBarcodeColumnCount() + 2];
for (int row = 0; row < barcodeMatrix.length; row++) {
for (int column = 0; column < barcodeMatrix[row].length; column++) {
barcodeMatrix[row][column] = new BarcodeValue();
}
}
int column = 0;
for (DetectionResultColumn detectionResultColumn : detectionResult.getDetectionResultColumns()) {
if (detectionResultColumn != null) {
for (Codeword codeword : detectionResultColumn.getCodewords()) {
if (codeword != null) {
int rowNumber = codeword.getRowNumber();
if (rowNumber >= 0) {
if (rowNumber >= barcodeMatrix.length) {
// We have more rows than the barcode metadata allows for, ignore them.
continue;
}
barcodeMatrix[rowNumber][column].setValue(codeword.getValue());
}
}
}
}
column++;
}
return barcodeMatrix;
}
private static boolean isValidBarcodeColumn(DetectionResult detectionResult, int barcodeColumn) {
return barcodeColumn >= 0 && barcodeColumn <= detectionResult.getBarcodeColumnCount() + 1;
}
private static int getStartColumn(DetectionResult detectionResult,
int barcodeColumn,
int imageRow,
boolean leftToRight) {
int offset = leftToRight ? 1 : -1;
Codeword codeword = null;
if (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) {
codeword = detectionResult.getDetectionResultColumn(barcodeColumn - offset).getCodeword(imageRow);
}
if (codeword != null) {
return leftToRight ? codeword.getEndX() : codeword.getStartX();
}
codeword = detectionResult.getDetectionResultColumn(barcodeColumn).getCodewordNearby(imageRow);
if (codeword != null) {
return leftToRight ? codeword.getStartX() : codeword.getEndX();
}
if (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) {
codeword = detectionResult.getDetectionResultColumn(barcodeColumn - offset).getCodewordNearby(imageRow);
}
if (codeword != null) {
return leftToRight ? codeword.getEndX() : codeword.getStartX();
}
int skippedColumns = 0;
while (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) {
barcodeColumn -= offset;
for (Codeword previousRowCodeword : detectionResult.getDetectionResultColumn(barcodeColumn).getCodewords()) {
if (previousRowCodeword != null) {
return (leftToRight ? previousRowCodeword.getEndX() : previousRowCodeword.getStartX()) +
offset *
skippedColumns *
(previousRowCodeword.getEndX() - previousRowCodeword.getStartX());
}
}
skippedColumns++;
}
return leftToRight ? detectionResult.getBoundingBox().getMinX() : detectionResult.getBoundingBox().getMaxX();
}
private static Codeword detectCodeword(BitMatrix image,
int minColumn,
int maxColumn,
boolean leftToRight,
int startColumn,
int imageRow,
int minCodewordWidth,
int maxCodewordWidth) {
startColumn = adjustCodewordStartColumn(image, minColumn, maxColumn, leftToRight, startColumn, imageRow);
// we usually know fairly exact now how long a codeword is. We should provide minimum and maximum expected length
// and try to adjust the read pixels, e.g. remove single pixel errors or try to cut off exceeding pixels.
// min and maxCodewordWidth should not be used as they are calculated for the whole barcode an can be inaccurate
// for the current position
int[] moduleBitCount = getModuleBitCount(image, minColumn, maxColumn, leftToRight, startColumn, imageRow);
if (moduleBitCount == null) {
return null;
}
int endColumn;
int codewordBitCount = MathUtils.sum(moduleBitCount);
if (leftToRight) {
endColumn = startColumn + codewordBitCount;
} else {
for (int i = 0; i < moduleBitCount.length / 2; i++) {
int tmpCount = moduleBitCount[i];
moduleBitCount[i] = moduleBitCount[moduleBitCount.length - 1 - i];
moduleBitCount[moduleBitCount.length - 1 - i] = tmpCount;
}
endColumn = startColumn;
startColumn = endColumn - codewordBitCount;
}
// TODO implement check for width and correction of black and white bars
// use start (and maybe stop pattern) to determine if black bars are wider than white bars. If so, adjust.
// should probably done only for codewords with a lot more than 17 bits.
// The following fixes 10-1.png, which has wide black bars and small white bars
// for (int i = 0; i < moduleBitCount.length; i++) {
// if (i % 2 == 0) {
// moduleBitCount[i]--;
// } else {
// moduleBitCount[i]++;
// }
// }
// We could also use the width of surrounding codewords for more accurate results, but this seems
// sufficient for now
if (!checkCodewordSkew(codewordBitCount, minCodewordWidth, maxCodewordWidth)) {
// We could try to use the startX and endX position of the codeword in the same column in the previous row,
// create the bit count from it and normalize it to 8. This would help with single pixel errors.
return null;
}
int decodedValue = PDF417CodewordDecoder.getDecodedValue(moduleBitCount);
int codeword = PDF417Common.getCodeword(decodedValue);
if (codeword == -1) {
return null;
}
return new Codeword(startColumn, endColumn, getCodewordBucketNumber(decodedValue), codeword);
}
private static int[] getModuleBitCount(BitMatrix image,
int minColumn,
int maxColumn,
boolean leftToRight,
int startColumn,
int imageRow) {
int imageColumn = startColumn;
int[] moduleBitCount = new int[8];
int moduleNumber = 0;
int increment = leftToRight ? 1 : -1;
boolean previousPixelValue = leftToRight;
while ((leftToRight ? imageColumn < maxColumn : imageColumn >= minColumn) &&
moduleNumber < moduleBitCount.length) {
if (image.get(imageColumn, imageRow) == previousPixelValue) {
moduleBitCount[moduleNumber]++;
imageColumn += increment;
} else {
moduleNumber++;
previousPixelValue = !previousPixelValue;
}
}
if (moduleNumber == moduleBitCount.length ||
((imageColumn == (leftToRight ? maxColumn : minColumn)) &&
moduleNumber == moduleBitCount.length - 1)) {
return moduleBitCount;
}
return null;
}
private static int getNumberOfECCodeWords(int barcodeECLevel) {
return 2 << barcodeECLevel;
}
private static int adjustCodewordStartColumn(BitMatrix image,
int minColumn,
int maxColumn,
boolean leftToRight,
int codewordStartColumn,
int imageRow) {
int correctedStartColumn = codewordStartColumn;
int increment = leftToRight ? -1 : 1;
// there should be no black pixels before the start column. If there are, then we need to start earlier.
for (int i = 0; i < 2; i++) {
while ((leftToRight ? correctedStartColumn >= minColumn : correctedStartColumn < maxColumn) &&
leftToRight == image.get(correctedStartColumn, imageRow)) {
if (Math.abs(codewordStartColumn - correctedStartColumn) > CODEWORD_SKEW_SIZE) {
return codewordStartColumn;
}
correctedStartColumn += increment;
}
increment = -increment;
leftToRight = !leftToRight;
}
return correctedStartColumn;
}
private static boolean checkCodewordSkew(int codewordSize, int minCodewordWidth, int maxCodewordWidth) {
return minCodewordWidth - CODEWORD_SKEW_SIZE <= codewordSize &&
codewordSize <= maxCodewordWidth + CODEWORD_SKEW_SIZE;
}
private static DecoderResult decodeCodewords(int[] codewords, int ecLevel, int[] erasures) throws FormatException,
ChecksumException {
if (codewords.length == 0) {
throw FormatException.getFormatInstance();
}
int numECCodewords = 1 << (ecLevel + 1);
int correctedErrorsCount = correctErrors(codewords, erasures, numECCodewords);
verifyCodewordCount(codewords, numECCodewords);
// Decode the codewords
DecoderResult decoderResult = DecodedBitStreamParser.decode(codewords, String.valueOf(ecLevel));
decoderResult.setErrorsCorrected(correctedErrorsCount);
decoderResult.setErasures(erasures.length);
return decoderResult;
}
/**
* <p>Given data and error-correction codewords received, possibly corrupted by errors, attempts to
* correct the errors in-place.</p>
*
* @param codewords data and error correction codewords
* @param erasures positions of any known erasures
* @param numECCodewords number of error correction codewords that are available in codewords
* @throws ChecksumException if error correction fails
*/
private static int correctErrors(int[] codewords, int[] erasures, int numECCodewords) throws ChecksumException {
if (erasures != null &&
erasures.length > numECCodewords / 2 + MAX_ERRORS ||
numECCodewords < 0 ||
numECCodewords > MAX_EC_CODEWORDS) {
// Too many errors or EC Codewords is corrupted
throw ChecksumException.getChecksumInstance();
}
return errorCorrection.decode(codewords, numECCodewords, erasures);
}
/**
* Verify that all is OK with the codeword array.
*/
private static void verifyCodewordCount(int[] codewords, int numECCodewords) throws FormatException {
if (codewords.length < 4) {
// Codeword array size should be at least 4 allowing for
// Count CW, At least one Data CW, Error Correction CW, Error Correction CW
throw FormatException.getFormatInstance();
}
// The first codeword, the Symbol Length Descriptor, shall always encode the total number of data
// codewords in the symbol, including the Symbol Length Descriptor itself, data codewords and pad
// codewords, but excluding the number of error correction codewords.
int numberOfCodewords = codewords[0];
if (numberOfCodewords > codewords.length) {
throw FormatException.getFormatInstance();
}
if (numberOfCodewords == 0) {
// Reset to the length of the array - 8 (Allow for at least level 3 Error Correction (8 Error Codewords)
if (numECCodewords < codewords.length) {
codewords[0] = codewords.length - numECCodewords;
} else {
throw FormatException.getFormatInstance();
}
}
}
private static int[] getBitCountForCodeword(int codeword) {
int[] result = new int[8];
int previousValue = 0;
int i = result.length - 1;
while (true) {
if ((codeword & 0x1) != previousValue) {
previousValue = codeword & 0x1;
i--;
if (i < 0) {
break;
}
}
result[i]++;
codeword >>= 1;
}
return result;
}
private static int getCodewordBucketNumber(int codeword) {
return getCodewordBucketNumber(getBitCountForCodeword(codeword));
}
private static int getCodewordBucketNumber(int[] moduleBitCount) {
return (moduleBitCount[0] - moduleBitCount[2] + moduleBitCount[4] - moduleBitCount[6] + 9) % 9;
}
public static String toString(BarcodeValue[][] barcodeMatrix) {
try (Formatter formatter = new Formatter()) {
for (int row = 0; row < barcodeMatrix.length; row++) {
formatter.format("Row %2d: ", row);
for (int column = 0; column < barcodeMatrix[row].length; column++) {
BarcodeValue barcodeValue = barcodeMatrix[row][column];
if (barcodeValue.getValue().length == 0) {
formatter.format(" ", (Object[]) null);
} else {
formatter.format("%4d(%2d)", barcodeValue.getValue()[0],
barcodeValue.getConfidence(barcodeValue.getValue()[0]));
}
}
formatter.format("%n");
}
return formatter.toString();
}
}
}