From 56f194794b5a76e40a8502d6da323d16b90c2e19 Mon Sep 17 00:00:00 2001 From: srowen Date: Thu, 20 Nov 2008 14:07:19 +0000 Subject: [PATCH] Added ITF-14 decoder from Kevin. Not enabled yet as we need to think a bit about how to handle this first. Also, the unit tests aren't passing for me but are for Kevin so I have commented them for the moment. git-svn-id: https://zxing.googlecode.com/svn/trunk@741 59b500cc-1b3d-0410-9834-0bbf25fbcc57 --- AUTHORS | 1 + core/src/com/google/zxing/BarcodeFormat.java | 3 + .../zxing/oned/AbstractUPCEANReader.java | 9 +- .../com/google/zxing/oned/ITF14Reader.java | 280 ++++++++++++++++++ core/test/data/blackbox/itf14-1/1.jpg | Bin 0 -> 5101 bytes core/test/data/blackbox/itf14-1/1.txt | 1 + core/test/data/blackbox/itf14-1/2.JPG | Bin 0 -> 8003 bytes core/test/data/blackbox/itf14-1/2.txt | 1 + .../zxing/oned/ITF14BlackBox1TestCase.java | 36 +++ 9 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 core/src/com/google/zxing/oned/ITF14Reader.java create mode 100755 core/test/data/blackbox/itf14-1/1.jpg create mode 100755 core/test/data/blackbox/itf14-1/1.txt create mode 100755 core/test/data/blackbox/itf14-1/2.JPG create mode 100755 core/test/data/blackbox/itf14-1/2.txt create mode 100644 core/test/src/com/google/zxing/oned/ITF14BlackBox1TestCase.java diff --git a/AUTHORS b/AUTHORS index f5a202f25..415fdf752 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,6 +9,7 @@ Daniel Switkin (Google) David Albert (Bug Labs) John Connolly (Bug Labs) Joseph Wain (Google) +Kevin O'Sullivan (SITA) Matthew Schulkind (Google) Matt York (LifeMarks) Paul Hackenberger diff --git a/core/src/com/google/zxing/BarcodeFormat.java b/core/src/com/google/zxing/BarcodeFormat.java index b7deb2798..c24d64192 100644 --- a/core/src/com/google/zxing/BarcodeFormat.java +++ b/core/src/com/google/zxing/BarcodeFormat.java @@ -49,6 +49,9 @@ public final class BarcodeFormat { /** Code 39 1D format. */ public static final BarcodeFormat CODE_39 = new BarcodeFormat("CODE_39"); + /** ITF-14 1D format. */ + public static final BarcodeFormat ITF_14 = new BarcodeFormat("ITF_14"); + private final String name; private BarcodeFormat(String name) { diff --git a/core/src/com/google/zxing/oned/AbstractUPCEANReader.java b/core/src/com/google/zxing/oned/AbstractUPCEANReader.java index ee38c8280..53ba2ccca 100644 --- a/core/src/com/google/zxing/oned/AbstractUPCEANReader.java +++ b/core/src/com/google/zxing/oned/AbstractUPCEANReader.java @@ -144,6 +144,13 @@ public abstract class AbstractUPCEANReader extends AbstractOneDReader implements abstract BarcodeFormat getBarcodeFormat(); + /** + * @return {@link #checkStandardUPCEANChecksum(String)} + */ + boolean checkChecksum(String s) throws ReaderException { + return checkStandardUPCEANChecksum(s); + } + /** * Computes the UPC/EAN checksum on a string of digits, and reports * whether the checksum is correct or not. @@ -152,7 +159,7 @@ public abstract class AbstractUPCEANReader extends AbstractOneDReader implements * @return true iff string of digits passes the UPC/EAN checksum algorithm * @throws ReaderException if the string does not contain only digits */ - boolean checkChecksum(String s) throws ReaderException { + public static boolean checkStandardUPCEANChecksum(String s) throws ReaderException { int length = s.length(); if (length == 0) { return false; diff --git a/core/src/com/google/zxing/oned/ITF14Reader.java b/core/src/com/google/zxing/oned/ITF14Reader.java new file mode 100644 index 000000000..028140f5c --- /dev/null +++ b/core/src/com/google/zxing/oned/ITF14Reader.java @@ -0,0 +1,280 @@ +/* + * Copyright 2008 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 com.google.zxing.BarcodeFormat; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; +import com.google.zxing.common.GenericResultPoint; + +import java.util.Hashtable; + +/** + *

Implements decoding of the ITF-14 format.

+ *

+ *

"ITF" stands for Interleaved Two of Five. The "-14" part indicates there are 14 digits encoded in the barcode.

+ *

+ *

http://en.wikipedia.org/wiki/Interleaved_2_of_5 + * is a great reference for Interleaved 2 of 5 information.

+ *

+ *

TODO: ITF-14 is an implementation of + * Interleaved 2 of 5 barcode. + * It stipulates that there is 14 digits in the bar code. A more abstract class should be implemented + * which will scan an arbritary number of digits encoded in two of five format.

+ * + * @author kevin.osullivan@sita.aero + */ +public final class ITF14Reader extends AbstractOneDReader { + + private static final int MAX_AVG_VARIANCE = (int) (PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.42f); + private static final int MAX_INDIVIDUAL_VARIANCE = (int) (PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.7f); + + private static final int W = 3; // Pixel width of a wide line + private static final int N = 1; // Pixed width of a narrow line + + private final int DIGIT_COUNT = 14; // There are 14 digits in ITF-14 + /** + * Start/end guard pattern. + *

+ * Note: The end pattern is reversed because the + * row is reversed before searching for the END_PATTERN + */ + private static final int[] START_PATTERN = {N, N, N, N}; + private static final int[] END_PATTERN_REVERSED = {N, N, W}; + + /** + * Patterns of Wide / Narrow lines to + * indicate each digit + */ + static final int[][] PATTERNS = { + {N, N, W, W, N}, // 0 + {W, N, N, N, W}, // 1 + {N, W, N, N, W}, // 2 + {W, W, N, N, N}, // 3 + {N, N, W, N, W}, // 4 + {W, N, W, N, N}, // 5 + {N, W, W, N, N}, // 6 + {N, N, N, W, W}, // 7 + {W, N, N, W, N}, // 8 + {N, W, N, W, N} // 9 + }; + + public final Result decodeRow(int rowNumber, BitArray row, Hashtable hints) throws ReaderException { + + StringBuffer result = new StringBuffer(20); + + /** + * Find out where the Middle section (payload) starts & ends + */ + int[] startRange = decodeStart(row); + int[] endRange = decodeEnd(row); + + decodeMiddle(row, startRange[1], endRange[0], result); + + String resultString = result.toString(); + if (!AbstractUPCEANReader.checkStandardUPCEANChecksum(resultString)) { + throw ReaderException.getInstance(); + } + + return new Result(resultString, null, // no natural byte representation + // for these barcodes + new ResultPoint[]{new GenericResultPoint(startRange[1], (float) rowNumber), + new GenericResultPoint(startRange[0], (float) rowNumber)}, + BarcodeFormat.ITF_14); + } + + + /** + * @param row row of black/white values to search + * @param payloadStart offset of start pattern + * @param resultString {@link StringBuffer} to append decoded chars to + * @throws ReaderException if decoding could not complete successfully + */ + protected void decodeMiddle(BitArray row, int payloadStart, int payloadEnd, StringBuffer resultString) + throws ReaderException { + + // Digits are interleaved in pairs - 5 black lines for one digit, and the 5 + // interleaved white lines for the second digit. + // Therefore, need to scan 10 lines and then + // split these into two arrays + int[] counterDigitPair = new int[10]; + int[] counterBlack = new int[5]; + int[] counterWhite = new int[5]; + + + for (int x = 0; x < DIGIT_COUNT / 2 && payloadStart < payloadEnd; x++) { + + // Get 10 runs of black/white. + recordPattern(row, payloadStart, counterDigitPair); + // Split them into each array + for (int k = 0; k < 5; k++) { + counterBlack[k] = counterDigitPair[k * 2]; + counterWhite[k] = counterDigitPair[(k * 2) + 1]; + } + + int bestMatch = decodeDigit(counterBlack); + resultString.append((char) ('0' + bestMatch % 10)); + bestMatch = decodeDigit(counterWhite); + resultString.append((char) ('0' + bestMatch % 10)); + + for (int i = 0; i < counterDigitPair.length; i++) { + payloadStart += counterDigitPair[i]; + } + } + } + + /** + * Identify where the start of the middle / payload section starts. + * + * @param row row of black/white values to search + * @return Array, containing index of start of 'start block' and end of 'start block' + * @throws ReaderException + */ + int[] decodeStart(BitArray row) throws ReaderException { + int endStart = skipWhiteSpace(row); + return findGuardPattern(row, endStart, START_PATTERN); + } + + /** + * Skip all whitespace until we get to the first black line. + * + * @param row row of black/white values to search + * @return index of the first black line. + * @throws ReaderException Throws exception if no black lines are found in the row + */ + private int skipWhiteSpace(BitArray row) throws ReaderException { + int width = row.getSize(); + int endStart = 0; + while (endStart < width) { + if (row.get(endStart)) { + break; + } + endStart++; + } + if (endStart == width) + throw ReaderException.getInstance(); + + return endStart; + } + + /** + * Identify where the start of the middle / payload section ends. + * + * @param row row of black/white values to search + * @return Array, containing index of start of 'end block' and end of 'end block' + * @throws ReaderException + */ + + int[] decodeEnd(BitArray row) throws ReaderException { + + // For convenience, reverse the row and then + // search from 'the start' for the end block + row.reverse(); + + int endStart = skipWhiteSpace(row); + int end[]; + try { + end = findGuardPattern(row, endStart, END_PATTERN_REVERSED); + } catch (ReaderException e) { + // Put our row of data back the right way before throwing + row.reverse(); + throw e; + } + + // Now recalc the indicies of where the 'endblock' starts & stops to accomodate + // the reversed nature of the search + int temp = end[0]; + end[0] = row.getSize() - end[1]; + end[1] = row.getSize() - temp; + + // Put the row back the righ way. + row.reverse(); + return end; + } + + /** + * @param row row of black/white values to search + * @param rowOffset position to start search + * @param pattern pattern of counts of number of black and white pixels that are + * being searched for as a pattern + * @return start/end horizontal offset of guard pattern, as an array of two ints + * @throws ReaderException if pattern is not found + */ + int[] findGuardPattern(BitArray row, int rowOffset, int[] pattern) throws ReaderException { + int patternLength = pattern.length; + int[] counters = new int[patternLength]; + int width = row.getSize(); + boolean isWhite = false; + + int counterPosition = 0; + int patternStart = rowOffset; + for (int x = rowOffset; x < width; x++) { + boolean pixel = row.get(x); + if ((!pixel && isWhite) || (pixel && !isWhite)) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + if (patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE) < MAX_AVG_VARIANCE) { + return new int[]{patternStart, x}; + } + patternStart += counters[0] + counters[1]; + for (int y = 2; y < patternLength; y++) { + counters[y - 2] = counters[y]; + } + counters[patternLength - 2] = 0; + counters[patternLength - 1] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw ReaderException.getInstance(); + } + + /** + * Attempts to decode a sequence of ITF-14 black/white lines into single digit. + * + * @param counters the counts of runs of observed black/white/black/... values + * @return The decoded digit + * @throws ReaderException if digit cannot be decoded + */ + static int decodeDigit(int[] counters) throws ReaderException { + + int bestVariance = MAX_AVG_VARIANCE; // worst variance we'll accept + int bestMatch = -1; + int max = PATTERNS.length; + for (int i = 0; i < max; i++) { + int[] pattern = PATTERNS[i]; + int variance = patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = i; + } + } + if (bestMatch >= 0) { + return bestMatch; + } else { + throw ReaderException.getInstance(); + } + } + +} \ No newline at end of file diff --git a/core/test/data/blackbox/itf14-1/1.jpg b/core/test/data/blackbox/itf14-1/1.jpg new file mode 100755 index 0000000000000000000000000000000000000000..325c1ebc57464f798a93f20c7b38699233f9dbc7 GIT binary patch literal 5101 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 pi2Z0+WbAyR&Ux>`be^6rNAJQbyrXn`D+xtm