Added, at least, parsing of ECI mode in QR Code
[zxing.git] / core / src / com / google / zxing / qrcode / decoder / DecodedBitStreamParser.java
index 8fa91f4..b6dda58 100644 (file)
 package com.google.zxing.qrcode.decoder;
 
 import com.google.zxing.ReaderException;
+import com.google.zxing.common.BitSource;
 
 import java.io.UnsupportedEncodingException;
 
 /**
- * See ISO 18004:2006, 6.4.3 - 6.4.7
+ * <p>QR Codes can encode text as bits in one of several modes, and can use multiple modes
+ * in one QR Code. This class decodes the bits back into text.</p>
+ *
+ * <p>See ISO 18004:2006, 6.4.3 - 6.4.7</p>
  *
  * @author srowen@google.com (Sean Owen)
  */
@@ -30,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() {
@@ -54,23 +60,39 @@ final class DecodedBitStreamParser {
     Mode mode;
     do {
       // While still another segment to read...
-      mode = Mode.forBits(bits.readBits(4));
+      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)) {
-        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: " + mode);
+          // 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));
 
+    // I thought it wasn't allowed to leave extra bytes after the terminator but it happens
     /*
     int bitsLeft = bits.available();
     if (bitsLeft > 0) {
@@ -82,12 +104,32 @@ 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 {
+    // Each character will require 2 bytes. Read the characters as 2-byte pairs
+    // and decode as Shift_JIS afterwards
     byte[] buffer = new byte[2 * count];
     int offset = 0;
     while (count > 0) {
+      // Each 13 bits encodes a 2-byte character
       int twoBytes = bits.readBits(13);
       int assembledTwoBytes = ((twoBytes / 0x0C0) << 8) | (twoBytes % 0x0C0);
       if (assembledTwoBytes < 0x01F00) {
@@ -104,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");
     }
   }
 
@@ -144,7 +186,7 @@ final class DecodedBitStreamParser {
       count -= 2;
     }
     if (count == 1) {
-      // special case on char left
+      // special case: one character left
       result.append(ALPHANUMERIC_CHARS[bits.readBits(6)]);
     }
   }
@@ -152,7 +194,9 @@ final class DecodedBitStreamParser {
   private static void decodeNumericSegment(BitSource bits,
                                            StringBuffer result,
                                            int count) throws ReaderException {
+    // Read three digits at a time
     while (count >= 3) {
+      // Each 10 bits encodes three digits
       int threeDigitsBits = bits.readBits(10);
       if (threeDigitsBits >= 1000) {
         throw new ReaderException("Illegal value for 3-digit unit: " + threeDigitsBits);
@@ -163,6 +207,7 @@ final class DecodedBitStreamParser {
       count -= 3;
     }
     if (count == 2) {
+      // Two digits left over to read, encoded in 7 bits
       int twoDigitsBits = bits.readBits(7);
       if (twoDigitsBits >= 100) {
         throw new ReaderException("Illegal value for 2-digit unit: " + twoDigitsBits);
@@ -170,6 +215,7 @@ final class DecodedBitStreamParser {
       result.append(ALPHANUMERIC_CHARS[twoDigitsBits / 10]);
       result.append(ALPHANUMERIC_CHARS[twoDigitsBits % 10]);
     } else if (count == 1) {
+      // One digit left over to read
       int digitBits = bits.readBits(4);
       if (digitBits >= 10) {
         throw new ReaderException("Illegal value for digit unit: " + digitBits);
@@ -182,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;
   }
 
 }