50c0ede58e5b1612db6017e82379a5d0617b8f61
[zxing.git] / core / src / com / google / zxing / oned / Code39Reader.java
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.ChecksumException;
21 import com.google.zxing.FormatException;
22 import com.google.zxing.NotFoundException;
23 import com.google.zxing.Result;
24 import com.google.zxing.ResultPoint;
25 import com.google.zxing.common.BitArray;
26
27 import java.util.Hashtable;
28
29 /**
30  * <p>Decodes Code 39 barcodes. This does not support "Full ASCII Code 39" yet.</p>
31  *
32  * @author Sean Owen
33  */
34 public final class Code39Reader extends OneDReader {
35
36   static final String ALPHABET_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. *$/+%";
37   private static final char[] ALPHABET = ALPHABET_STRING.toCharArray();
38
39   /**
40    * These represent the encodings of characters, as patterns of wide and narrow bars.
41    * The 9 least-significant bits of each int correspond to the pattern of wide and narrow,
42    * with 1s representing "wide" and 0s representing narrow.
43    */
44   static final int[] CHARACTER_ENCODINGS = {
45       0x034, 0x121, 0x061, 0x160, 0x031, 0x130, 0x070, 0x025, 0x124, 0x064, // 0-9
46       0x109, 0x049, 0x148, 0x019, 0x118, 0x058, 0x00D, 0x10C, 0x04C, 0x01C, // A-J
47       0x103, 0x043, 0x142, 0x013, 0x112, 0x052, 0x007, 0x106, 0x046, 0x016, // K-T
48       0x181, 0x0C1, 0x1C0, 0x091, 0x190, 0x0D0, 0x085, 0x184, 0x0C4, 0x094, // U-*
49       0x0A8, 0x0A2, 0x08A, 0x02A // $-%
50   };
51
52   private static final int ASTERISK_ENCODING = CHARACTER_ENCODINGS[39];
53
54   private final boolean usingCheckDigit;
55   private final boolean extendedMode;
56
57   /**
58    * Creates a reader that assumes all encoded data is data, and does not treat the final
59    * character as a check digit. It will not decoded "extended Code 39" sequences.
60    */
61   public Code39Reader() {
62     usingCheckDigit = false;
63     extendedMode = false;
64   }
65
66   /**
67    * Creates a reader that can be configured to check the last character as a check digit.
68    * It will not decoded "extended Code 39" sequences.
69    *
70    * @param usingCheckDigit if true, treat the last data character as a check digit, not
71    * data, and verify that the checksum passes.
72    */
73   public Code39Reader(boolean usingCheckDigit) {
74     this.usingCheckDigit = usingCheckDigit;
75     this.extendedMode = false;
76   }
77
78   /**
79    * Creates a reader that can be configured to check the last character as a check digit,
80    * or optionally attempt to decode "extended Code 39" sequences that are used to encode
81    * the full ASCII character set.
82    *
83    * @param usingCheckDigit if true, treat the last data character as a check digit, not
84    * data, and verify that the checksum passes.
85    * @param extendedMode if true, will attempt to decode extended Code 39 sequences in the
86    * text.
87    */
88   public Code39Reader(boolean usingCheckDigit, boolean extendedMode) {
89     this.usingCheckDigit = usingCheckDigit;
90     this.extendedMode = extendedMode;
91   }
92
93   public Result decodeRow(int rowNumber, BitArray row, Hashtable hints)
94       throws NotFoundException, ChecksumException, FormatException {
95
96     int[] start = findAsteriskPattern(row);
97     int nextStart = start[1];
98     int end = row.getSize();
99
100     // Read off white space
101     while (nextStart < end && !row.get(nextStart)) {
102       nextStart++;
103     }
104
105     StringBuffer result = new StringBuffer(20);
106     int[] counters = new int[9];
107     char decodedChar;
108     int lastStart;
109     do {
110       recordPattern(row, nextStart, counters);
111       int pattern = toNarrowWidePattern(counters);
112       if (pattern < 0) {
113         throw NotFoundException.getNotFoundInstance();
114       }
115       decodedChar = patternToChar(pattern);
116       result.append(decodedChar);
117       lastStart = nextStart;
118       for (int i = 0; i < counters.length; i++) {
119         nextStart += counters[i];
120       }
121       // Read off white space
122       while (nextStart < end && !row.get(nextStart)) {
123         nextStart++;
124       }
125     } while (decodedChar != '*');
126     result.deleteCharAt(result.length() - 1); // remove asterisk
127
128     // Look for whitespace after pattern:
129     int lastPatternSize = 0;
130     for (int i = 0; i < counters.length; i++) {
131       lastPatternSize += counters[i];
132     }
133     int whiteSpaceAfterEnd = nextStart - lastStart - lastPatternSize;
134     // If 50% of last pattern size, following last pattern, is not whitespace, fail
135     // (but if it's whitespace to the very end of the image, that's OK)
136     if (nextStart != end && whiteSpaceAfterEnd / 2 < lastPatternSize) {
137       throw NotFoundException.getNotFoundInstance();
138     }
139
140     if (usingCheckDigit) {
141       int max = result.length() - 1;
142       int total = 0;
143       for (int i = 0; i < max; i++) {
144         total += ALPHABET_STRING.indexOf(result.charAt(i));
145       }
146       if (total % 43 != ALPHABET_STRING.indexOf(result.charAt(max))) {
147         throw ChecksumException.getChecksumInstance();
148       }
149       result.deleteCharAt(max);
150     }
151
152     String resultString = result.toString();
153     if (extendedMode) {
154       resultString = decodeExtended(resultString);
155     }
156
157     if (resultString.length() == 0) {
158       // Almost surely a false positive
159       throw NotFoundException.getNotFoundInstance();
160     }
161
162     float left = (float) (start[1] + start[0]) / 2.0f;
163     float right = (float) (nextStart + lastStart) / 2.0f;
164     return new Result(
165         resultString,
166         null,
167         new ResultPoint[]{
168             new ResultPoint(left, (float) rowNumber),
169             new ResultPoint(right, (float) rowNumber)},
170         BarcodeFormat.CODE_39);
171
172   }
173
174   private static int[] findAsteriskPattern(BitArray row) throws NotFoundException {
175     int width = row.getSize();
176     int rowOffset = 0;
177     while (rowOffset < width) {
178       if (row.get(rowOffset)) {
179         break;
180       }
181       rowOffset++;
182     }
183
184     int counterPosition = 0;
185     int[] counters = new int[9];
186     int patternStart = rowOffset;
187     boolean isWhite = false;
188     int patternLength = counters.length;
189
190     for (int i = rowOffset; i < width; i++) {
191       boolean pixel = row.get(i);
192       if (pixel ^ isWhite) {
193         counters[counterPosition]++;
194       } else {
195         if (counterPosition == patternLength - 1) {
196           if (toNarrowWidePattern(counters) == ASTERISK_ENCODING) {
197             // Look for whitespace before start pattern, >= 50% of width of start pattern
198             if (row.isRange(Math.max(0, patternStart - (i - patternStart) / 2), patternStart, false)) {
199               return new int[]{patternStart, i};
200             }
201           }
202           patternStart += counters[0] + counters[1];
203           for (int y = 2; y < patternLength; y++) {
204             counters[y - 2] = counters[y];
205           }
206           counters[patternLength - 2] = 0;
207           counters[patternLength - 1] = 0;
208           counterPosition--;
209         } else {
210           counterPosition++;
211         }
212         counters[counterPosition] = 1;
213         isWhite = !isWhite;
214       }
215     }
216     throw NotFoundException.getNotFoundInstance();
217   }
218
219   // For efficiency, returns -1 on failure. Not throwing here saved as many as 700 exceptions
220   // per image when using some of our blackbox images.
221   private static int toNarrowWidePattern(int[] counters) {
222     int numCounters = counters.length;
223     int maxNarrowCounter = 0;
224     int wideCounters;
225     do {
226       int minCounter = Integer.MAX_VALUE;
227       for (int i = 0; i < numCounters; i++) {
228         int counter = counters[i];
229         if (counter < minCounter && counter > maxNarrowCounter) {
230           minCounter = counter;
231         }
232       }
233       maxNarrowCounter = minCounter;
234       wideCounters = 0;
235       int totalWideCountersWidth = 0;
236       int pattern = 0;
237       for (int i = 0; i < numCounters; i++) {
238         int counter = counters[i];
239         if (counters[i] > maxNarrowCounter) {
240           pattern |= 1 << (numCounters - 1 - i);
241           wideCounters++;
242           totalWideCountersWidth += counter;
243         }
244       }
245       if (wideCounters == 3) {
246         // Found 3 wide counters, but are they close enough in width?
247         // We can perform a cheap, conservative check to see if any individual
248         // counter is more than 1.5 times the average:
249         for (int i = 0; i < numCounters && wideCounters > 0; i++) {
250           int counter = counters[i];
251           if (counters[i] > maxNarrowCounter) {
252             wideCounters--;
253             // totalWideCountersWidth = 3 * average, so this checks if counter >= 3/2 * average
254             if ((counter << 1) >= totalWideCountersWidth) {
255               return -1;
256             }
257           }
258         }
259         return pattern;
260       }
261     } while (wideCounters > 3);
262     return -1;
263   }
264
265   private static char patternToChar(int pattern) throws NotFoundException {
266     for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) {
267       if (CHARACTER_ENCODINGS[i] == pattern) {
268         return ALPHABET[i];
269       }
270     }
271     throw NotFoundException.getNotFoundInstance();
272   }
273
274   private static String decodeExtended(String encoded) throws FormatException {
275     int length = encoded.length();
276     StringBuffer decoded = new StringBuffer(length);
277     for (int i = 0; i < length; i++) {
278       char c = encoded.charAt(i);
279       if (c == '+' || c == '$' || c == '%' || c == '/') {
280         char next = encoded.charAt(i + 1);
281         char decodedChar = '\0';
282         switch (c) {
283           case '+':
284             // +A to +Z map to a to z
285             if (next >= 'A' && next <= 'Z') {
286               decodedChar = (char) (next + 32);
287             } else {
288               throw FormatException.getFormatInstance();
289             }
290             break;
291           case '$':
292             // $A to $Z map to control codes SH to SB
293             if (next >= 'A' && next <= 'Z') {
294               decodedChar = (char) (next - 64);
295             } else {
296               throw FormatException.getFormatInstance();
297             }
298             break;
299           case '%':
300             // %A to %E map to control codes ESC to US
301             if (next >= 'A' && next <= 'E') {
302               decodedChar = (char) (next - 38);
303             } else if (next >= 'F' && next <= 'W') {
304               decodedChar = (char) (next - 11);
305             } else {
306               throw FormatException.getFormatInstance();
307             }
308             break;
309           case '/':
310             // /A to /O map to ! to , and /Z maps to :
311             if (next >= 'A' && next <= 'O') {
312               decodedChar = (char) (next - 32);
313             } else if (next == 'Z') {
314               decodedChar = ':';
315             } else {
316               throw FormatException.getFormatInstance();
317             }
318             break;
319         }
320         decoded.append(decodedChar);
321         // bump up i again since we read two characters
322         i++;
323       } else {
324         decoded.append(c);
325       }
326     }
327     return decoded.toString();
328   }
329
330 }