2D barcode formats typically encode text, but allow for a sort of 'byte mode' @@ -49,15 +55,49 @@ public final class ResultMetadataType { *
This maps to a {@link java.util.Vector} of byte arrays corresponding to the * raw bytes in the byte segments in the barcode, in order.
*/ - public static final ResultMetadataType BYTE_SEGMENTS = new ResultMetadataType(); + public static final ResultMetadataType BYTE_SEGMENTS = new ResultMetadataType("BYTE_SEGMENTS"); /** * Error correction level used, if applicable. The value type depends on the * format, but is typically a String. */ - public static final ResultMetadataType ERROR_CORRECTION_LEVEL = new ResultMetadataType(); + public static final ResultMetadataType ERROR_CORRECTION_LEVEL = new ResultMetadataType("ERROR_CORRECTION_LEVEL"); - private ResultMetadataType() { + /** + * For some periodicals, indicates the issue number as an {@link Integer}. + */ + public static final ResultMetadataType ISSUE_NUMBER = new ResultMetadataType("ISSUE_NUMBER"); + + /** + * For some products, indicates the suggested retail price in the barcode as a + * formatted {@link String}. + */ + public static final ResultMetadataType SUGGESTED_PRICE = new ResultMetadataType("SUGGESTED_PRICE"); + + private final String name; + + private ResultMetadataType(String name) { + this.name = name; + VALUES.put(name, this); + } + + public String getName() { + return name; + } + + public String toString() { + return name; + } + + public static ResultMetadataType valueOf(String name) { + if (name == null || name.length() == 0) { + throw new IllegalArgumentException(); + } + ResultMetadataType format = (ResultMetadataType) VALUES.get(name); + if (format == null) { + throw new IllegalArgumentException(); + } + return format; } } diff --git a/core/src/com/google/zxing/oned/UPCEANExtensionSupport.java b/core/src/com/google/zxing/oned/UPCEANExtensionSupport.java new file mode 100644 index 000000000..3076941fd --- /dev/null +++ b/core/src/com/google/zxing/oned/UPCEANExtensionSupport.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2010 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.oned; + +import java.util.Hashtable; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.common.BitArray; + +final class UPCEANExtensionSupport { + + private static final int[] EXTENSION_START_PATTERN = {1,1,2}; + private static final int[] CHECK_DIGIT_ENCODINGS = { + 0x18, 0x14, 0x12, 0x11, 0x0C, 0x06, 0x03, 0x0A, 0x09, 0x05 + }; + private static final int[][] SEPARATOR_PATTERNS = {{1,1}}; + + private final int[] decodeMiddleCounters = new int[4]; + private final int[] separatorCounters = new int[2]; + private final StringBuffer decodeRowStringBuffer = new StringBuffer(); + + Result decodeRow(BitArray row, int rowOffset) throws NotFoundException { + + int[] extensionStartRange = UPCEANReader.findGuardPattern(row, rowOffset, false, EXTENSION_START_PATTERN); + + StringBuffer result = decodeRowStringBuffer; + result.setLength(0); + decodeMiddle(row, extensionStartRange, result); + + String resultString = result.toString(); + Hashtable extensionData = parseExtensionString(resultString); + + Result extensionResult = new Result(resultString, null, null, BarcodeFormat.UPC_EAN_EXTENSION); + if (extensionData != null) { + extensionResult.putAllMetadata(extensionData); + } + return extensionResult; + } + + int decodeMiddle(BitArray row, int[] startRange, StringBuffer resultString) throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int[] separatorCounters = this.separatorCounters; + separatorCounters[0] = 0; + separatorCounters[1] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + int lgPatternFound = 0; + + for (int x = 0; x < 5 && rowOffset < end; x++) { + int bestMatch = UPCEANReader.decodeDigit(row, counters, rowOffset, UPCEANReader.L_AND_G_PATTERNS); + resultString.append((char) ('0' + bestMatch % 10)); + for (int i = 0; i < counters.length; i++) { + rowOffset += counters[i]; + } + if (bestMatch >= 10) { + lgPatternFound |= 1 << (4 - x); + } + // Read off separator + /* + try { + UPCEANReader.decodeDigit(row, separatorCounters, rowOffset, SEPARATOR_PATTERNS); + rowOffset += separatorCounters[0] + separatorCounters[1]; + } catch (NotFoundException nfe) { + break; + } + */ + while (rowOffset < end && !row.get(rowOffset)) { + rowOffset++; + } + while (rowOffset < end && row.get(rowOffset)) { + rowOffset++; + } + } + + if (resultString.length() != 5) { + throw NotFoundException.getNotFoundInstance(); + } + + int checkDigit = determineCheckDigit(lgPatternFound); + if (extensionChecksum(resultString.toString()) != checkDigit) { + throw NotFoundException.getNotFoundInstance(); + } + + return rowOffset; + } + + private static int extensionChecksum(String s) { + int length = s.length(); + int sum = 0; + for (int i = length - 2; i >= 0; i -= 2) { + sum += (int) s.charAt(i) - (int) '0'; + } + sum *= 3; + for (int i = length - 1; i >= 0; i -= 2) { + sum += (int) s.charAt(i) - (int) '0'; + } + sum *= 3; + return sum % 10; + } + + private static int determineCheckDigit(int lgPatternFound) + throws NotFoundException { + for (int d = 0; d < 10; d++) { + if (lgPatternFound == CHECK_DIGIT_ENCODINGS[d]) { + return d; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * @param raw raw content of extension + * @return formatted interpretation of raw content as a {@link Hashtable} mapping + * one {@link ResultMetadataType} to appropriate value, ornull
if not known
+ */
+ private static Hashtable parseExtensionString(String raw) {
+ ResultMetadataType type;
+ Object value;
+ switch (raw.length()) {
+ case 2:
+ type = ResultMetadataType.ISSUE_NUMBER;
+ value = parseExtension2String(raw);
+ break;
+ case 5:
+ type = ResultMetadataType.SUGGESTED_PRICE;
+ value = parseExtension5String(raw);
+ break;
+ default:
+ return null;
+ }
+ if (value == null) {
+ return null;
+ }
+ Hashtable result = new Hashtable(1);
+ result.put(type, value);
+ return result;
+ }
+
+ private static Integer parseExtension2String(String raw) {
+ return Integer.valueOf(raw);
+ }
+
+ private static String parseExtension5String(String raw) {
+ String currency = null;
+ switch (raw.charAt(0)) {
+ case '0':
+ currency = "£";
+ break;
+ case '5':
+ currency = "$";
+ break;
+ case '9':
+ if ("99991".equals(raw)) {
+ return "0.00";
+ } else if ("99990".equals(raw)) {
+ return "Used";
+ }
+ break;
+ default:
+ currency = "";
+ break;
+ }
+ int rawAmount = Integer.parseInt(raw.substring(1));
+ return currency + (rawAmount / 100) + '.' + (rawAmount % 100);
+ }
+
+}
diff --git a/core/src/com/google/zxing/oned/UPCEANReader.java b/core/src/com/google/zxing/oned/UPCEANReader.java
index 803eaa6c2..6036157f2 100644
--- a/core/src/com/google/zxing/oned/UPCEANReader.java
+++ b/core/src/com/google/zxing/oned/UPCEANReader.java
@@ -21,6 +21,7 @@ import com.google.zxing.ChecksumException;
import com.google.zxing.DecodeHintType;
import com.google.zxing.FormatException;
import com.google.zxing.NotFoundException;
+import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.ResultPoint;
import com.google.zxing.ResultPointCallback;
@@ -91,9 +92,11 @@ public abstract class UPCEANReader extends OneDReader {
}
private final StringBuffer decodeRowStringBuffer;
+ private final UPCEANExtensionSupport extensionReader;
protected UPCEANReader() {
decodeRowStringBuffer = new StringBuffer(20);
+ extensionReader = new UPCEANExtensionSupport();
}
static int[] findStartGuardPattern(BitArray row) throws NotFoundException {
@@ -171,12 +174,20 @@ public abstract class UPCEANReader extends OneDReader {
float left = (float) (startGuardRange[1] + startGuardRange[0]) / 2.0f;
float right = (float) (endRange[1] + endRange[0]) / 2.0f;
- return new Result(resultString,
+ Result decodeResult = new Result(resultString,
null, // no natural byte representation for these barcodes
new ResultPoint[]{
new ResultPoint(left, (float) rowNumber),
new ResultPoint(right, (float) rowNumber)},
getBarcodeFormat());
+
+ try {
+ Result extensionResult = extensionReader.decodeRow(row, endRange[1]);
+ decodeResult.putAllMetadata(extensionResult.getResultMetadata());
+ } catch (ReaderException re) {
+ // continue
+ }
+ return decodeResult;
}
/**
@@ -193,9 +204,8 @@ public abstract class UPCEANReader extends OneDReader {
* @param s string of digits to check
* @return true iff string of digits passes the UPC/EAN checksum algorithm
* @throws FormatException if the string does not contain only digits
- * @throws ChecksumException if checksum mismatches
*/
- private static boolean checkStandardUPCEANChecksum(String s) throws ChecksumException, FormatException {
+ private static boolean checkStandardUPCEANChecksum(String s) throws FormatException {
int length = s.length();
if (length == 0) {
return false;
@@ -213,7 +223,7 @@ public abstract class UPCEANReader extends OneDReader {
for (int i = length - 1; i >= 0; i -= 2) {
int digit = (int) s.charAt(i) - (int) '0';
if (digit < 0 || digit > 9) {
- throw ChecksumException.getChecksumInstance();
+ throw FormatException.getFormatInstance();
}
sum += digit;
}
diff --git a/core/test/data/blackbox/upcean-extension-1/1.gif b/core/test/data/blackbox/upcean-extension-1/1.gif
new file mode 100644
index 000000000..2ef57e07f
Binary files /dev/null and b/core/test/data/blackbox/upcean-extension-1/1.gif differ
diff --git a/core/test/data/blackbox/upcean-extension-1/1.metadata.txt b/core/test/data/blackbox/upcean-extension-1/1.metadata.txt
new file mode 100644
index 000000000..6ad3f363c
--- /dev/null
+++ b/core/test/data/blackbox/upcean-extension-1/1.metadata.txt
@@ -0,0 +1 @@
+SUGGESTED_PRICE=$12.99
\ No newline at end of file
diff --git a/core/test/data/blackbox/upcean-extension-1/1.txt b/core/test/data/blackbox/upcean-extension-1/1.txt
new file mode 100644
index 000000000..101471a1f
--- /dev/null
+++ b/core/test/data/blackbox/upcean-extension-1/1.txt
@@ -0,0 +1 @@
+9780735200449
\ No newline at end of file
diff --git a/core/test/data/blackbox/upcean-extension-1/2.jpg b/core/test/data/blackbox/upcean-extension-1/2.jpg
new file mode 100755
index 000000000..b716cce27
Binary files /dev/null and b/core/test/data/blackbox/upcean-extension-1/2.jpg differ
diff --git a/core/test/data/blackbox/upcean-extension-1/2.metadata.txt b/core/test/data/blackbox/upcean-extension-1/2.metadata.txt
new file mode 100644
index 000000000..e14808d31
--- /dev/null
+++ b/core/test/data/blackbox/upcean-extension-1/2.metadata.txt
@@ -0,0 +1 @@
+SUGGESTED_PRICE=$24.95
\ No newline at end of file
diff --git a/core/test/data/blackbox/upcean-extension-1/2.txt b/core/test/data/blackbox/upcean-extension-1/2.txt
new file mode 100644
index 000000000..576a81120
--- /dev/null
+++ b/core/test/data/blackbox/upcean-extension-1/2.txt
@@ -0,0 +1 @@
+9780884271789
\ No newline at end of file
diff --git a/core/test/src/com/google/zxing/common/AbstractBlackBoxTestCase.java b/core/test/src/com/google/zxing/common/AbstractBlackBoxTestCase.java
index d078fa121..504834396 100644
--- a/core/test/src/com/google/zxing/common/AbstractBlackBoxTestCase.java
+++ b/core/test/src/com/google/zxing/common/AbstractBlackBoxTestCase.java
@@ -23,6 +23,7 @@ import com.google.zxing.LuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
+import com.google.zxing.ResultMetadataType;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import junit.framework.TestCase;
@@ -40,6 +41,8 @@ import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
+import java.util.Map;
+import java.util.Properties;
/**
* @author Sean Owen
@@ -174,19 +177,25 @@ public abstract class AbstractBlackBoxTestCase extends TestCase {
BufferedImage image = ImageIO.read(testImage);
String testImageFileName = testImage.getName();
- File expectedTextFile = new File(testBase,
- testImageFileName.substring(0, testImageFileName.indexOf('.')) + ".txt");
+ String fileBaseName = testImageFileName.substring(0, testImageFileName.indexOf('.'));
+ File expectedTextFile = new File(testBase, fileBaseName + ".txt");
String expectedText = readFileAsString(expectedTextFile);
+ File expectedMetadataFile = new File(testBase, fileBaseName + ".metadata.txt");
+ Properties expectedMetadata = new Properties();
+ if (expectedMetadataFile.exists()) {
+ expectedMetadata.load(new FileInputStream(expectedMetadataFile));
+ }
+
for (int x = 0; x < testCount; x++) {
float rotation = testResults.get(x).getRotation();
BufferedImage rotatedImage = rotateImage(image, rotation);
LuminanceSource source = new BufferedImageLuminanceSource(rotatedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
- if (decode(bitmap, rotation, expectedText, false)) {
+ if (decode(bitmap, rotation, expectedText, expectedMetadata, false)) {
passedCounts[x]++;
}
- if (decode(bitmap, rotation, expectedText, true)) {
+ if (decode(bitmap, rotation, expectedText, expectedMetadata, true)) {
tryHarderCounts[x]++;
}
}
@@ -231,7 +240,10 @@ public abstract class AbstractBlackBoxTestCase extends TestCase {
return new SummaryResults(totalFound, totalMustPass, totalTests);
}
- private boolean decode(BinaryBitmap source, float rotation, String expectedText,
+ private boolean decode(BinaryBitmap source,
+ float rotation,
+ String expectedText,
+ Properties expectedMetadata,
boolean tryHarder) {
Result result;
String suffix = " (" + (tryHarder ? "try harder, " : "") + "rotation: " + rotation + ')';
@@ -263,6 +275,19 @@ public abstract class AbstractBlackBoxTestCase extends TestCase {
'\'' + suffix);
return false;
}
+
+ Hashtable resultMetadata = result.getResultMetadata();
+ for (Map.Entry