1 /* 2 * Copyright 2008 ZXing authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.zxing.oned; 18 19 import com.google.zxing.BarcodeFormat; 20 import com.google.zxing.DecodeHintType; 21 import com.google.zxing.FormatException; 22 import com.google.zxing.NotFoundException; 23 import com.google.zxing.Result; 24 import com.google.zxing.ResultMetadataType; 25 import com.google.zxing.ResultPoint; 26 import com.google.zxing.common.BitArray; 27 28 import java.util.Map; 29 30 /** 31 * <p>Implements decoding of the ITF format, or Interleaved Two of Five.</p> 32 * 33 * <p>This Reader will scan ITF barcodes of certain lengths only. 34 * At the moment it reads length 6, 8, 10, 12, 14, 16, 18, 20, 24, and 44 as these have appeared "in the wild". Not all 35 * lengths are scanned, especially shorter ones, to avoid false positives. This in turn is due to a lack of 36 * required checksum function.</p> 37 * 38 * <p>The checksum is optional and is not applied by this Reader. The consumer of the decoded 39 * value will have to apply a checksum if required.</p> 40 * 41 * <p><a href="http://en.wikipedia.org/wiki/Interleaved_2_of_5">http://en.wikipedia.org/wiki/Interleaved_2_of_5</a> 42 * is a great reference for Interleaved 2 of 5 information.</p> 43 * 44 * @author kevin.osullivan@sita.aero, SITA Lab. 45 */ 46 public final class ITFReader extends OneDReader { 47 48 private static final float MAX_AVG_VARIANCE = 0.38f; 49 private static final float MAX_INDIVIDUAL_VARIANCE = 0.5f; 50 51 private static final int W = 3; // Pixel width of a 3x wide line 52 private static final int w = 2; // Pixel width of a 2x wide line 53 private static final int N = 1; // Pixed width of a narrow line 54 55 /** Valid ITF lengths. Anything longer than the largest value is also allowed. */ 56 private static final int[] DEFAULT_ALLOWED_LENGTHS = {6, 8, 10, 12, 14}; 57 58 // Stores the actual narrow line width of the image being decoded. 59 private int narrowLineWidth = -1; 60 61 /** 62 * Start/end guard pattern. 63 * 64 * Note: The end pattern is reversed because the row is reversed before 65 * searching for the END_PATTERN 66 */ 67 private static final int[] START_PATTERN = {N, N, N, N}; 68 private static final int[][] END_PATTERN_REVERSED = { 69 {N, N, w}, // 2x 70 {N, N, W} // 3x 71 }; 72 73 // See ITFWriter.PATTERNS 74 75 /** 76 * Patterns of Wide / Narrow lines to indicate each digit 77 */ 78 private static final int[][] PATTERNS = { 79 {N, N, w, w, N}, // 0 80 {w, N, N, N, w}, // 1 81 {N, w, N, N, w}, // 2 82 {w, w, N, N, N}, // 3 83 {N, N, w, N, w}, // 4 84 {w, N, w, N, N}, // 5 85 {N, w, w, N, N}, // 6 86 {N, N, N, w, w}, // 7 87 {w, N, N, w, N}, // 8 88 {N, w, N, w, N}, // 9 89 {N, N, W, W, N}, // 0 90 {W, N, N, N, W}, // 1 91 {N, W, N, N, W}, // 2 92 {W, W, N, N, N}, // 3 93 {N, N, W, N, W}, // 4 94 {W, N, W, N, N}, // 5 95 {N, W, W, N, N}, // 6 96 {N, N, N, W, W}, // 7 97 {W, N, N, W, N}, // 8 98 {N, W, N, W, N} // 9 99 }; 100 101 @Override decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints)102 public Result decodeRow(int rowNumber, BitArray row, Map<DecodeHintType,?> hints) 103 throws FormatException, NotFoundException { 104 105 // Find out where the Middle section (payload) starts & ends 106 int[] startRange = decodeStart(row); 107 int[] endRange = decodeEnd(row); 108 109 StringBuilder result = new StringBuilder(20); 110 decodeMiddle(row, startRange[1], endRange[0], result); 111 String resultString = result.toString(); 112 113 int[] allowedLengths = null; 114 if (hints != null) { 115 allowedLengths = (int[]) hints.get(DecodeHintType.ALLOWED_LENGTHS); 116 117 } 118 if (allowedLengths == null) { 119 allowedLengths = DEFAULT_ALLOWED_LENGTHS; 120 } 121 122 // To avoid false positives with 2D barcodes (and other patterns), make 123 // an assumption that the decoded string must be a 'standard' length if it's short 124 int length = resultString.length(); 125 boolean lengthOK = false; 126 int maxAllowedLength = 0; 127 for (int allowedLength : allowedLengths) { 128 if (length == allowedLength) { 129 lengthOK = true; 130 break; 131 } 132 if (allowedLength > maxAllowedLength) { 133 maxAllowedLength = allowedLength; 134 } 135 } 136 if (!lengthOK && length > maxAllowedLength) { 137 lengthOK = true; 138 } 139 if (!lengthOK) { 140 throw FormatException.getFormatInstance(); 141 } 142 143 Result resultObject = new Result( 144 resultString, 145 null, // no natural byte representation for these barcodes 146 new ResultPoint[] {new ResultPoint(startRange[1], rowNumber), 147 new ResultPoint(endRange[0], rowNumber)}, 148 BarcodeFormat.ITF); 149 resultObject.putMetadata(ResultMetadataType.SYMBOLOGY_IDENTIFIER, "]I0"); 150 return resultObject; 151 } 152 153 /** 154 * @param row row of black/white values to search 155 * @param payloadStart offset of start pattern 156 * @param resultString {@link StringBuilder} to append decoded chars to 157 * @throws NotFoundException if decoding could not complete successfully 158 */ decodeMiddle(BitArray row, int payloadStart, int payloadEnd, StringBuilder resultString)159 private static void decodeMiddle(BitArray row, 160 int payloadStart, 161 int payloadEnd, 162 StringBuilder resultString) throws NotFoundException { 163 164 // Digits are interleaved in pairs - 5 black lines for one digit, and the 165 // 5 166 // interleaved white lines for the second digit. 167 // Therefore, need to scan 10 lines and then 168 // split these into two arrays 169 int[] counterDigitPair = new int[10]; 170 int[] counterBlack = new int[5]; 171 int[] counterWhite = new int[5]; 172 173 while (payloadStart < payloadEnd) { 174 175 // Get 10 runs of black/white. 176 recordPattern(row, payloadStart, counterDigitPair); 177 // Split them into each array 178 for (int k = 0; k < 5; k++) { 179 int twoK = 2 * k; 180 counterBlack[k] = counterDigitPair[twoK]; 181 counterWhite[k] = counterDigitPair[twoK + 1]; 182 } 183 184 int bestMatch = decodeDigit(counterBlack); 185 resultString.append((char) ('0' + bestMatch)); 186 bestMatch = decodeDigit(counterWhite); 187 resultString.append((char) ('0' + bestMatch)); 188 189 for (int counterDigit : counterDigitPair) { 190 payloadStart += counterDigit; 191 } 192 } 193 } 194 195 /** 196 * Identify where the start of the middle / payload section starts. 197 * 198 * @param row row of black/white values to search 199 * @return Array, containing index of start of 'start block' and end of 200 * 'start block' 201 */ decodeStart(BitArray row)202 private int[] decodeStart(BitArray row) throws NotFoundException { 203 int endStart = skipWhiteSpace(row); 204 int[] startPattern = findGuardPattern(row, endStart, START_PATTERN); 205 206 // Determine the width of a narrow line in pixels. We can do this by 207 // getting the width of the start pattern and dividing by 4 because its 208 // made up of 4 narrow lines. 209 this.narrowLineWidth = (startPattern[1] - startPattern[0]) / 4; 210 211 validateQuietZone(row, startPattern[0]); 212 213 return startPattern; 214 } 215 216 /** 217 * The start & end patterns must be pre/post fixed by a quiet zone. This 218 * zone must be at least 10 times the width of a narrow line. Scan back until 219 * we either get to the start of the barcode or match the necessary number of 220 * quiet zone pixels. 221 * 222 * Note: Its assumed the row is reversed when using this method to find 223 * quiet zone after the end pattern. 224 * 225 * ref: http://www.barcode-1.net/i25code.html 226 * 227 * @param row bit array representing the scanned barcode. 228 * @param startPattern index into row of the start or end pattern. 229 * @throws NotFoundException if the quiet zone cannot be found 230 */ validateQuietZone(BitArray row, int startPattern)231 private void validateQuietZone(BitArray row, int startPattern) throws NotFoundException { 232 233 int quietCount = this.narrowLineWidth * 10; // expect to find this many pixels of quiet zone 234 235 // if there are not so many pixel at all let's try as many as possible 236 quietCount = Math.min(quietCount, startPattern); 237 238 for (int i = startPattern - 1; quietCount > 0 && i >= 0; i--) { 239 if (row.get(i)) { 240 break; 241 } 242 quietCount--; 243 } 244 if (quietCount != 0) { 245 // Unable to find the necessary number of quiet zone pixels. 246 throw NotFoundException.getNotFoundInstance(); 247 } 248 } 249 250 /** 251 * Skip all whitespace until we get to the first black line. 252 * 253 * @param row row of black/white values to search 254 * @return index of the first black line. 255 * @throws NotFoundException Throws exception if no black lines are found in the row 256 */ skipWhiteSpace(BitArray row)257 private static int skipWhiteSpace(BitArray row) throws NotFoundException { 258 int width = row.getSize(); 259 int endStart = row.getNextSet(0); 260 if (endStart == width) { 261 throw NotFoundException.getNotFoundInstance(); 262 } 263 264 return endStart; 265 } 266 267 /** 268 * Identify where the end of the middle / payload section ends. 269 * 270 * @param row row of black/white values to search 271 * @return Array, containing index of start of 'end block' and end of 'end 272 * block' 273 */ decodeEnd(BitArray row)274 private int[] decodeEnd(BitArray row) throws NotFoundException { 275 276 // For convenience, reverse the row and then 277 // search from 'the start' for the end block 278 row.reverse(); 279 try { 280 int endStart = skipWhiteSpace(row); 281 int[] endPattern; 282 try { 283 endPattern = findGuardPattern(row, endStart, END_PATTERN_REVERSED[0]); 284 } catch (NotFoundException nfe) { 285 endPattern = findGuardPattern(row, endStart, END_PATTERN_REVERSED[1]); 286 } 287 288 // The start & end patterns must be pre/post fixed by a quiet zone. This 289 // zone must be at least 10 times the width of a narrow line. 290 // ref: http://www.barcode-1.net/i25code.html 291 validateQuietZone(row, endPattern[0]); 292 293 // Now recalculate the indices of where the 'endblock' starts & stops to 294 // accommodate 295 // the reversed nature of the search 296 int temp = endPattern[0]; 297 endPattern[0] = row.getSize() - endPattern[1]; 298 endPattern[1] = row.getSize() - temp; 299 300 return endPattern; 301 } finally { 302 // Put the row back the right way. 303 row.reverse(); 304 } 305 } 306 307 /** 308 * @param row row of black/white values to search 309 * @param rowOffset position to start search 310 * @param pattern pattern of counts of number of black and white pixels that are 311 * being searched for as a pattern 312 * @return start/end horizontal offset of guard pattern, as an array of two 313 * ints 314 * @throws NotFoundException if pattern is not found 315 */ findGuardPattern(BitArray row, int rowOffset, int[] pattern)316 private static int[] findGuardPattern(BitArray row, 317 int rowOffset, 318 int[] pattern) throws NotFoundException { 319 int patternLength = pattern.length; 320 int[] counters = new int[patternLength]; 321 int width = row.getSize(); 322 boolean isWhite = false; 323 324 int counterPosition = 0; 325 int patternStart = rowOffset; 326 for (int x = rowOffset; x < width; x++) { 327 if (row.get(x) != isWhite) { 328 counters[counterPosition]++; 329 } else { 330 if (counterPosition == patternLength - 1) { 331 if (patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE) < MAX_AVG_VARIANCE) { 332 return new int[]{patternStart, x}; 333 } 334 patternStart += counters[0] + counters[1]; 335 System.arraycopy(counters, 2, counters, 0, counterPosition - 1); 336 counters[counterPosition - 1] = 0; 337 counters[counterPosition] = 0; 338 counterPosition--; 339 } else { 340 counterPosition++; 341 } 342 counters[counterPosition] = 1; 343 isWhite = !isWhite; 344 } 345 } 346 throw NotFoundException.getNotFoundInstance(); 347 } 348 349 /** 350 * Attempts to decode a sequence of ITF black/white lines into single 351 * digit. 352 * 353 * @param counters the counts of runs of observed black/white/black/... values 354 * @return The decoded digit 355 * @throws NotFoundException if digit cannot be decoded 356 */ decodeDigit(int[] counters)357 private static int decodeDigit(int[] counters) throws NotFoundException { 358 float bestVariance = MAX_AVG_VARIANCE; // worst variance we'll accept 359 int bestMatch = -1; 360 int max = PATTERNS.length; 361 for (int i = 0; i < max; i++) { 362 int[] pattern = PATTERNS[i]; 363 float variance = patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE); 364 if (variance < bestVariance) { 365 bestVariance = variance; 366 bestMatch = i; 367 } else if (variance == bestVariance) { 368 // if we find a second 'best match' with the same variance, we can not reliably report to have a suitable match 369 bestMatch = -1; 370 } 371 } 372 if (bestMatch >= 0) { 373 return bestMatch % 10; 374 } else { 375 throw NotFoundException.getNotFoundInstance(); 376 } 377 } 378 379 } 380