Added, at least, parsing of ECI mode in QR Code
[zxing.git] / core / src / com / google / zxing / qrcode / decoder / DecodedBitStreamParser.java
index 08cc5bd..b6dda58 100644 (file)
@@ -17,6 +17,7 @@
 package com.google.zxing.qrcode.decoder;
 
 import com.google.zxing.ReaderException;
+import com.google.zxing.common.BitSource;
 
 import java.io.UnsupportedEncodingException;
 
@@ -33,19 +34,21 @@ final class DecodedBitStreamParser {
   /**
    * See ISO 18004:2006, 6.4.4 Table 5
    */
-  private static final char[] ALPHANUMERIC_CHARS = new char[]{
+  private static final char[] ALPHANUMERIC_CHARS = {
       '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B',
       'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
       'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
       ' ', '$', '%', '*', '+', '-', '.', '/', ':'
   };
   private static final String SHIFT_JIS = "Shift_JIS";
+  private static final String EUC_JP = "EUC-JP";
   private static final boolean ASSUME_SHIFT_JIS;
+  private static final String UTF8 = "UTF-8";
+  private static final String ISO88591 = "ISO-8859-1";
 
   static {
     String platformDefault = System.getProperty("file.encoding");
-    ASSUME_SHIFT_JIS = SHIFT_JIS.equalsIgnoreCase(platformDefault) ||
-        "EUC-JP".equalsIgnoreCase(platformDefault);
+    ASSUME_SHIFT_JIS = SHIFT_JIS.equalsIgnoreCase(platformDefault) || EUC_JP.equalsIgnoreCase(platformDefault);
   }
 
   private DecodedBitStreamParser() {
@@ -57,20 +60,34 @@ final class DecodedBitStreamParser {
     Mode mode;
     do {
       // While still another segment to read...
-      mode = Mode.forBits(bits.readBits(4)); // mode is encoded by 4 bits
+      if (bits.available() == 0) {
+        // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here
+        mode = Mode.TERMINATOR;
+      } else {
+        mode = Mode.forBits(bits.readBits(4)); // mode is encoded by 4 bits
+      }
       if (!mode.equals(Mode.TERMINATOR)) {
-        // How many characters will follow, encoded in this mode?
-        int count = bits.readBits(mode.getCharacterCountBits(version));
-        if (mode.equals(Mode.NUMERIC)) {
-          decodeNumericSegment(bits, result, count);
-        } else if (mode.equals(Mode.ALPHANUMERIC)) {
-          decodeAlphanumericSegment(bits, result, count);
-        } else if (mode.equals(Mode.BYTE)) {
-          decodeByteSegment(bits, result, count);
-        } else if (mode.equals(Mode.KANJI)) {
-          decodeKanjiSegment(bits, result, count);
+        if (mode.equals(Mode.ECI)) {
+          // Count doesn't apply to ECI
+          parseECI(bits);
+          // We don't currently do anything with ECI, since there seems to be no reference
+          // defining what each value means. AIM's "Extended Channel Interpretations" does
+          // not define it. I have never observed a QR Code using it. So for now, we at least
+          // parse it but don't know how to take action on it.
         } else {
-          throw new ReaderException("Unsupported mode indicator");
+          // How many characters will follow, encoded in this mode?
+         int count = bits.readBits(mode.getCharacterCountBits(version));
+          if (mode.equals(Mode.NUMERIC)) {
+            decodeNumericSegment(bits, result, count);
+          } else if (mode.equals(Mode.ALPHANUMERIC)) {
+            decodeAlphanumericSegment(bits, result, count);
+          } else if (mode.equals(Mode.BYTE)) {
+            decodeByteSegment(bits, result, count);
+          } else if (mode.equals(Mode.KANJI)) {
+            decodeKanjiSegment(bits, result, count);
+          } else {
+            throw new ReaderException("Unsupported mode indicator");
+          }
         }
       }
     } while (!mode.equals(Mode.TERMINATOR));
@@ -87,6 +104,23 @@ final class DecodedBitStreamParser {
     return result.toString();
   }
 
+  private static int parseECI(BitSource bits) {
+    int firstByte = bits.readBits(8);
+    if (firstByte & 0x80 == 0) {
+      // just one byte
+      return firstByte & 0x7F;
+    } else if (firstByte & 0xC0 == 0x80) {
+      // two bytes
+      int secondByte = bits.readBits(8);
+      return ((firstByte & 0x3F) << 8) | secondByte;
+    } else if (firstByte & 0xE0 == 0xC0) {
+      // three bytes
+      int secondByte = bits.readBits(8);
+      int thirdByte = bits.readBits(8);
+      return ((firstByte & 0x1F) << 16) | (secondByte << 8) | thirdByte;
+    }
+  }
+
   private static void decodeKanjiSegment(BitSource bits,
                                          StringBuffer result,
                                          int count) throws ReaderException {
@@ -112,9 +146,9 @@ final class DecodedBitStreamParser {
     }
     // Shift_JIS may not be supported in some environments:
     try {
-      result.append(new String(buffer, "Shift_JIS"));
+      result.append(new String(buffer, SHIFT_JIS));
     } catch (UnsupportedEncodingException uee) {
-      throw new ReaderException("Can't decode SHIFT_JIS string: " + uee);
+      throw new ReaderException(SHIFT_JIS + " encoding is not supported on this device");
     }
   }
 
@@ -194,32 +228,55 @@ final class DecodedBitStreamParser {
     if (ASSUME_SHIFT_JIS) {
       return SHIFT_JIS;
     }
-    // For now, merely tries to distinguish ISO-8859-1 and Shift_JIS,
+    // Does it start with the UTF-8 byte order mark? then guess it's UTF-8
+    if (bytes.length > 3 && bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
+      return UTF8;
+    }
+    // For now, merely tries to distinguish ISO-8859-1, UTF-8 and Shift_JIS,
     // which should be by far the most common encodings. ISO-8859-1
     // should not have bytes in the 0x80 - 0x9F range, while Shift_JIS
     // uses this as a first byte of a two-byte character. If we see this
     // followed by a valid second byte in Shift_JIS, assume it is Shift_JIS.
+    // If we see something else in that second byte, we'll make the risky guess
+    // that it's UTF-8.
     int length = bytes.length;
+    boolean canBeISO88591 = true;
+    boolean lastWasPossibleDoubleByteStart = false;
     for (int i = 0; i < length; i++) {
       int value = bytes[i] & 0xFF;
       if (value >= 0x80 && value <= 0x9F && i < length - 1) {
+        canBeISO88591 = false;
         // ISO-8859-1 shouldn't use this, but before we decide it is Shift_JIS,
         // just double check that it is followed by a byte that's valid in
         // the Shift_JIS encoding
-        int nextValue = bytes[i + 1] & 0xFF;
-        if ((value & 0x1) == 0) {
-          // if even,
-          if (nextValue >= 0x40 && nextValue <= 0x9E) {
-            return SHIFT_JIS;
-          }
+        if (lastWasPossibleDoubleByteStart) {
+          // If we just checked this and the last byte for being a valid double-byte
+          // char, don't check starting on this byte. If the this and the last byte
+          // formed a valid pair, then this shouldn't be checked to see if it starts
+          // a double byte pair of course.
+          lastWasPossibleDoubleByteStart = false;
         } else {
-          if (nextValue >= 0x9F && nextValue <= 0x7C) {
-            return SHIFT_JIS;
+          // ... otherwise do check to see if this plus the next byte form a valid
+          // double byte pair encoding a character.
+          lastWasPossibleDoubleByteStart = true;
+          int nextValue = bytes[i + 1] & 0xFF;
+          if ((value & 0x1) == 0) {
+            // if even, next value should be in [0x9F,0xFC]
+            // if not, we'll guess UTF-8
+            if (nextValue < 0x9F || nextValue > 0xFC) {
+              return UTF8;
+            }
+          } else {
+            // if odd, next value should be in [0x40,0x9E]
+            // if not, we'll guess UTF-8
+            if (nextValue < 0x40 || nextValue > 0x9E) {
+              return UTF8;
+            }
           }
         }
       }
     }
-    return "ISO-8859-1";
+    return canBeISO88591 ? ISO88591 : SHIFT_JIS;
   }
 
 }