commit bb61a6680f667e8a5587da67e7b7bee03ffa8b90
Author: xuenhua Encapsulates the result of decoding a barcode within an image. 2D barcode formats typically encode text, but allow for a sort of 'byte mode'
+ * which is sometimes used to encode binary data. While {@link Result} makes available
+ * the complete raw bytes in the barcode for these formats, it does not offer the bytes
+ * from the byte segments alone. This maps to a {@link java.util.Vector} of byte arrays corresponding to the
+ * raw bytes in the byte segments in the barcode, in order. Encapsulates a point of interest in an image containing a barcode. Typically, this
+ * would be the location of a finder pattern or the corner of the barcode, for example. Orders an array of three ResultPoints in an order [A,B,C] such that AB < AC and
+ * BC < AC and the angle between BC and BA is less than 180 degrees.
+ */
+ public static void orderBestPatterns(ResultPoint[] patterns) {
+
+ // Find distances between pattern centers
+ float zeroOneDistance = distance(patterns[0], patterns[1]);
+ float oneTwoDistance = distance(patterns[1], patterns[2]);
+ float zeroTwoDistance = distance(patterns[0], patterns[2]);
+
+ ResultPoint pointA, pointB, pointC;
+ // Assume one closest to other two is B; A and C will just be guesses at first
+ if (oneTwoDistance >= zeroOneDistance && oneTwoDistance >= zeroTwoDistance) {
+ pointB = patterns[0];
+ pointA = patterns[1];
+ pointC = patterns[2];
+ } else if (zeroTwoDistance >= oneTwoDistance && zeroTwoDistance >= zeroOneDistance) {
+ pointB = patterns[1];
+ pointA = patterns[0];
+ pointC = patterns[2];
+ } else {
+ pointB = patterns[2];
+ pointA = patterns[0];
+ pointC = patterns[1];
+ }
+
+ // Use cross product to figure out whether A and C are correct or flipped.
+ // This asks whether BC x BA has a positive z component, which is the arrangement
+ // we want for A, B, C. If it's negative, then we've got it flipped around and
+ // should swap A and C.
+ if (crossProductZ(pointA, pointB, pointC) < 0.0f) {
+ ResultPoint temp = pointA;
+ pointA = pointC;
+ pointC = temp;
+ }
+
+ patterns[0] = pointA;
+ patterns[1] = pointB;
+ patterns[2] = pointC;
+ }
+
+
+ /**
+ * @return distance between two points
+ */
+ public static float distance(ResultPoint pattern1, ResultPoint pattern2) {
+ float xDiff = pattern1.getX() - pattern2.getX();
+ float yDiff = pattern1.getY() - pattern2.getY();
+ return (float) Math.sqrt((double) (xDiff * xDiff + yDiff * yDiff));
+ }
+
+ /**
+ * Returns the z component of the cross product between vectors BC and BA.
+ */
+ private static float crossProductZ(ResultPoint pointA, ResultPoint pointB, ResultPoint pointC) {
+ float bX = pointB.x;
+ float bY = pointB.y;
+ return ((pointC.x - bX) * (pointA.y - bY)) - ((pointC.y - bY) * (pointA.x - bX));
+ }
+
+
+}
diff --git a/src/com/google/zxing/ResultPointCallback.java b/src/com/google/zxing/ResultPointCallback.java
new file mode 100644
index 0000000..0c85410
--- /dev/null
+++ b/src/com/google/zxing/ResultPointCallback.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2009 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;
+
+/**
+ * Callback which is invoked when a possible result point (significant
+ * point in the barcode image such as a corner) is found.
+ *
+ * @see DecodeHintType#NEED_RESULT_POINT_CALLBACK
+ */
+public interface ResultPointCallback {
+
+ void foundPossibleResultPoint(ResultPoint point);
+
+}
diff --git a/src/com/google/zxing/Writer.java b/src/com/google/zxing/Writer.java
new file mode 100644
index 0000000..6474ca7
--- /dev/null
+++ b/src/com/google/zxing/Writer.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import com.google.zxing.common.BitMatrix;
+
+import java.util.Hashtable;
+
+/**
+ * The base class for all objects which encode/generate a barcode image.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public interface Writer {
+
+ /**
+ * Encode a barcode using the default settings.
+ *
+ * @param contents The contents to encode in the barcode
+ * @param format The barcode format to generate
+ * @param width The preferred width in pixels
+ * @param height The preferred height in pixels
+ * @return The generated barcode as a Matrix of unsigned bytes (0 == black, 255 == white)
+ */
+ BitMatrix encode(String contents, BarcodeFormat format, int width, int height)
+ throws WriterException;
+
+ /**
+ *
+ * @param contents The contents to encode in the barcode
+ * @param format The barcode format to generate
+ * @param width The preferred width in pixels
+ * @param height The preferred height in pixels
+ * @param hints Additional parameters to supply to the encoder
+ * @return The generated barcode as a Matrix of unsigned bytes (0 == black, 255 == white)
+ */
+ BitMatrix encode(String contents, BarcodeFormat format, int width, int height, Hashtable hints)
+ throws WriterException;
+
+}
diff --git a/src/com/google/zxing/WriterException.java b/src/com/google/zxing/WriterException.java
new file mode 100644
index 0000000..0c19af0
--- /dev/null
+++ b/src/com/google/zxing/WriterException.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * A base class which covers the range of exceptions which may occur when encoding a barcode using
+ * the Writer framework.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class WriterException extends Exception {
+
+ public WriterException() {
+ super();
+ }
+
+ public WriterException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/com/google/zxing/client/j2me/LCDUIImageLuminanceSource.java b/src/com/google/zxing/client/j2me/LCDUIImageLuminanceSource.java
new file mode 100644
index 0000000..4f929f2
--- /dev/null
+++ b/src/com/google/zxing/client/j2me/LCDUIImageLuminanceSource.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2009 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.client.j2me;
+
+import com.google.zxing.LuminanceSource;
+
+import javax.microedition.lcdui.Image;
+
+/**
+ * A LuminanceSource based on Java ME's Image class. It does not support cropping or rotation.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ * @author Sean Owen
+ */
+public final class LCDUIImageLuminanceSource extends LuminanceSource {
+
+ private final Image image;
+ private int[] rgbData;
+
+ public LCDUIImageLuminanceSource(Image image) {
+ super(image.getWidth(), image.getHeight());
+ this.image = image;
+ }
+
+ // Instead of multiplying by 306, 601, 117, we multiply by 256, 512, 256, so that
+ // the multiplies can be implemented as shifts.
+ //
+ // Really, it's:
+ //
+ // return ((((pixel >> 16) & 0xFF) << 8) +
+ // (((pixel >> 8) & 0xFF) << 9) +
+ // (( pixel & 0xFF) << 8)) >> 10;
+ //
+ // That is, we're replacing the coefficients in the original with powers of two,
+ // which can be implemented as shifts, even though changing the coefficients slightly
+ // alters the conversion. The difference is not significant for our purposes.
+ public byte[] getRow(int y, byte[] row) {
+ if (y < 0 || y >= getHeight()) {
+ throw new IllegalArgumentException("Requested row is outside the image: " + y);
+ }
+ int width = getWidth();
+ if (row == null || row.length < width) {
+ row = new byte[width];
+ }
+
+ if (rgbData == null || rgbData.length < width) {
+ rgbData = new int[width];
+ }
+ image.getRGB(rgbData, 0, width, 0, y, width, 1);
+ for (int x = 0; x < width; x++) {
+ int pixel = rgbData[x];
+ int luminance = (((pixel & 0x00FF0000) >> 16) +
+ ((pixel & 0x0000FF00) >> 7) +
+ (pixel & 0x000000FF )) >> 2;
+ row[x] = (byte) luminance;
+ }
+ return row;
+ }
+
+ public byte[] getMatrix() {
+ int width = getWidth();
+ int height = getHeight();
+ int area = width * height;
+ byte[] matrix = new byte[area];
+
+ int[] rgb = new int[area];
+ image.getRGB(rgb, 0, width, 0, 0, width, height);
+ for (int y = 0; y < height; y++) {
+ int offset = y * width;
+ for (int x = 0; x < width; x++) {
+ int pixel = rgb[offset + x];
+ int luminance = (((pixel & 0x00FF0000) >> 16) +
+ ((pixel & 0x0000FF00) >> 7) +
+ (pixel & 0x000000FF )) >> 2;
+ matrix[offset + x] = (byte) luminance;
+ }
+ }
+ return matrix;
+ }
+
+}
diff --git a/src/com/google/zxing/client/j2me/PNGEncoder.java b/src/com/google/zxing/client/j2me/PNGEncoder.java
new file mode 100644
index 0000000..2bfc5fb
--- /dev/null
+++ b/src/com/google/zxing/client/j2me/PNGEncoder.java
@@ -0,0 +1,262 @@
+package com.google.zxing.client.j2me;
+/*
+ * Minimal PNG encoder to create PNG streams (and MIDP images) from RGBA arrays.
+ *
+ * Copyright 2006-2009 Christian Fröschlin
+ *
+ * www.chrfr.de
+ *
+ *
+ * Changelog:
+ *
+ * 09/22/08: Fixed Adler checksum calculation and byte order for storing length
+ * of zlib deflate block. Thanks to Miloslav Ruzicka for noting this.
+ *
+ * 05/12/09: Split PNG and ZLIB functionality into separate classes. Added
+ * support for images > 64K by splitting the data into multiple uncompressed
+ * deflate blocks.
+ *
+ * 03/19/10: Re-packaged, and modified interface to be more MIDP-2 friendly,
+ * using int[] rather than byte[] data. Alpha channel is now optional, to
+ * allow a smaller output size. New toPNG() method works directly from an
+ * Image object. (Graham Hughes, for Forum Nokia)
+ *
+ * Terms of Use:
+ *
+ * You may use the PNG encoder free of charge for any purpose you desire, as
+ * long as you do not claim credit for the original sources and agree not to
+ * hold me responsible for any damage arising out of its use.
+ *
+ * If you have a suitable location in GUI or documentation for giving credit,
+ * I'd appreciate a mention of
+ *
+ * PNG encoder (C) 2006-2009 by Christian Fröschlin, www.chrfr.de
+ *
+ * but that's not mandatory.
+ *
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import javax.microedition.lcdui.Image;
+
+public class PNGEncoder {
+ private static final byte[] SIGNATURE = new byte[] { (byte) 137, (byte) 80,
+ (byte) 78, (byte) 71, (byte) 13, (byte) 10, (byte) 26, (byte) 10 };
+
+ /**
+ * Generate a PNG data stream from a pixel array.
+ *
+ * Setting processAlpha to false will result in a PNG file that contains no
+ * transparency information, but may be up to 25% smaller.
+ *
+ * The pixel array must contain (width * height) pixels.
+ *
+ * @param width
+ * width of image, in pixels
+ * @param height
+ * height of image, in pixels
+ * @param argb
+ * pixel array, as populated from Image.getRGB()
+ * @param processAlpha
+ * true if you want to keep alpha channel data
+ * @return PNG data in a byte[]
+ * @throws IllegalArgumentException
+ * if the size of the pixel array does not match the specified
+ * width and height
+ */
+ public static byte[] toPNG(int width, int height, int[] argb,
+ boolean processAlpha) throws IllegalArgumentException {
+ ByteArrayOutputStream png;
+ try {
+ byte[] header = createHeaderChunk(width, height, processAlpha);
+ byte[] data = createDataChunk(width, height, argb, processAlpha);
+ byte[] trailer = createTrailerChunk();
+
+ png = new ByteArrayOutputStream(SIGNATURE.length + header.length
+ + data.length + trailer.length);
+ png.write(SIGNATURE);
+ png.write(header);
+ png.write(data);
+ png.write(trailer);
+ } catch (IOException ioe) {
+ // none of the code should ever throw an IOException
+ throw new IllegalStateException("Unexpected " + ioe);
+ }
+ return png.toByteArray();
+ }
+
+ /**
+ * Generate a PNG data stream from an Image object.
+ *
+ * @param img
+ * source Image
+ * @param processAlpha
+ * true if you want to keep the alpha channel data
+ * @return PNG data in a byte[]
+ */
+ public static byte[] toPNG(Image img, boolean processAlpha) {
+ int width = img.getWidth();
+ int height = img.getHeight();
+ int[] argb = new int[width * height];
+ img.getRGB(argb, 0, width, 0, 0, width, height);
+ // allow garbage collection, if this is the only reference
+ img = null;
+ return toPNG(width, height, argb, processAlpha);
+ }
+
+ private static byte[] createHeaderChunk(int width, int height,
+ boolean processAlpha) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(13);
+ DataOutputStream chunk = new DataOutputStream(baos);
+ chunk.writeInt(width);
+ chunk.writeInt(height);
+ chunk.writeByte(8); // Bitdepth
+ chunk.writeByte(processAlpha ? 6 : 2); // Colortype ARGB or RGB
+ chunk.writeByte(0); // Compression
+ chunk.writeByte(0); // Filter
+ chunk.writeByte(0); // Interlace
+ return toChunk("IHDR", baos.toByteArray());
+ }
+
+ private static byte[] createDataChunk(int width, int height, int[] argb,
+ boolean processAlpha) throws IOException, IllegalArgumentException {
+ if (argb.length != (width * height)) {
+ throw new IllegalArgumentException(
+ "array size does not match image dimensions");
+ }
+ int source = 0;
+ int dest = 0;
+ byte[] raw = new byte[(processAlpha ? 4 : 3) * (width * height)
+ + height];
+ for (int y = 0; y < height; y++) {
+ raw[dest++] = 0; // No filter
+ for (int x = 0; x < width; x++) {
+ int pixel = argb[source++];
+ raw[dest++] = (byte) (pixel >> 16); // red
+ raw[dest++] = (byte) (pixel >> 8); // green
+ raw[dest++] = (byte) (pixel); // blue
+ if (processAlpha) {
+ raw[dest++] = (byte) (pixel >> 24); // alpha
+ }
+ }
+ }
+ return toChunk("IDAT", toZLIB(raw));
+ }
+
+ private static byte[] createTrailerChunk() throws IOException {
+ return toChunk("IEND", new byte[] {});
+ }
+
+ private static byte[] toChunk(String id, byte[] raw) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(raw.length + 12);
+ DataOutputStream chunk = new DataOutputStream(baos);
+
+ chunk.writeInt(raw.length);
+
+ byte[] bid = new byte[4];
+ for (int i = 0; i < 4; i++) {
+ bid[i] = (byte) id.charAt(i);
+ }
+
+ chunk.write(bid);
+
+ chunk.write(raw);
+
+ int crc = 0xFFFFFFFF;
+ crc = updateCRC(crc, bid);
+ crc = updateCRC(crc, raw);
+ chunk.writeInt(~crc);
+
+ return baos.toByteArray();
+ }
+
+ private static int[] crcTable = null;
+
+ private static void createCRCTable() {
+ crcTable = new int[256];
+
+ for (int i = 0; i < 256; i++) {
+ int c = i;
+ for (int k = 0; k < 8; k++) {
+ c = ((c & 1) > 0) ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
+ }
+ crcTable[i] = c;
+ }
+ }
+
+ private static int updateCRC(int crc, byte[] raw) {
+ if (crcTable == null) {
+ createCRCTable();
+ }
+
+ for (int i = 0; i < raw.length; i++) {
+ crc = crcTable[(crc ^ raw[i]) & 0xFF] ^ (crc >>> 8);
+ }
+
+ return crc;
+ }
+
+ /*
+ * This method is called to encode the image data as a zlib block as
+ * required by the PNG specification. This file comes with a minimal ZLIB
+ * encoder which uses uncompressed deflate blocks (fast, short, easy, but no
+ * compression). If you want compression, call another encoder (such as
+ * JZLib?) here.
+ */
+ private static byte[] toZLIB(byte[] raw) throws IOException {
+ return ZLIB.toZLIB(raw);
+ }
+}
+
+class ZLIB {
+ private static final int BLOCK_SIZE = 32000;
+
+ public static byte[] toZLIB(byte[] raw) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(raw.length + 6
+ + (raw.length / BLOCK_SIZE) * 5);
+ DataOutputStream zlib = new DataOutputStream(baos);
+
+ byte tmp = (byte) 8;
+ zlib.writeByte(tmp); // CM = 8, CMINFO = 0
+ zlib.writeByte((31 - ((tmp << 8) % 31)) % 31); // FCHECK(FDICT/FLEVEL=0)
+
+ int pos = 0;
+ while (raw.length - pos > BLOCK_SIZE) {
+ writeUncompressedDeflateBlock(zlib, false, raw, pos,
+ (char) BLOCK_SIZE);
+ pos += BLOCK_SIZE;
+ }
+
+ writeUncompressedDeflateBlock(zlib, true, raw, pos,
+ (char) (raw.length - pos));
+
+ // zlib check sum of uncompressed data
+ zlib.writeInt(calcADLER32(raw));
+
+ return baos.toByteArray();
+ }
+
+ private static void writeUncompressedDeflateBlock(DataOutputStream zlib,
+ boolean last, byte[] raw, int off, char len) throws IOException {
+ zlib.writeByte((byte) (last ? 1 : 0)); // Final flag, Compression type 0
+ zlib.writeByte((byte) (len & 0xFF)); // Length LSB
+ zlib.writeByte((byte) ((len & 0xFF00) >> 8)); // Length MSB
+ zlib.writeByte((byte) (~len & 0xFF)); // Length 1st complement LSB
+ zlib.writeByte((byte) ((~len & 0xFF00) >> 8)); // Length 1st complement
+ // MSB
+ zlib.write(raw, off, len); // Data
+ }
+
+ private static int calcADLER32(byte[] raw) {
+ int s1 = 1;
+ int s2 = 0;
+ for (int i = 0; i < raw.length; i++) {
+ int abs = raw[i] >= 0 ? raw[i] : (raw[i] + 256);
+ s1 = (s1 + abs) % 65521;
+ s2 = (s2 + s1) % 65521;
+ }
+ return (s2 << 16) + s1;
+ }
+}
diff --git a/src/com/google/zxing/client/j2me/ZXMIDlet.java b/src/com/google/zxing/client/j2me/ZXMIDlet.java
new file mode 100644
index 0000000..b7bd2e9
--- /dev/null
+++ b/src/com/google/zxing/client/j2me/ZXMIDlet.java
@@ -0,0 +1,670 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.google.zxing.client.j2me;
+
+import org.netbeans.microedition.lcdui.SplashScreen;
+import org.netbeans.microedition.lcdui.pda.FileBrowser;
+import java.io.*;
+import javax.microedition.io.file.*;
+import javax.microedition.midlet.*;
+import javax.microedition.lcdui.*;
+import com.google.zxing.*;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.common.HybridBinarizer;
+import com.google.zxing.qrcode.QRCodeWriter;
+import java.util.Date;
+import org.netbeans.microedition.lcdui.SplashScreen;
+import org.netbeans.microedition.lcdui.pda.FileBrowser;
+
+/**
+ * @author URSER
+ */
+public class ZXMIDlet extends MIDlet implements CommandListener {
+
+private boolean midletPaused = false;
+private Image img;
+private Image qrCodeImage;
+private String filename;
+private static final int BACK = 0xFF000000;
+private static final int WHTIE = 0xFFFFFFFF;
+// See
+ *
+ * DoCoMo's documentation about the result types represented by subclasses of this class. Thanks to Jeff Griffin for proposing rewrite of these classes that relies less
+ * on exception-based mechanisms during parsing. We would return the start and end date as a {@link java.util.Date} except that this code
+ * needs to work under JavaME / MIDP and there is no date parsing library available there, such
+ * as null
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * @return raw bytes encoded by the barcode, if applicable, otherwise null
+ */
+ public byte[] getRawBytes() {
+ return rawBytes;
+ }
+
+ /**
+ * @return points related to the barcode in the image. These are typically points
+ * identifying finder patterns or the corners of the barcode. The exact meaning is
+ * specific to the type of barcode that was decoded.
+ */
+ public ResultPoint[] getResultPoints() {
+ return resultPoints;
+ }
+
+ /**
+ * @return {@link BarcodeFormat} representing the format of the barcode that was decoded
+ */
+ public BarcodeFormat getBarcodeFormat() {
+ return format;
+ }
+
+ /**
+ * @return {@link Hashtable} mapping {@link ResultMetadataType} keys to values. May be
+ * null
. This contains optional metadata about what was detected about the barcode,
+ * like orientation.
+ */
+ public Hashtable getResultMetadata() {
+ return resultMetadata;
+ }
+
+ public void putMetadata(ResultMetadataType type, Object value) {
+ if (resultMetadata == null) {
+ resultMetadata = new Hashtable(3);
+ }
+ resultMetadata.put(type, value);
+ }
+
+ public void putAllMetadata(Hashtable metadata) {
+ if (metadata != null) {
+ if (resultMetadata == null) {
+ resultMetadata = metadata;
+ } else {
+ Enumeration e = metadata.keys();
+ while (e.hasMoreElements()) {
+ ResultMetadataType key = (ResultMetadataType) e.nextElement();
+ Object value = metadata.get(key);
+ resultMetadata.put(key, value);
+ }
+ }
+ }
+ }
+
+ public void addResultPoints(ResultPoint[] newPoints) {
+ if (resultPoints == null) {
+ resultPoints = newPoints;
+ } else if (newPoints != null && newPoints.length > 0) {
+ ResultPoint[] allPoints = new ResultPoint[resultPoints.length + newPoints.length];
+ System.arraycopy(resultPoints, 0, allPoints, 0, resultPoints.length);
+ System.arraycopy(newPoints, 0, allPoints, resultPoints.length, newPoints.length);
+ resultPoints = allPoints;
+ }
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String toString() {
+ if (text == null) {
+ return "[" + rawBytes.length + " bytes]";
+ } else {
+ return text;
+ }
+ }
+
+}
diff --git a/src/com/google/zxing/ResultMetadataType.java b/src/com/google/zxing/ResultMetadataType.java
new file mode 100644
index 0000000..33d69d9
--- /dev/null
+++ b/src/com/google/zxing/ResultMetadataType.java
@@ -0,0 +1,109 @@
+/*
+ * 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;
+
+import java.util.Hashtable;
+
+/**
+ * Represents some type of metadata about the result of the decoding that the decoder
+ * wishes to communicate back to the caller.
+ *
+ * @author Sean Owen
+ */
+public final class ResultMetadataType {
+
+ // No, we can't use an enum here. J2ME doesn't support it.
+
+ private static final Hashtable VALUES = new Hashtable();
+
+ // No, we can't use an enum here. J2ME doesn't support it.
+
+ /**
+ * Unspecified, application-specific metadata. Maps to an unspecified {@link Object}.
+ */
+ public static final ResultMetadataType OTHER = new ResultMetadataType("OTHER");
+
+ /**
+ * Denotes the likely approximate orientation of the barcode in the image. This value
+ * is given as degrees rotated clockwise from the normal, upright orientation.
+ * For example a 1D barcode which was found by reading top-to-bottom would be
+ * said to have orientation "90". This key maps to an {@link Integer} whose
+ * value is in the range [0,360).
+ */
+ public static final ResultMetadataType ORIENTATION = new ResultMetadataType("ORIENTATION");
+
+ /**
+ * startMIDlet
method.
+ */
+ private void initialize() {//GEN-END:|0-initialize|0|0-preInitialize
+ // write pre-initialize user code here
+//GEN-LINE:|0-initialize|1|0-postInitialize
+ // write post-initialize user code here
+ }//GEN-BEGIN:|0-initialize|2|
+ //display
instance is taken from getDisplay
method. This method is used by all actions in the design for switching displayable.
+ * @param alert the Alert which is temporarily set to the display; if null
, then nextDisplayable
is set immediately
+ * @param nextDisplayable the Displayable to be set
+ */
+ public void switchDisplayable(Alert alert, Displayable nextDisplayable) {//GEN-END:|5-switchDisplayable|0|5-preSwitch
+ // write pre-switch user code here
+ Display display = getDisplay();//GEN-BEGIN:|5-switchDisplayable|1|5-postSwitch
+ if (alert == null) {
+ display.setCurrent(nextDisplayable);
+ } else {
+ display.setCurrent(alert, nextDisplayable);
+ }//GEN-END:|5-switchDisplayable|1|5-postSwitch
+ // write post-switch user code here
+ }//GEN-BEGIN:|5-switchDisplayable|2|
+ //java.text.SimpleDateFormat
.
Abstract class representing the result of decoding a barcode, as more than + * a String -- as some type of structured data. This might be a subclass which represents + * a URL, or an e-mail address. {@link ResultParser#parseResult(Result)} will turn a raw + * decoded string into the most appropriate type of structured representation.
+ * + *Thanks to Jeff Griffin for proposing rewrite of these classes that relies less + * on exception-based mechanisms during parsing.
+ * + * @author Sean Owen + */ +public abstract class ParsedResult { + + private final ParsedResultType type; + + protected ParsedResult(ParsedResultType type) { + this.type = type; + } + + public ParsedResultType getType() { + return type; + } + + public abstract String getDisplayResult(); + + public String toString() { + return getDisplayResult(); + } + + public static void maybeAppend(String value, StringBuffer result) { + if (value != null && value.length() > 0) { + // Don't add a newline before the first value + if (result.length() > 0) { + result.append('\n'); + } + result.append(value); + } + } + + public static void maybeAppend(String[] value, StringBuffer result) { + if (value != null) { + for (int i = 0; i < value.length; i++) { + if (value[i] != null && value[i].length() > 0) { + if (result.length() > 0) { + result.append('\n'); + } + result.append(value[i]); + } + } + } + } + +} diff --git a/src/com/google/zxing/client/result/ParsedResultType.java b/src/com/google/zxing/client/result/ParsedResultType.java new file mode 100644 index 0000000..92c81cb --- /dev/null +++ b/src/com/google/zxing/client/result/ParsedResultType.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010 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.client.result; + +/** + * Represents the type of data encoded by a barcode -- from plain text, to a + * URI, to an e-mail address, etc. + * + * @author Sean Owen + */ +public final class ParsedResultType { + + public static final ParsedResultType ADDRESSBOOK = new ParsedResultType("ADDRESSBOOK"); + public static final ParsedResultType EMAIL_ADDRESS = new ParsedResultType("EMAIL_ADDRESS"); + public static final ParsedResultType PRODUCT = new ParsedResultType("PRODUCT"); + public static final ParsedResultType URI = new ParsedResultType("URI"); + public static final ParsedResultType TEXT = new ParsedResultType("TEXT"); + public static final ParsedResultType ANDROID_INTENT = new ParsedResultType("ANDROID_INTENT"); + public static final ParsedResultType GEO = new ParsedResultType("GEO"); + public static final ParsedResultType TEL = new ParsedResultType("TEL"); + public static final ParsedResultType SMS = new ParsedResultType("SMS"); + public static final ParsedResultType CALENDAR = new ParsedResultType("CALENDAR"); + public static final ParsedResultType WIFI = new ParsedResultType("WIFI"); + // "optional" types + public static final ParsedResultType NDEF_SMART_POSTER = new ParsedResultType("NDEF_SMART_POSTER"); + public static final ParsedResultType MOBILETAG_RICH_WEB = new ParsedResultType("MOBILETAG_RICH_WEB"); + public static final ParsedResultType ISBN = new ParsedResultType("ISBN"); + + private final String name; + + private ParsedResultType(String name) { + this.name = name; + } + + public String toString() { + return name; + } + +} diff --git a/src/com/google/zxing/client/result/ProductParsedResult.java b/src/com/google/zxing/client/result/ProductParsedResult.java new file mode 100644 index 0000000..9014ca8 --- /dev/null +++ b/src/com/google/zxing/client/result/ProductParsedResult.java @@ -0,0 +1,49 @@ +/* + * Copyright 2007 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.client.result; + +/** + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ProductParsedResult extends ParsedResult { + + private final String productID; + private final String normalizedProductID; + + ProductParsedResult(String productID) { + this(productID, productID); + } + + ProductParsedResult(String productID, String normalizedProductID) { + super(ParsedResultType.PRODUCT); + this.productID = productID; + this.normalizedProductID = normalizedProductID; + } + + public String getProductID() { + return productID; + } + + public String getNormalizedProductID() { + return normalizedProductID; + } + + public String getDisplayResult() { + return productID; + } + +} diff --git a/src/com/google/zxing/client/result/ProductResultParser.java b/src/com/google/zxing/client/result/ProductResultParser.java new file mode 100644 index 0000000..0ca23b2 --- /dev/null +++ b/src/com/google/zxing/client/result/ProductResultParser.java @@ -0,0 +1,66 @@ +/* + * Copyright 2007 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.client.result; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.Result; +import com.google.zxing.oned.UPCEReader; + +/** + * Parses strings of digits that represent a UPC code. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +final class ProductResultParser extends ResultParser { + + private ProductResultParser() { + } + + // Treat all UPC and EAN variants as UPCs, in the sense that they are all product barcodes. + public static ProductParsedResult parse(Result result) { + BarcodeFormat format = result.getBarcodeFormat(); + if (!(BarcodeFormat.UPC_A.equals(format) || BarcodeFormat.UPC_E.equals(format) || + BarcodeFormat.EAN_8.equals(format) || BarcodeFormat.EAN_13.equals(format))) { + return null; + } + // Really neither of these should happen: + String rawText = result.getText(); + if (rawText == null) { + return null; + } + + int length = rawText.length(); + for (int x = 0; x < length; x++) { + char c = rawText.charAt(x); + if (c < '0' || c > '9') { + return null; + } + } + // Not actually checking the checksum again here + + String normalizedProductID; + // Expand UPC-E for purposes of searching + if (BarcodeFormat.UPC_E.equals(format)) { + normalizedProductID = UPCEReader.convertUPCEtoUPCA(rawText); + } else { + normalizedProductID = rawText; + } + + return new ProductParsedResult(rawText, normalizedProductID); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/ResultParser.java b/src/com/google/zxing/client/result/ResultParser.java new file mode 100644 index 0000000..df30865 --- /dev/null +++ b/src/com/google/zxing/client/result/ResultParser.java @@ -0,0 +1,319 @@ +/* + * Copyright 2007 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.client.result; + +import com.google.zxing.Result; + +import java.util.Hashtable; +import java.util.Vector; + +/** + *Abstract class representing the result of decoding a barcode, as more than + * a String -- as some type of structured data. This might be a subclass which represents + * a URL, or an e-mail address. {@link #parseResult(com.google.zxing.Result)} will turn a raw + * decoded string into the most appropriate type of structured representation.
+ * + *Thanks to Jeff Griffin for proposing rewrite of these classes that relies less + * on exception-based mechanisms during parsing.
+ * + * @author Sean Owen + */ +public abstract class ResultParser { + + public static ParsedResult parseResult(Result theResult) { + // This is a bit messy, but given limited options in MIDP / CLDC, this may well be the simplest + // way to go about this. For example, we have no reflection available, really. + // Order is important here. + ParsedResult result; + if ((result = BookmarkDoCoMoResultParser.parse(theResult)) != null) { + return result; + } else if ((result = AddressBookDoCoMoResultParser.parse(theResult)) != null) { + return result; + } else if ((result = EmailDoCoMoResultParser.parse(theResult)) != null) { + return result; + } else if ((result = AddressBookAUResultParser.parse(theResult)) != null) { + return result; + } else if ((result = VCardResultParser.parse(theResult)) != null) { + return result; + } else if ((result = BizcardResultParser.parse(theResult)) != null) { + return result; + } else if ((result = VEventResultParser.parse(theResult)) != null) { + return result; + } else if ((result = EmailAddressResultParser.parse(theResult)) != null) { + return result; + } else if ((result = TelResultParser.parse(theResult)) != null) { + return result; + } else if ((result = SMSMMSResultParser.parse(theResult)) != null) { + return result; + } else if ((result = SMSTOMMSTOResultParser.parse(theResult)) != null) { + return result; + } else if ((result = GeoResultParser.parse(theResult)) != null) { + return result; + } else if ((result = WifiResultParser.parse(theResult)) != null) { + return result; + } else if ((result = URLTOResultParser.parse(theResult)) != null) { + return result; + } else if ((result = URIResultParser.parse(theResult)) != null) { + return result; + } else if ((result = ISBNResultParser.parse(theResult)) != null) { + // We depend on ISBN parsing coming before UPC, as it is a subset. + return result; + } else if ((result = ProductResultParser.parse(theResult)) != null) { + return result; + } else if ((result = ExpandedProductResultParser.parse(theResult)) != null) { + return result; + } + return new TextParsedResult(theResult.getText(), null); + } + + protected static void maybeAppend(String value, StringBuffer result) { + if (value != null) { + result.append('\n'); + result.append(value); + } + } + + protected static void maybeAppend(String[] value, StringBuffer result) { + if (value != null) { + for (int i = 0; i < value.length; i++) { + result.append('\n'); + result.append(value[i]); + } + } + } + + protected static String[] maybeWrap(String value) { + return value == null ? null : new String[] { value }; + } + + protected static String unescapeBackslash(String escaped) { + if (escaped != null) { + int backslash = escaped.indexOf((int) '\\'); + if (backslash >= 0) { + int max = escaped.length(); + StringBuffer unescaped = new StringBuffer(max - 1); + unescaped.append(escaped.toCharArray(), 0, backslash); + boolean nextIsEscaped = false; + for (int i = backslash; i < max; i++) { + char c = escaped.charAt(i); + if (nextIsEscaped || c != '\\') { + unescaped.append(c); + nextIsEscaped = false; + } else { + nextIsEscaped = true; + } + } + return unescaped.toString(); + } + } + return escaped; + } + + private static String urlDecode(String escaped) { + + // No we can't use java.net.URLDecoder here. JavaME doesn't have it. + if (escaped == null) { + return null; + } + char[] escapedArray = escaped.toCharArray(); + + int first = findFirstEscape(escapedArray); + if (first < 0) { + return escaped; + } + + int max = escapedArray.length; + // final length is at most 2 less than original due to at least 1 unescaping + StringBuffer unescaped = new StringBuffer(max - 2); + // Can append everything up to first escape character + unescaped.append(escapedArray, 0, first); + + for (int i = first; i < max; i++) { + char c = escapedArray[i]; + if (c == '+') { + // + is translated directly into a space + unescaped.append(' '); + } else if (c == '%') { + // Are there even two more chars? if not we will just copy the escaped sequence and be done + if (i >= max - 2) { + unescaped.append('%'); // append that % and move on + } else { + int firstDigitValue = parseHexDigit(escapedArray[++i]); + int secondDigitValue = parseHexDigit(escapedArray[++i]); + if (firstDigitValue < 0 || secondDigitValue < 0) { + // bad digit, just move on + unescaped.append('%'); + unescaped.append(escapedArray[i-1]); + unescaped.append(escapedArray[i]); + } + unescaped.append((char) ((firstDigitValue << 4) + secondDigitValue)); + } + } else { + unescaped.append(c); + } + } + return unescaped.toString(); + } + + private static int findFirstEscape(char[] escapedArray) { + int max = escapedArray.length; + for (int i = 0; i < max; i++) { + char c = escapedArray[i]; + if (c == '+' || c == '%') { + return i; + } + } + return -1; + } + + private static int parseHexDigit(char c) { + if (c >= 'a') { + if (c <= 'f') { + return 10 + (c - 'a'); + } + } else if (c >= 'A') { + if (c <= 'F') { + return 10 + (c - 'A'); + } + } else if (c >= '0') { + if (c <= '9') { + return c - '0'; + } + } + return -1; + } + + protected static boolean isStringOfDigits(String value, int length) { + if (value == null) { + return false; + } + int stringLength = value.length(); + if (length != stringLength) { + return false; + } + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + + protected static boolean isSubstringOfDigits(String value, int offset, int length) { + if (value == null) { + return false; + } + int stringLength = value.length(); + int max = offset + length; + if (stringLength < max) { + return false; + } + for (int i = offset; i < max; i++) { + char c = value.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + + static Hashtable parseNameValuePairs(String uri) { + int paramStart = uri.indexOf('?'); + if (paramStart < 0) { + return null; + } + Hashtable result = new Hashtable(3); + paramStart++; + int paramEnd; + while ((paramEnd = uri.indexOf('&', paramStart)) >= 0) { + appendKeyValue(uri, paramStart, paramEnd, result); + paramStart = paramEnd + 1; + } + appendKeyValue(uri, paramStart, uri.length(), result); + return result; + } + + private static void appendKeyValue(String uri, int paramStart, int paramEnd, Hashtable result) { + int separator = uri.indexOf('=', paramStart); + if (separator >= 0) { + // key = value + String key = uri.substring(paramStart, separator); + String value = uri.substring(separator + 1, paramEnd); + value = urlDecode(value); + result.put(key, value); + } + // Can't put key, null into a hashtable + } + + static String[] matchPrefixedField(String prefix, String rawText, char endChar, boolean trim) { + Vector matches = null; + int i = 0; + int max = rawText.length(); + while (i < max) { + i = rawText.indexOf(prefix, i); + if (i < 0) { + break; + } + i += prefix.length(); // Skip past this prefix we found to start + int start = i; // Found the start of a match here + boolean done = false; + while (!done) { + i = rawText.indexOf((int) endChar, i); + if (i < 0) { + // No terminating end character? uh, done. Set i such that loop terminates and break + i = rawText.length(); + done = true; + } else if (rawText.charAt(i - 1) == '\\') { + // semicolon was escaped so continue + i++; + } else { + // found a match + if (matches == null) { + matches = new Vector(3); // lazy init + } + String element = unescapeBackslash(rawText.substring(start, i)); + if (trim) { + element = element.trim(); + } + matches.addElement(element); + i++; + done = true; + } + } + } + if (matches == null || matches.isEmpty()) { + return null; + } + return toStringArray(matches); + } + + static String matchSinglePrefixedField(String prefix, String rawText, char endChar, boolean trim) { + String[] matches = matchPrefixedField(prefix, rawText, endChar, trim); + return matches == null ? null : matches[0]; + } + + static String[] toStringArray(Vector strings) { + int size = strings.size(); + String[] result = new String[size]; + for (int j = 0; j < size; j++) { + result[j] = (String) strings.elementAt(j); + } + return result; + } + +} diff --git a/src/com/google/zxing/client/result/SMSMMSResultParser.java b/src/com/google/zxing/client/result/SMSMMSResultParser.java new file mode 100644 index 0000000..e309132 --- /dev/null +++ b/src/com/google/zxing/client/result/SMSMMSResultParser.java @@ -0,0 +1,107 @@ +/* + * 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.client.result; + +import com.google.zxing.Result; + +import java.util.Hashtable; +import java.util.Vector; + +/** + *Parses an "sms:" URI result, which specifies a number to SMS. + * See RFC 5724 on this.
+ * + *This class supports "via" syntax for numbers, which is not part of the spec. + * For example "+12125551212;via=+12124440101" may appear as a number. + * It also supports a "subject" query parameter, which is not mentioned in the spec. + * These are included since they were mentioned in earlier IETF drafts and might be + * used.
+ * + *This actually also parses URIs starting with "mms:" and treats them all the same way, + * and effectively converts them to an "sms:" URI for purposes of forwarding to the platform.
+ * + * @author Sean Owen + */ +final class SMSMMSResultParser extends ResultParser { + + private SMSMMSResultParser() { + } + + public static SMSParsedResult parse(Result result) { + String rawText = result.getText(); + if (rawText == null) { + return null; + } + if (!(rawText.startsWith("sms:") || rawText.startsWith("SMS:") || + rawText.startsWith("mms:") || rawText.startsWith("MMS:"))) { + return null; + } + + // Check up front if this is a URI syntax string with query arguments + Hashtable nameValuePairs = parseNameValuePairs(rawText); + String subject = null; + String body = null; + boolean querySyntax = false; + if (nameValuePairs != null && !nameValuePairs.isEmpty()) { + subject = (String) nameValuePairs.get("subject"); + body = (String) nameValuePairs.get("body"); + querySyntax = true; + } + + // Drop sms, query portion + int queryStart = rawText.indexOf('?', 4); + String smsURIWithoutQuery; + // If it's not query syntax, the question mark is part of the subject or message + if (queryStart < 0 || !querySyntax) { + smsURIWithoutQuery = rawText.substring(4); + } else { + smsURIWithoutQuery = rawText.substring(4, queryStart); + } + + int lastComma = -1; + int comma; + Vector numbers = new Vector(1); + Vector vias = new Vector(1); + while ((comma = smsURIWithoutQuery.indexOf(',', lastComma + 1)) > lastComma) { + String numberPart = smsURIWithoutQuery.substring(lastComma + 1, comma); + addNumberVia(numbers, vias, numberPart); + lastComma = comma; + } + addNumberVia(numbers, vias, smsURIWithoutQuery.substring(lastComma + 1)); + + return new SMSParsedResult(toStringArray(numbers), toStringArray(vias), subject, body); + } + + private static void addNumberVia(Vector numbers, Vector vias, String numberPart) { + int numberEnd = numberPart.indexOf(';'); + if (numberEnd < 0) { + numbers.addElement(numberPart); + vias.addElement(null); + } else { + numbers.addElement(numberPart.substring(0, numberEnd)); + String maybeVia = numberPart.substring(numberEnd + 1); + String via; + if (maybeVia.startsWith("via=")) { + via = maybeVia.substring(4); + } else { + via = null; + } + vias.addElement(via); + } + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/SMSParsedResult.java b/src/com/google/zxing/client/result/SMSParsedResult.java new file mode 100644 index 0000000..93792ae --- /dev/null +++ b/src/com/google/zxing/client/result/SMSParsedResult.java @@ -0,0 +1,104 @@ +/* + * 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.client.result; + +/** + * @author Sean Owen + */ +public final class SMSParsedResult extends ParsedResult { + + private final String[] numbers; + private final String[] vias; + private final String subject; + private final String body; + + public SMSParsedResult(String number, String via, String subject, String body) { + super(ParsedResultType.SMS); + this.numbers = new String[] {number}; + this.vias = new String[] {via}; + this.subject = subject; + this.body = body; + } + + public SMSParsedResult(String[] numbers, String[] vias, String subject, String body) { + super(ParsedResultType.SMS); + this.numbers = numbers; + this.vias = vias; + this.subject = subject; + this.body = body; + } + + public String getSMSURI() { + StringBuffer result = new StringBuffer(); + result.append("sms:"); + boolean first = true; + for (int i = 0; i < numbers.length; i++) { + if (first) { + first = false; + } else { + result.append(','); + } + result.append(numbers[i]); + if (vias[i] != null) { + result.append(";via="); + result.append(vias[i]); + } + } + boolean hasBody = body != null; + boolean hasSubject = subject != null; + if (hasBody || hasSubject) { + result.append('?'); + if (hasBody) { + result.append("body="); + result.append(body); + } + if (hasSubject) { + if (hasBody) { + result.append('&'); + } + result.append("subject="); + result.append(subject); + } + } + return result.toString(); + } + + public String[] getNumbers() { + return numbers; + } + + public String[] getVias() { + return vias; + } + + public String getSubject() { + return subject; + } + + public String getBody() { + return body; + } + + public String getDisplayResult() { + StringBuffer result = new StringBuffer(100); + maybeAppend(numbers, result); + maybeAppend(subject, result); + maybeAppend(body, result); + return result.toString(); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/SMSTOMMSTOResultParser.java b/src/com/google/zxing/client/result/SMSTOMMSTOResultParser.java new file mode 100644 index 0000000..76418d0 --- /dev/null +++ b/src/com/google/zxing/client/result/SMSTOMMSTOResultParser.java @@ -0,0 +1,57 @@ +/* + * 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.client.result; + +import com.google.zxing.Result; + +/** + *Parses an "smsto:" URI result, whose format is not standardized but appears to be like:
+ * smsto:number(:body)
.
This actually also parses URIs starting with "smsto:", "mmsto:", "SMSTO:", and + * "MMSTO:", and treats them all the same way, and effectively converts them to an "sms:" URI + * for purposes of forwarding to the platform.
+ * + * @author Sean Owen + */ +final class SMSTOMMSTOResultParser extends ResultParser { + + private SMSTOMMSTOResultParser() { + } + + public static SMSParsedResult parse(Result result) { + String rawText = result.getText(); + if (rawText == null) { + return null; + } + if (!(rawText.startsWith("smsto:") || rawText.startsWith("SMSTO:") || + rawText.startsWith("mmsto:") || rawText.startsWith("MMSTO:"))) { + return null; + } + // Thanks to dominik.wild for suggesting this enhancement to support + // smsto:number:body URIs + String number = rawText.substring(6); + String body = null; + int bodyStart = number.indexOf(':'); + if (bodyStart >= 0) { + body = number.substring(bodyStart + 1); + number = number.substring(0, bodyStart); + } + return new SMSParsedResult(number, null, null, body); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/TelParsedResult.java b/src/com/google/zxing/client/result/TelParsedResult.java new file mode 100644 index 0000000..a618fe7 --- /dev/null +++ b/src/com/google/zxing/client/result/TelParsedResult.java @@ -0,0 +1,54 @@ +/* + * 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.client.result; + +/** + * @author Sean Owen + */ +public final class TelParsedResult extends ParsedResult { + + private final String number; + private final String telURI; + private final String title; + + public TelParsedResult(String number, String telURI, String title) { + super(ParsedResultType.TEL); + this.number = number; + this.telURI = telURI; + this.title = title; + } + + public String getNumber() { + return number; + } + + public String getTelURI() { + return telURI; + } + + public String getTitle() { + return title; + } + + public String getDisplayResult() { + StringBuffer result = new StringBuffer(20); + maybeAppend(number, result); + maybeAppend(title, result); + return result.toString(); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/TelResultParser.java b/src/com/google/zxing/client/result/TelResultParser.java new file mode 100644 index 0000000..d3f7775 --- /dev/null +++ b/src/com/google/zxing/client/result/TelResultParser.java @@ -0,0 +1,44 @@ +/* + * 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.client.result; + +import com.google.zxing.Result; + +/** + * Parses a "tel:" URI result, which specifies a phone number. + * + * @author Sean Owen + */ +final class TelResultParser extends ResultParser { + + private TelResultParser() { + } + + public static TelParsedResult parse(Result result) { + String rawText = result.getText(); + if (rawText == null || (!rawText.startsWith("tel:") && !rawText.startsWith("TEL:"))) { + return null; + } + // Normalize "TEL:" to "tel:" + String telURI = rawText.startsWith("TEL:") ? "tel:" + rawText.substring(4) : rawText; + // Drop tel, query portion + int queryStart = rawText.indexOf('?', 4); + String number = queryStart < 0 ? rawText.substring(4) : rawText.substring(4, queryStart); + return new TelParsedResult(number, telURI, null); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/TextParsedResult.java b/src/com/google/zxing/client/result/TextParsedResult.java new file mode 100644 index 0000000..052d4d0 --- /dev/null +++ b/src/com/google/zxing/client/result/TextParsedResult.java @@ -0,0 +1,48 @@ +/* + * Copyright 2007 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.client.result; + +/** + * A simple result type encapsulating a string that has no further + * interpretation. + * + * @author Sean Owen + */ +public final class TextParsedResult extends ParsedResult { + + private final String text; + private final String language; + + public TextParsedResult(String text, String language) { + super(ParsedResultType.TEXT); + this.text = text; + this.language = language; + } + + public String getText() { + return text; + } + + public String getLanguage() { + return language; + } + + public String getDisplayResult() { + return text; + } + +} diff --git a/src/com/google/zxing/client/result/URIParsedResult.java b/src/com/google/zxing/client/result/URIParsedResult.java new file mode 100644 index 0000000..6931c45 --- /dev/null +++ b/src/com/google/zxing/client/result/URIParsedResult.java @@ -0,0 +1,113 @@ +/* + * Copyright 2007 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.client.result; + +/** + * @author Sean Owen + */ +public final class URIParsedResult extends ParsedResult { + + private final String uri; + private final String title; + + public URIParsedResult(String uri, String title) { + super(ParsedResultType.URI); + this.uri = massageURI(uri); + this.title = title; + } + + public String getURI() { + return uri; + } + + public String getTitle() { + return title; + } + + /** + * @return true if the URI contains suspicious patterns that may suggest it intends to + * mislead the user about its true nature. At the moment this looks for the presence + * of user/password syntax in the host/authority portion of a URI which may be used + * in attempts to make the URI's host appear to be other than it is. Example: + * http://yourbank.com@phisher.com This URI connects to phisher.com but may appear + * to connect to yourbank.com at first glance. + */ + public boolean isPossiblyMaliciousURI() { + return containsUser(); + } + + private boolean containsUser() { + // This method is likely not 100% RFC compliant yet + int hostStart = uri.indexOf(':'); // we should always have scheme at this point + hostStart++; + // Skip slashes preceding host + int uriLength = uri.length(); + while (hostStart < uriLength && uri.charAt(hostStart) == '/') { + hostStart++; + } + int hostEnd = uri.indexOf('/', hostStart); + if (hostEnd < 0) { + hostEnd = uriLength; + } + int at = uri.indexOf('@', hostStart); + return at >= hostStart && at < hostEnd; + } + + public String getDisplayResult() { + StringBuffer result = new StringBuffer(30); + maybeAppend(title, result); + maybeAppend(uri, result); + return result.toString(); + } + + /** + * Transforms a string that represents a URI into something more proper, by adding or canonicalizing + * the protocol. + */ + private static String massageURI(String uri) { + int protocolEnd = uri.indexOf(':'); + if (protocolEnd < 0) { + // No protocol, assume http + uri = "http://" + uri; + } else if (isColonFollowedByPortNumber(uri, protocolEnd)) { + // Found a colon, but it looks like it is after the host, so the protocol is still missing + uri = "http://" + uri; + } else { + // Lowercase protocol to avoid problems + uri = uri.substring(0, protocolEnd).toLowerCase() + uri.substring(protocolEnd); + } + return uri; + } + + private static boolean isColonFollowedByPortNumber(String uri, int protocolEnd) { + int nextSlash = uri.indexOf('/', protocolEnd + 1); + if (nextSlash < 0) { + nextSlash = uri.length(); + } + if (nextSlash <= protocolEnd + 1) { + return false; + } + for (int x = protocolEnd + 1; x < nextSlash; x++) { + if (uri.charAt(x) < '0' || uri.charAt(x) > '9') { + return false; + } + } + return true; + } + + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/URIResultParser.java b/src/com/google/zxing/client/result/URIResultParser.java new file mode 100644 index 0000000..5b2c24b --- /dev/null +++ b/src/com/google/zxing/client/result/URIResultParser.java @@ -0,0 +1,87 @@ +/* + * Copyright 2007 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.client.result; + +import com.google.zxing.Result; + +/** + * Tries to parse results that are a URI of some kind. + * + * @author Sean Owen + */ +final class URIResultParser extends ResultParser { + + private URIResultParser() { + } + + public static URIParsedResult parse(Result result) { + String rawText = result.getText(); + // We specifically handle the odd "URL" scheme here for simplicity + if (rawText != null && rawText.startsWith("URL:")) { + rawText = rawText.substring(4); + } + if (!isBasicallyValidURI(rawText)) { + return null; + } + return new URIParsedResult(rawText, null); + } + + /** + * Determines whether a string is not obviously not a URI. This implements crude checks; this class does not + * intend to strictly check URIs as its only function is to represent what is in a barcode, but, it does + * need to know when a string is obviously not a URI. + */ + static boolean isBasicallyValidURI(String uri) { + if (uri == null || uri.indexOf(' ') >= 0 || uri.indexOf('\n') >= 0) { + return false; + } + // Look for period in a domain but followed by at least a two-char TLD + // Forget strings that don't have a valid-looking protocol + int period = uri.indexOf('.'); + if (period >= uri.length() - 2) { + return false; + } + int colon = uri.indexOf(':'); + if (period < 0 && colon < 0) { + return false; + } + if (colon >= 0) { + if (period < 0 || period > colon) { + // colon ends the protocol + for (int i = 0; i < colon; i++) { + char c = uri.charAt(i); + if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z')) { + return false; + } + } + } else { + // colon starts the port; crudely look for at least two numbers + if (colon >= uri.length() - 2) { + return false; + } + for (int i = colon + 1; i < colon + 3; i++) { + char c = uri.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + } + } + return true; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/URLTOResultParser.java b/src/com/google/zxing/client/result/URLTOResultParser.java new file mode 100644 index 0000000..0ccd638 --- /dev/null +++ b/src/com/google/zxing/client/result/URLTOResultParser.java @@ -0,0 +1,47 @@ +/* + * Copyright 2007 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.client.result; + +import com.google.zxing.Result; + +/** + * Parses the "URLTO" result format, which is of the form "URLTO:[title]:[url]". + * This seems to be used sometimes, but I am not able to find documentation + * on its origin or official format? + * + * @author Sean Owen + */ +final class URLTOResultParser { + + private URLTOResultParser() { + } + + public static URIParsedResult parse(Result result) { + String rawText = result.getText(); + if (rawText == null || (!rawText.startsWith("urlto:") && !rawText.startsWith("URLTO:"))) { + return null; + } + int titleEnd = rawText.indexOf(':', 6); + if (titleEnd < 0) { + return null; + } + String title = titleEnd <= 6 ? null : rawText.substring(6, titleEnd); + String uri = rawText.substring(titleEnd + 1); + return new URIParsedResult(uri, title); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/VCardResultParser.java b/src/com/google/zxing/client/result/VCardResultParser.java new file mode 100644 index 0000000..4d53971 --- /dev/null +++ b/src/com/google/zxing/client/result/VCardResultParser.java @@ -0,0 +1,346 @@ +/* + * 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.client.result; + +import com.google.zxing.Result; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.util.Vector; + +/** + * Parses contact information formatted according to the VCard (2.1) format. This is not a complete + * implementation but should parse information as commonly encoded in 2D barcodes. + * + * @author Sean Owen + */ +final class VCardResultParser extends ResultParser { + + private VCardResultParser() { + } + + public static AddressBookParsedResult parse(Result result) { + // Although we should insist on the raw text ending with "END:VCARD", there's no reason + // to throw out everything else we parsed just because this was omitted. In fact, Eclair + // is doing just that, and we can't parse its contacts without this leniency. + String rawText = result.getText(); + if (rawText == null || !rawText.startsWith("BEGIN:VCARD")) { + return null; + } + String[] names = matchVCardPrefixedField("FN", rawText, true); + if (names == null) { + // If no display names found, look for regular name fields and format them + names = matchVCardPrefixedField("N", rawText, true); + formatNames(names); + } + String[] phoneNumbers = matchVCardPrefixedField("TEL", rawText, true); + String[] emails = matchVCardPrefixedField("EMAIL", rawText, true); + String note = matchSingleVCardPrefixedField("NOTE", rawText, false); + String[] addresses = matchVCardPrefixedField("ADR", rawText, true); + if (addresses != null) { + for (int i = 0; i < addresses.length; i++) { + addresses[i] = formatAddress(addresses[i]); + } + } + String org = matchSingleVCardPrefixedField("ORG", rawText, true); + String birthday = matchSingleVCardPrefixedField("BDAY", rawText, true); + if (!isLikeVCardDate(birthday)) { + birthday = null; + } + String title = matchSingleVCardPrefixedField("TITLE", rawText, true); + String url = matchSingleVCardPrefixedField("URL", rawText, true); + return new AddressBookParsedResult(names, null, phoneNumbers, emails, note, addresses, org, + birthday, title, url); + } + + private static String[] matchVCardPrefixedField(String prefix, String rawText, boolean trim) { + Vector matches = null; + int i = 0; + int max = rawText.length(); + + while (i < max) { + + i = rawText.indexOf(prefix, i); + if (i < 0) { + break; + } + + if (i > 0 && rawText.charAt(i - 1) != '\n') { + // then this didn't start a new token, we matched in the middle of something + i++; + continue; + } + i += prefix.length(); // Skip past this prefix we found to start + if (rawText.charAt(i) != ':' && rawText.charAt(i) != ';') { + continue; + } + + int metadataStart = i; + while (rawText.charAt(i) != ':') { // Skip until a colon + i++; + } + + boolean quotedPrintable = false; + String quotedPrintableCharset = null; + if (i > metadataStart) { + // There was something after the tag, before colon + int j = metadataStart+1; + while (j <= i) { + if (rawText.charAt(j) == ';' || rawText.charAt(j) == ':') { + String metadata = rawText.substring(metadataStart+1, j); + int equals = metadata.indexOf('='); + if (equals >= 0) { + String key = metadata.substring(0, equals); + String value = metadata.substring(equals+1); + if (key.equalsIgnoreCase("ENCODING")) { + if (value.equalsIgnoreCase("QUOTED-PRINTABLE")) { + quotedPrintable = true; + } + } else if (key.equalsIgnoreCase("CHARSET")) { + quotedPrintableCharset = value; + } + } + metadataStart = j; + } + j++; + } + } + + i++; // skip colon + + int matchStart = i; // Found the start of a match here + + while ((i = rawText.indexOf((int) '\n', i)) >= 0) { // Really, end in \r\n + if (i < rawText.length() - 1 && // But if followed by tab or space, + (rawText.charAt(i+1) == ' ' || // this is only a continuation + rawText.charAt(i+1) == '\t')) { + i += 2; // Skip \n and continutation whitespace + } else if (quotedPrintable && // If preceded by = in quoted printable + (rawText.charAt(i-1) == '=' || // this is a continuation + rawText.charAt(i-2) == '=')) { + i++; // Skip \n + } else { + break; + } + } + + if (i < 0) { + // No terminating end character? uh, done. Set i such that loop terminates and break + i = max; + } else if (i > matchStart) { + // found a match + if (matches == null) { + matches = new Vector(1); // lazy init + } + if (rawText.charAt(i-1) == '\r') { + i--; // Back up over \r, which really should be there + } + String element = rawText.substring(matchStart, i); + if (trim) { + element = element.trim(); + } + if (quotedPrintable) { + element = decodeQuotedPrintable(element, quotedPrintableCharset); + } else { + element = stripContinuationCRLF(element); + } + matches.addElement(element); + i++; + } else { + i++; + } + + } + + if (matches == null || matches.isEmpty()) { + return null; + } + return toStringArray(matches); + } + + private static String stripContinuationCRLF(String value) { + int length = value.length(); + StringBuffer result = new StringBuffer(length); + boolean lastWasLF = false; + for (int i = 0; i < length; i++) { + if (lastWasLF) { + lastWasLF = false; + continue; + } + char c = value.charAt(i); + lastWasLF = false; + switch (c) { + case '\n': + lastWasLF = true; + break; + case '\r': + break; + default: + result.append(c); + } + } + return result.toString(); + } + + private static String decodeQuotedPrintable(String value, String charset) { + int length = value.length(); + StringBuffer result = new StringBuffer(length); + ByteArrayOutputStream fragmentBuffer = new ByteArrayOutputStream(); + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + switch (c) { + case '\r': + case '\n': + break; + case '=': + if (i < length - 2) { + char nextChar = value.charAt(i+1); + if (nextChar == '\r' || nextChar == '\n') { + // Ignore, it's just a continuation symbol + } else { + char nextNextChar = value.charAt(i+2); + try { + int encodedByte = 16 * toHexValue(nextChar) + toHexValue(nextNextChar); + fragmentBuffer.write(encodedByte); + } catch (IllegalArgumentException iae) { + // continue, assume it was incorrectly encoded + } + i += 2; + } + } + break; + default: + maybeAppendFragment(fragmentBuffer, charset, result); + result.append(c); + } + } + maybeAppendFragment(fragmentBuffer, charset, result); + return result.toString(); + } + + private static int toHexValue(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } else if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } else if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + throw new IllegalArgumentException(); + } + + private static void maybeAppendFragment(ByteArrayOutputStream fragmentBuffer, + String charset, + StringBuffer result) { + if (fragmentBuffer.size() > 0) { + byte[] fragmentBytes = fragmentBuffer.toByteArray(); + String fragment; + if (charset == null) { + fragment = new String(fragmentBytes); + } else { + try { + fragment = new String(fragmentBytes, charset); + } catch (UnsupportedEncodingException e) { + // Yikes, well try anyway: + fragment = new String(fragmentBytes); + } + } + fragmentBuffer.reset(); + result.append(fragment); + } + } + + static String matchSingleVCardPrefixedField(String prefix, String rawText, boolean trim) { + String[] values = matchVCardPrefixedField(prefix, rawText, trim); + return values == null ? null : values[0]; + } + + private static boolean isLikeVCardDate(String value) { + if (value == null) { + return true; + } + // Not really sure this is true but matches practice + // Mach YYYYMMDD + if (isStringOfDigits(value, 8)) { + return true; + } + // or YYYY-MM-DD + return + value.length() == 10 && + value.charAt(4) == '-' && + value.charAt(7) == '-' && + isSubstringOfDigits(value, 0, 4) && + isSubstringOfDigits(value, 5, 2) && + isSubstringOfDigits(value, 8, 2); + } + + private static String formatAddress(String address) { + if (address == null) { + return null; + } + int length = address.length(); + StringBuffer newAddress = new StringBuffer(length); + for (int j = 0; j < length; j++) { + char c = address.charAt(j); + if (c == ';') { + newAddress.append(' '); + } else { + newAddress.append(c); + } + } + return newAddress.toString().trim(); + } + + /** + * Formats name fields of the form "Public;John;Q.;Reverend;III" into a form like + * "Reverend John Q. Public III". + * + * @param names name values to format, in place + */ + private static void formatNames(String[] names) { + if (names != null) { + for (int i = 0; i < names.length; i++) { + String name = names[i]; + String[] components = new String[5]; + int start = 0; + int end; + int componentIndex = 0; + while ((end = name.indexOf(';', start)) > 0) { + components[componentIndex] = name.substring(start, end); + componentIndex++; + start = end + 1; + } + components[componentIndex] = name.substring(start); + StringBuffer newName = new StringBuffer(100); + maybeAppendComponent(components, 3, newName); + maybeAppendComponent(components, 1, newName); + maybeAppendComponent(components, 2, newName); + maybeAppendComponent(components, 0, newName); + maybeAppendComponent(components, 4, newName); + names[i] = newName.toString().trim(); + } + } + } + + private static void maybeAppendComponent(String[] components, int i, StringBuffer newName) { + if (components[i] != null) { + newName.append(' '); + newName.append(components[i]); + } + } + +} diff --git a/src/com/google/zxing/client/result/VEventResultParser.java b/src/com/google/zxing/client/result/VEventResultParser.java new file mode 100644 index 0000000..2dd3ac6 --- /dev/null +++ b/src/com/google/zxing/client/result/VEventResultParser.java @@ -0,0 +1,54 @@ +/* + * 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.client.result; + +import com.google.zxing.Result; + +/** + * Partially implements the iCalendar format's "VEVENT" format for specifying a + * calendar event. See RFC 2445. This supports SUMMARY, LOCATION, DTSTART and DTEND fields. + * + * @author Sean Owen + */ +final class VEventResultParser extends ResultParser { + + private VEventResultParser() { + } + + public static CalendarParsedResult parse(Result result) { + String rawText = result.getText(); + if (rawText == null) { + return null; + } + int vEventStart = rawText.indexOf("BEGIN:VEVENT"); + if (vEventStart < 0) { + return null; + } + + String summary = VCardResultParser.matchSingleVCardPrefixedField("SUMMARY", rawText, true); + String start = VCardResultParser.matchSingleVCardPrefixedField("DTSTART", rawText, true); + String end = VCardResultParser.matchSingleVCardPrefixedField("DTEND", rawText, true); + String location = VCardResultParser.matchSingleVCardPrefixedField("LOCATION", rawText, true); + String description = VCardResultParser.matchSingleVCardPrefixedField("DESCRIPTION", rawText, true); + try { + return new CalendarParsedResult(summary, start, end, location, null, description); + } catch (IllegalArgumentException iae) { + return null; + } + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/WifiParsedResult.java b/src/com/google/zxing/client/result/WifiParsedResult.java new file mode 100644 index 0000000..21fa03e --- /dev/null +++ b/src/com/google/zxing/client/result/WifiParsedResult.java @@ -0,0 +1,53 @@ +/* + * Copyright 2010 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.client.result; + +/** + * @author Vikram Aggarwal + */ +public final class WifiParsedResult extends ParsedResult { + private final String ssid; + private final String networkEncryption; + private final String password; + + public WifiParsedResult(String networkEncryption, String ssid, String password) { + super(ParsedResultType.WIFI); + this.ssid = ssid; + this.networkEncryption = networkEncryption; + this.password = password; + } + + public String getSsid() { + return ssid; + } + + public String getNetworkEncryption() { + return networkEncryption; + } + + public String getPassword() { + return password; + } + + public String getDisplayResult() { + StringBuffer result = new StringBuffer(80); + maybeAppend(ssid, result); + maybeAppend(networkEncryption, result); + maybeAppend(password, result); + return result.toString(); + } +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/WifiResultParser.java b/src/com/google/zxing/client/result/WifiResultParser.java new file mode 100644 index 0000000..32bf856 --- /dev/null +++ b/src/com/google/zxing/client/result/WifiResultParser.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010 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.client.result; + +import com.google.zxing.Result; + +/** + * Parses a WIFI configuration string. Strings will be of the form: + * WIFI:T:WPA;S:mynetwork;P:mypass;; + * + * The fields can come in any order, and there should be tests to see + * if we can parse them all correctly. + * + * @author Vikram Aggarwal + */ +final class WifiResultParser extends ResultParser { + + private WifiResultParser() { + } + + public static WifiParsedResult parse(Result result) { + String rawText = result.getText(); + + if (rawText == null || !rawText.startsWith("WIFI:")) { + return null; + } + + // Don't remove leading or trailing whitespace + boolean trim = false; + String ssid = matchSinglePrefixedField("S:", rawText, ';', trim); + String pass = matchSinglePrefixedField("P:", rawText, ';', trim); + String type = matchSinglePrefixedField("T:", rawText, ';', trim); + + return new WifiParsedResult(type, ssid, pass); + } +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/optional/AbstractNDEFResultParser.java b/src/com/google/zxing/client/result/optional/AbstractNDEFResultParser.java new file mode 100644 index 0000000..59cb0b2 --- /dev/null +++ b/src/com/google/zxing/client/result/optional/AbstractNDEFResultParser.java @@ -0,0 +1,45 @@ +/* + * 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.client.result.optional; + +import com.google.zxing.client.result.ResultParser; + +import java.io.UnsupportedEncodingException; + +/** + *Superclass for classes encapsulating results in the NDEF format. + * See http://www.nfc-forum.org/specs/.
+ * + *This code supports a limited subset of NDEF messages, ones that are plausibly + * useful in 2D barcode formats. This generally includes 1-record messages, no chunking, + * "short record" syntax, no ID field.
+ * + * @author Sean Owen + */ +abstract class AbstractNDEFResultParser extends ResultParser { + + static String bytesToString(byte[] bytes, int offset, int length, String encoding) { + try { + return new String(bytes, offset, length, encoding); + } catch (UnsupportedEncodingException uee) { + // This should only be used when 'encoding' is an encoding that must necessarily + // be supported by the JVM, like UTF-8 + throw new RuntimeException("Platform does not support required encoding: " + uee); + } + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/optional/NDEFRecord.java b/src/com/google/zxing/client/result/optional/NDEFRecord.java new file mode 100644 index 0000000..5a51407 --- /dev/null +++ b/src/com/google/zxing/client/result/optional/NDEFRecord.java @@ -0,0 +1,87 @@ +/* + * 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.client.result.optional; + +/** + *Represents a record in an NDEF message. This class only supports certain types + * of records -- namely, non-chunked records, where ID length is omitted, and only + * "short records".
+ * + * @author Sean Owen + */ +final class NDEFRecord { + + private static final int SUPPORTED_HEADER_MASK = 0x3F; // 0 0 1 1 1 111 (the bottom 6 bits matter) + private static final int SUPPORTED_HEADER = 0x11; // 0 0 0 1 0 001 + + public static final String TEXT_WELL_KNOWN_TYPE = "T"; + public static final String URI_WELL_KNOWN_TYPE = "U"; + public static final String SMART_POSTER_WELL_KNOWN_TYPE = "Sp"; + public static final String ACTION_WELL_KNOWN_TYPE = "act"; + + private final int header; + private final String type; + private final byte[] payload; + private final int totalRecordLength; + + private NDEFRecord(int header, String type, byte[] payload, int totalRecordLength) { + this.header = header; + this.type = type; + this.payload = payload; + this.totalRecordLength = totalRecordLength; + } + + static NDEFRecord readRecord(byte[] bytes, int offset) { + int header = bytes[offset] & 0xFF; + // Does header match what we support in the bits we care about? + // XOR figures out where we differ, and if any of those are in the mask, fail + if (((header ^ SUPPORTED_HEADER) & SUPPORTED_HEADER_MASK) != 0) { + return null; + } + int typeLength = bytes[offset + 1] & 0xFF; + + int payloadLength = bytes[offset + 2] & 0xFF; + + String type = AbstractNDEFResultParser.bytesToString(bytes, offset + 3, typeLength, "US-ASCII"); + + byte[] payload = new byte[payloadLength]; + System.arraycopy(bytes, offset + 3 + typeLength, payload, 0, payloadLength); + + return new NDEFRecord(header, type, payload, 3 + typeLength + payloadLength); + } + + boolean isMessageBegin() { + return (header & 0x80) != 0; + } + + boolean isMessageEnd() { + return (header & 0x40) != 0; + } + + String getType() { + return type; + } + + byte[] getPayload() { + return payload; + } + + int getTotalRecordLength() { + return totalRecordLength; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/optional/NDEFSmartPosterParsedResult.java b/src/com/google/zxing/client/result/optional/NDEFSmartPosterParsedResult.java new file mode 100644 index 0000000..2c0607c --- /dev/null +++ b/src/com/google/zxing/client/result/optional/NDEFSmartPosterParsedResult.java @@ -0,0 +1,63 @@ +/* + * 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.client.result.optional; + +import com.google.zxing.client.result.ParsedResult; +import com.google.zxing.client.result.ParsedResultType; + +/** + * @author Sean Owen + */ +public final class NDEFSmartPosterParsedResult extends ParsedResult { + + public static final int ACTION_UNSPECIFIED = -1; + public static final int ACTION_DO = 0; + public static final int ACTION_SAVE = 1; + public static final int ACTION_OPEN = 2; + + private final String title; + private final String uri; + private final int action; + + NDEFSmartPosterParsedResult(int action, String uri, String title) { + super(ParsedResultType.NDEF_SMART_POSTER); + this.action = action; + this.uri = uri; + this.title = title; + } + + public String getTitle() { + return title; + } + + public String getURI() { + return uri; + } + + public int getAction() { + return action; + } + + public String getDisplayResult() { + if (title == null) { + return uri; + } else { + return title + '\n' + uri; + } + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/optional/NDEFSmartPosterResultParser.java b/src/com/google/zxing/client/result/optional/NDEFSmartPosterResultParser.java new file mode 100644 index 0000000..ac3ea1c --- /dev/null +++ b/src/com/google/zxing/client/result/optional/NDEFSmartPosterResultParser.java @@ -0,0 +1,81 @@ +/* + * 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.client.result.optional; + +import com.google.zxing.Result; + +/** + *Recognizes an NDEF message that encodes information according to the + * "Smart Poster Record Type Definition" specification.
+ * + *This actually only supports some parts of the Smart Poster format: title, + * URI, and action records. Icon records are not supported because the size + * of these records are infeasibly large for barcodes. Size and type records + * are not supported. Multiple titles are not supported.
+ * + * @author Sean Owen + */ +final class NDEFSmartPosterResultParser extends AbstractNDEFResultParser { + + public static NDEFSmartPosterParsedResult parse(Result result) { + byte[] bytes = result.getRawBytes(); + if (bytes == null) { + return null; + } + NDEFRecord headerRecord = NDEFRecord.readRecord(bytes, 0); + // Yes, header record starts and ends a message + if (headerRecord == null || !headerRecord.isMessageBegin() || !headerRecord.isMessageEnd()) { + return null; + } + if (!headerRecord.getType().equals(NDEFRecord.SMART_POSTER_WELL_KNOWN_TYPE)) { + return null; + } + + int offset = 0; + int recordNumber = 0; + NDEFRecord ndefRecord = null; + byte[] payload = headerRecord.getPayload(); + int action = NDEFSmartPosterParsedResult.ACTION_UNSPECIFIED; + String title = null; + String uri = null; + + while (offset < payload.length && (ndefRecord = NDEFRecord.readRecord(payload, offset)) != null) { + if (recordNumber == 0 && !ndefRecord.isMessageBegin()) { + return null; + } + + String type = ndefRecord.getType(); + if (NDEFRecord.TEXT_WELL_KNOWN_TYPE.equals(type)) { + String[] languageText = NDEFTextResultParser.decodeTextPayload(ndefRecord.getPayload()); + title = languageText[1]; + } else if (NDEFRecord.URI_WELL_KNOWN_TYPE.equals(type)) { + uri = NDEFURIResultParser.decodeURIPayload(ndefRecord.getPayload()); + } else if (NDEFRecord.ACTION_WELL_KNOWN_TYPE.equals(type)) { + action = ndefRecord.getPayload()[0]; + } + recordNumber++; + offset += ndefRecord.getTotalRecordLength(); + } + + if (recordNumber == 0 || (ndefRecord != null && !ndefRecord.isMessageEnd())) { + return null; + } + + return new NDEFSmartPosterParsedResult(action, uri, title); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/optional/NDEFTextResultParser.java b/src/com/google/zxing/client/result/optional/NDEFTextResultParser.java new file mode 100644 index 0000000..40651c5 --- /dev/null +++ b/src/com/google/zxing/client/result/optional/NDEFTextResultParser.java @@ -0,0 +1,57 @@ +/* + * 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.client.result.optional; + +import com.google.zxing.Result; +import com.google.zxing.client.result.TextParsedResult; + +/** + * Recognizes an NDEF message that encodes text according to the + * "Text Record Type Definition" specification. + * + * @author Sean Owen + */ +final class NDEFTextResultParser extends AbstractNDEFResultParser { + + public static TextParsedResult parse(Result result) { + byte[] bytes = result.getRawBytes(); + if (bytes == null) { + return null; + } + NDEFRecord ndefRecord = NDEFRecord.readRecord(bytes, 0); + if (ndefRecord == null || !ndefRecord.isMessageBegin() || !ndefRecord.isMessageEnd()) { + return null; + } + if (!ndefRecord.getType().equals(NDEFRecord.TEXT_WELL_KNOWN_TYPE)) { + return null; + } + String[] languageText = decodeTextPayload(ndefRecord.getPayload()); + return new TextParsedResult(languageText[0], languageText[1]); + } + + static String[] decodeTextPayload(byte[] payload) { + byte statusByte = payload[0]; + boolean isUTF16 = (statusByte & 0x80) != 0; + int languageLength = statusByte & 0x1F; + // language is always ASCII-encoded: + String language = bytesToString(payload, 1, languageLength, "US-ASCII"); + String encoding = isUTF16 ? "UTF-16" : "UTF8"; + String text = bytesToString(payload, 1 + languageLength, payload.length - languageLength - 1, encoding); + return new String[] { language, text }; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/client/result/optional/NDEFURIResultParser.java b/src/com/google/zxing/client/result/optional/NDEFURIResultParser.java new file mode 100644 index 0000000..3532c5b --- /dev/null +++ b/src/com/google/zxing/client/result/optional/NDEFURIResultParser.java @@ -0,0 +1,95 @@ +/* + * 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.client.result.optional; + +import com.google.zxing.Result; +import com.google.zxing.client.result.URIParsedResult; + +/** + * Recognizes an NDEF message that encodes a URI according to the + * "URI Record Type Definition" specification. + * + * @author Sean Owen + */ +final class NDEFURIResultParser extends AbstractNDEFResultParser { + + private static final String[] URI_PREFIXES = { + null, + "http://www.", + "https://www.", + "http://", + "https://", + "tel:", + "mailto:", + "ftp://anonymous:anonymous@", + "ftp://ftp.", + "ftps://", + "sftp://", + "smb://", + "nfs://", + "ftp://", + "dav://", + "news:", + "telnet://", + "imap:", + "rtsp://", + "urn:", + "pop:", + "sip:", + "sips:", + "tftp:", + "btspp://", + "btl2cap://", + "btgoep://", + "tcpobex://", + "irdaobex://", + "file://", + "urn:epc:id:", + "urn:epc:tag:", + "urn:epc:pat:", + "urn:epc:raw:", + "urn:epc:", + "urn:nfc:", + }; + + public static URIParsedResult parse(Result result) { + byte[] bytes = result.getRawBytes(); + if (bytes == null) { + return null; + } + NDEFRecord ndefRecord = NDEFRecord.readRecord(bytes, 0); + if (ndefRecord == null || !ndefRecord.isMessageBegin() || !ndefRecord.isMessageEnd()) { + return null; + } + if (!ndefRecord.getType().equals(NDEFRecord.URI_WELL_KNOWN_TYPE)) { + return null; + } + String fullURI = decodeURIPayload(ndefRecord.getPayload()); + return new URIParsedResult(fullURI, null); + } + + static String decodeURIPayload(byte[] payload) { + int identifierCode = payload[0] & 0xFF; + String prefix = null; + if (identifierCode < URI_PREFIXES.length) { + prefix = URI_PREFIXES[identifierCode]; + } + String restOfURI = bytesToString(payload, 1, payload.length - 1, "UTF8"); + return prefix == null ? restOfURI : prefix + restOfURI; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/common/BitArray.java b/src/com/google/zxing/common/BitArray.java new file mode 100644 index 0000000..bce6cee --- /dev/null +++ b/src/com/google/zxing/common/BitArray.java @@ -0,0 +1,247 @@ +/* + * Copyright 2007 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.common; + +/** + *A simple, fast array of bits, represented compactly by an array of ints internally.
+ * + * @author Sean Owen + */ +public final class BitArray { + + // TODO: I have changed these members to be public so ProGuard can inline get() and set(). Ideally + // they'd be private and we'd use the -allowaccessmodification flag, but Dalvik rejects the + // resulting binary at runtime on Android. If we find a solution to this, these should be changed + // back to private. + public int[] bits; + public int size; + + public BitArray() { + this.size = 0; + this.bits = new int[1]; + } + + public BitArray(int size) { + this.size = size; + this.bits = makeArray(size); + } + + public int getSize() { + return size; + } + + public int getSizeInBytes() { + return (size + 7) >> 3; + } + + private void ensureCapacity(int size) { + if (size > bits.length << 5) { + int[] newBits = makeArray(size); + System.arraycopy(bits, 0, newBits, 0, bits.length); + this.bits = newBits; + } + } + + /** + * @param i bit to get + * @return true iff bit i is set + */ + public boolean get(int i) { + return (bits[i >> 5] & (1 << (i & 0x1F))) != 0; + } + + /** + * Sets bit i. + * + * @param i bit to set + */ + public void set(int i) { + bits[i >> 5] |= 1 << (i & 0x1F); + } + + /** + * Flips bit i. + * + * @param i bit to set + */ + public void flip(int i) { + bits[i >> 5] ^= 1 << (i & 0x1F); + } + + /** + * Sets a block of 32 bits, starting at bit i. + * + * @param i first bit to set + * @param newBits the new value of the next 32 bits. Note again that the least-significant bit + * corresponds to bit i, the next-least-significant to i+1, and so on. + */ + public void setBulk(int i, int newBits) { + bits[i >> 5] = newBits; + } + + /** + * Clears all bits (sets to false). + */ + public void clear() { + int max = bits.length; + for (int i = 0; i < max; i++) { + bits[i] = 0; + } + } + + /** + * Efficient method to check if a range of bits is set, or not set. + * + * @param start start of range, inclusive. + * @param end end of range, exclusive + * @param value if true, checks that bits in range are set, otherwise checks that they are not set + * @return true iff all bits are set or not set in range, according to value argument + * @throws IllegalArgumentException if end is less than or equal to start + */ + public boolean isRange(int start, int end, boolean value) { + if (end < start) { + throw new IllegalArgumentException(); + } + if (end == start) { + return true; // empty range matches + } + end--; // will be easier to treat this as the last actually set bit -- inclusive + int firstInt = start >> 5; + int lastInt = end >> 5; + for (int i = firstInt; i <= lastInt; i++) { + int firstBit = i > firstInt ? 0 : start & 0x1F; + int lastBit = i < lastInt ? 31 : end & 0x1F; + int mask; + if (firstBit == 0 && lastBit == 31) { + mask = -1; + } else { + mask = 0; + for (int j = firstBit; j <= lastBit; j++) { + mask |= 1 << j; + } + } + + // Return false if we're looking for 1s and the masked bits[i] isn't all 1s (that is, + // equals the mask, or we're looking for 0s and the masked portion is not all 0s + if ((bits[i] & mask) != (value ? mask : 0)) { + return false; + } + } + return true; + } + + public void appendBit(boolean bit) { + ensureCapacity(size + 1); + if (bit) { + bits[size >> 5] |= (1 << (size & 0x1F)); + } + size++; + } + + /** + * Appends the least-significant bits, from value, in order from most-significant to + * least-significant. For example, appending 6 bits from 0x000001E will append the bits + * 0, 1, 1, 1, 1, 0 in that order. + */ + public void appendBits(int value, int numBits) { + if (numBits < 0 || numBits > 32) { + throw new IllegalArgumentException("Num bits must be between 0 and 32"); + } + ensureCapacity(size + numBits); + for (int numBitsLeft = numBits; numBitsLeft > 0; numBitsLeft--) { + appendBit(((value >> (numBitsLeft - 1)) & 0x01) == 1); + } + } + + public void appendBitArray(BitArray other) { + int otherSize = other.getSize(); + ensureCapacity(size + otherSize); + for (int i = 0; i < otherSize; i++) { + appendBit(other.get(i)); + } + } + + public void xor(BitArray other) { + if (bits.length != other.bits.length) { + throw new IllegalArgumentException("Sizes don't match"); + } + for (int i = 0; i < bits.length; i++) { + // The last byte could be incomplete (i.e. not have 8 bits in + // it) but there is no problem since 0 XOR 0 == 0. + bits[i] ^= other.bits[i]; + } + } + + /** + * + * @param bitOffset first bit to start writing + * @param array array to write into. Bytes are written most-significant byte first. This is the opposite + * of the internal representation, which is exposed by {@link #getBitArray()} + * @param offset position in array to start writing + * @param numBytes how many bytes to write + */ + public void toBytes(int bitOffset, byte[] array, int offset, int numBytes) { + for (int i = 0; i < numBytes; i++) { + int theByte = 0; + for (int j = 0; j < 8; j++) { + if (get(bitOffset)) { + theByte |= 1 << (7 - j); + } + bitOffset++; + } + array[offset + i] = (byte) theByte; + } + } + + /** + * @return underlying array of ints. The first element holds the first 32 bits, and the least + * significant bit is bit 0. + */ + public int[] getBitArray() { + return bits; + } + + /** + * Reverses all bits in the array. + */ + public void reverse() { + int[] newBits = new int[bits.length]; + int size = this.size; + for (int i = 0; i < size; i++) { + if (get(size - i - 1)) { + newBits[i >> 5] |= 1 << (i & 0x1F); + } + } + bits = newBits; + } + + private static int[] makeArray(int size) { + return new int[(size + 31) >> 5]; + } + + public String toString() { + StringBuffer result = new StringBuffer(size); + for (int i = 0; i < size; i++) { + if ((i & 0x07) == 0) { + result.append(' '); + } + result.append(get(i) ? 'X' : '.'); + } + return result.toString(); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/common/BitMatrix.java b/src/com/google/zxing/common/BitMatrix.java new file mode 100644 index 0000000..26cd599 --- /dev/null +++ b/src/com/google/zxing/common/BitMatrix.java @@ -0,0 +1,226 @@ +/* + * Copyright 2007 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.common; + +/** + *Represents a 2D matrix of bits. In function arguments below, and throughout the common + * module, x is the column position, and y is the row position. The ordering is always x, y. + * The origin is at the top-left.
+ * + *Internally the bits are represented in a 1-D array of 32-bit ints. However, each row begins + * with a new int. This is done intentionally so that we can copy out a row into a BitArray very + * efficiently.
+ * + *The ordering of bits is row-major. Within each int, the least significant bits are used first, + * meaning they represent lower x values. This is compatible with BitArray's implementation.
+ * + * @author Sean Owen + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class BitMatrix { + + // TODO: Just like BitArray, these need to be public so ProGuard can inline them. + public final int width; + public final int height; + public final int rowSize; + public final int[] bits; + + // A helper to construct a square matrix. + public BitMatrix(int dimension) { + this(dimension, dimension); + } + + public BitMatrix(int width, int height) { + if (width < 1 || height < 1) { + throw new IllegalArgumentException("Both dimensions must be greater than 0"); + } + this.width = width; + this.height = height; + this.rowSize = (width + 31) >> 5; + bits = new int[rowSize * height]; + } + + /** + *Gets the requested bit, where true means black.
+ * + * @param x The horizontal component (i.e. which column) + * @param y The vertical component (i.e. which row) + * @return value of given bit in matrix + */ + public boolean get(int x, int y) { + int offset = y * rowSize + (x >> 5); + return ((bits[offset] >>> (x & 0x1f)) & 1) != 0; + } + + /** + *Sets the given bit to true.
+ * + * @param x The horizontal component (i.e. which column) + * @param y The vertical component (i.e. which row) + */ + public void set(int x, int y) { + int offset = y * rowSize + (x >> 5); + bits[offset] |= 1 << (x & 0x1f); + } + + /** + *Flips the given bit.
+ * + * @param x The horizontal component (i.e. which column) + * @param y The vertical component (i.e. which row) + */ + public void flip(int x, int y) { + int offset = y * rowSize + (x >> 5); + bits[offset] ^= 1 << (x & 0x1f); + } + + /** + * Clears all bits (sets to false). + */ + public void clear() { + int max = bits.length; + for (int i = 0; i < max; i++) { + bits[i] = 0; + } + } + + /** + *Sets a square region of the bit matrix to true.
+ * + * @param left The horizontal position to begin at (inclusive) + * @param top The vertical position to begin at (inclusive) + * @param width The width of the region + * @param height The height of the region + */ + public void setRegion(int left, int top, int width, int height) { + if (top < 0 || left < 0) { + throw new IllegalArgumentException("Left and top must be nonnegative"); + } + if (height < 1 || width < 1) { + throw new IllegalArgumentException("Height and width must be at least 1"); + } + int right = left + width; + int bottom = top + height; + if (bottom > this.height || right > this.width) { + throw new IllegalArgumentException("The region must fit inside the matrix"); + } + for (int y = top; y < bottom; y++) { + int offset = y * rowSize; + for (int x = left; x < right; x++) { + bits[offset + (x >> 5)] |= 1 << (x & 0x1f); + } + } + } + + /** + * A fast method to retrieve one row of data from the matrix as a BitArray. + * + * @param y The row to retrieve + * @param row An optional caller-allocated BitArray, will be allocated if null or too small + * @return The resulting BitArray - this reference should always be used even when passing + * your own row + */ + public BitArray getRow(int y, BitArray row) { + if (row == null || row.getSize() < width) { + row = new BitArray(width); + } + int offset = y * rowSize; + for (int x = 0; x < rowSize; x++) { + row.setBulk(x << 5, bits[offset + x]); + } + return row; + } + + /** + * This is useful in detecting a corner of a 'pure' barcode. + * + * @return {x,y} coordinate of top-left-most 1 bit, or null if it is all white + */ + public int[] getTopLeftOnBit() { + int bitsOffset = 0; + while (bitsOffset < bits.length && bits[bitsOffset] == 0) { + bitsOffset++; + } + if (bitsOffset == bits.length) { + return null; + } + int y = bitsOffset / rowSize; + int x = (bitsOffset % rowSize) << 5; + + int theBits = bits[bitsOffset]; + int bit = 0; + while ((theBits << (31-bit)) == 0) { + bit++; + } + x += bit; + return new int[] {x, y}; + } + + /** + * @return The width of the matrix + */ + public int getWidth() { + return width; + } + + /** + * @return The height of the matrix + */ + public int getHeight() { + return height; + } + + public boolean equals(Object o) { + if (!(o instanceof BitMatrix)) { + return false; + } + BitMatrix other = (BitMatrix) o; + if (width != other.width || height != other.height || + rowSize != other.rowSize || bits.length != other.bits.length) { + return false; + } + for (int i = 0; i < bits.length; i++) { + if (bits[i] != other.bits[i]) { + return false; + } + } + return true; + } + + public int hashCode() { + int hash = width; + hash = 31 * hash + width; + hash = 31 * hash + height; + hash = 31 * hash + rowSize; + for (int i = 0; i < bits.length; i++) { + hash = 31 * hash + bits[i]; + } + return hash; + } + + public String toString() { + StringBuffer result = new StringBuffer(height * (width + 1)); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + result.append(get(x, y) ? "X " : " "); + } + result.append('\n'); + } + return result.toString(); + } + +} diff --git a/src/com/google/zxing/common/BitSource.java b/src/com/google/zxing/common/BitSource.java new file mode 100644 index 0000000..e5d7b1c --- /dev/null +++ b/src/com/google/zxing/common/BitSource.java @@ -0,0 +1,97 @@ +/* + * Copyright 2007 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.common; + +/** + *This provides an easy abstraction to read bits at a time from a sequence of bytes, where the + * number of bits read is not often a multiple of 8.
+ * + *This class is thread-safe but not reentrant. Unless the caller modifies the bytes array + * it passed in, in which case all bets are off.
+ * + * @author Sean Owen + */ +public final class BitSource { + + private final byte[] bytes; + private int byteOffset; + private int bitOffset; + + /** + * @param bytes bytes from which this will read bits. Bits will be read from the first byte first. + * Bits are read within a byte from most-significant to least-significant bit. + */ + public BitSource(byte[] bytes) { + this.bytes = bytes; + } + + /** + * @param numBits number of bits to read + * @return int representing the bits read. The bits will appear as the least-significant + * bits of the int + * @throws IllegalArgumentException if numBits isn't in [1,32] + */ + public int readBits(int numBits) { + if (numBits < 1 || numBits > 32) { + throw new IllegalArgumentException(); + } + + int result = 0; + + // First, read remainder from current byte + if (bitOffset > 0) { + int bitsLeft = 8 - bitOffset; + int toRead = numBits < bitsLeft ? numBits : bitsLeft; + int bitsToNotRead = bitsLeft - toRead; + int mask = (0xFF >> (8 - toRead)) << bitsToNotRead; + result = (bytes[byteOffset] & mask) >> bitsToNotRead; + numBits -= toRead; + bitOffset += toRead; + if (bitOffset == 8) { + bitOffset = 0; + byteOffset++; + } + } + + // Next read whole bytes + if (numBits > 0) { + while (numBits >= 8) { + result = (result << 8) | (bytes[byteOffset] & 0xFF); + byteOffset++; + numBits -= 8; + } + + // Finally read a partial byte + if (numBits > 0) { + int bitsToNotRead = 8 - numBits; + int mask = (0xFF >> bitsToNotRead) << bitsToNotRead; + result = (result << numBits) | ((bytes[byteOffset] & mask) >> bitsToNotRead); + bitOffset += numBits; + } + } + + return result; + } + + /** + * @return number of bits that can be read successfully + */ + public int available() { + return 8 * (bytes.length - byteOffset) - bitOffset; + } + +} diff --git a/src/com/google/zxing/common/CharacterSetECI.java b/src/com/google/zxing/common/CharacterSetECI.java new file mode 100644 index 0000000..26a1518 --- /dev/null +++ b/src/com/google/zxing/common/CharacterSetECI.java @@ -0,0 +1,110 @@ +/* + * 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.common; + +import java.util.Hashtable; + +/** + * Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 + * of ISO 18004. + * + * @author Sean Owen + */ +public final class CharacterSetECI extends ECI { + + private static Hashtable VALUE_TO_ECI; + private static Hashtable NAME_TO_ECI; + + private static void initialize() { + VALUE_TO_ECI = new Hashtable(29); + NAME_TO_ECI = new Hashtable(29); + // TODO figure out if these values are even right! + addCharacterSet(0, "Cp437"); + addCharacterSet(1, new String[] {"ISO8859_1", "ISO-8859-1"}); + addCharacterSet(2, "Cp437"); + addCharacterSet(3, new String[] {"ISO8859_1", "ISO-8859-1"}); + addCharacterSet(4, "ISO8859_2"); + addCharacterSet(5, "ISO8859_3"); + addCharacterSet(6, "ISO8859_4"); + addCharacterSet(7, "ISO8859_5"); + addCharacterSet(8, "ISO8859_6"); + addCharacterSet(9, "ISO8859_7"); + addCharacterSet(10, "ISO8859_8"); + addCharacterSet(11, "ISO8859_9"); + addCharacterSet(12, "ISO8859_10"); + addCharacterSet(13, "ISO8859_11"); + addCharacterSet(15, "ISO8859_13"); + addCharacterSet(16, "ISO8859_14"); + addCharacterSet(17, "ISO8859_15"); + addCharacterSet(18, "ISO8859_16"); + addCharacterSet(20, new String[] {"SJIS", "Shift_JIS"}); + } + + private final String encodingName; + + private CharacterSetECI(int value, String encodingName) { + super(value); + this.encodingName = encodingName; + } + + public String getEncodingName() { + return encodingName; + } + + private static void addCharacterSet(int value, String encodingName) { + CharacterSetECI eci = new CharacterSetECI(value, encodingName); + VALUE_TO_ECI.put(new Integer(value), eci); // can't use valueOf + NAME_TO_ECI.put(encodingName, eci); + } + + private static void addCharacterSet(int value, String[] encodingNames) { + CharacterSetECI eci = new CharacterSetECI(value, encodingNames[0]); + VALUE_TO_ECI.put(new Integer(value), eci); // can't use valueOf + for (int i = 0; i < encodingNames.length; i++) { + NAME_TO_ECI.put(encodingNames[i], eci); + } + } + + /** + * @param value character set ECI value + * @return {@link CharacterSetECI} representing ECI of given value, or null if it is legal but + * unsupported + * @throws IllegalArgumentException if ECI value is invalid + */ + public static CharacterSetECI getCharacterSetECIByValue(int value) { + if (VALUE_TO_ECI == null) { + initialize(); + } + if (value < 0 || value >= 900) { + throw new IllegalArgumentException("Bad ECI value: " + value); + } + return (CharacterSetECI) VALUE_TO_ECI.get(new Integer(value)); + } + + /** + * @param name character set ECI encoding name + * @return {@link CharacterSetECI} representing ECI for character encoding, or null if it is legal + * but unsupported + */ + public static CharacterSetECI getCharacterSetECIByName(String name) { + if (NAME_TO_ECI == null) { + initialize(); + } + return (CharacterSetECI) NAME_TO_ECI.get(name); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/common/Collections.java b/src/com/google/zxing/common/Collections.java new file mode 100644 index 0000000..319ebfe --- /dev/null +++ b/src/com/google/zxing/common/Collections.java @@ -0,0 +1,53 @@ +/* + * Copyright 2007 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.common; + +import java.util.Vector; + +/** + *This is basically a substitute for java.util.Collections
, which is not
+ * present in MIDP 2.0 / CLDC 1.1.
Comparator
since it is not available in
+ * CLDC 1.1 / MIDP 2.0.
+ */
+public interface Comparator {
+
+ int compare(Object o1, Object o2);
+
+}
\ No newline at end of file
diff --git a/src/com/google/zxing/common/DecoderResult.java b/src/com/google/zxing/common/DecoderResult.java
new file mode 100644
index 0000000..ce0e3c5
--- /dev/null
+++ b/src/com/google/zxing/common/DecoderResult.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2007 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.common;
+
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+
+import java.util.Vector;
+
+/**
+ * Encapsulates the result of decoding a matrix of bits. This typically + * applies to 2D barcode formats. For now it contains the raw bytes obtained, + * as well as a String interpretation of those bytes, if applicable.
+ * + * @author Sean Owen + */ +public final class DecoderResult { + + private final byte[] rawBytes; + private final String text; + private final Vector byteSegments; + private final ErrorCorrectionLevel ecLevel; + + public DecoderResult(byte[] rawBytes, String text, Vector byteSegments, ErrorCorrectionLevel ecLevel) { + if (rawBytes == null && text == null) { + throw new IllegalArgumentException(); + } + this.rawBytes = rawBytes; + this.text = text; + this.byteSegments = byteSegments; + this.ecLevel = ecLevel; + } + + public byte[] getRawBytes() { + return rawBytes; + } + + public String getText() { + return text; + } + + public Vector getByteSegments() { + return byteSegments; + } + + public ErrorCorrectionLevel getECLevel() { + return ecLevel; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/common/DefaultGridSampler.java b/src/com/google/zxing/common/DefaultGridSampler.java new file mode 100644 index 0000000..9a2a683 --- /dev/null +++ b/src/com/google/zxing/common/DefaultGridSampler.java @@ -0,0 +1,81 @@ +/* + * Copyright 2007 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.common; + +import com.google.zxing.NotFoundException; + +/** + * @author Sean Owen + */ +public final class DefaultGridSampler extends GridSampler { + + public BitMatrix sampleGrid(BitMatrix image, + int dimension, + float p1ToX, float p1ToY, + float p2ToX, float p2ToY, + float p3ToX, float p3ToY, + float p4ToX, float p4ToY, + float p1FromX, float p1FromY, + float p2FromX, float p2FromY, + float p3FromX, float p3FromY, + float p4FromX, float p4FromY) throws NotFoundException { + + PerspectiveTransform transform = PerspectiveTransform.quadrilateralToQuadrilateral( + p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY, + p1FromX, p1FromY, p2FromX, p2FromY, p3FromX, p3FromY, p4FromX, p4FromY); + + return sampleGrid(image, dimension, transform); + } + + public BitMatrix sampleGrid(BitMatrix image, + int dimension, + PerspectiveTransform transform) throws NotFoundException { + BitMatrix bits = new BitMatrix(dimension); + float[] points = new float[dimension << 1]; + for (int y = 0; y < dimension; y++) { + int max = points.length; + float iValue = (float) y + 0.5f; + for (int x = 0; x < max; x += 2) { + points[x] = (float) (x >> 1) + 0.5f; + points[x + 1] = iValue; + } + transform.transformPoints(points); + // Quick check to see if points transformed to something inside the image; + // sufficient to check the endpoints + checkAndNudgePoints(image, points); + try { + for (int x = 0; x < max; x += 2) { + if (image.get((int) points[x], (int) points[x + 1])) { + // Black(-ish) pixel + bits.set(x >> 1, y); + } + } + } catch (ArrayIndexOutOfBoundsException aioobe) { + // This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting + // transform gets "twisted" such that it maps a straight line of points to a set of points + // whose endpoints are in bounds, but others are not. There is probably some mathematical + // way to detect this about the transformation that I don't know yet. + // This results in an ugly runtime exception despite our clever checks above -- can't have + // that. We could check each point's coordinates but that feels duplicative. We settle for + // catching and wrapping ArrayIndexOutOfBoundsException. + throw NotFoundException.getNotFoundInstance(); + } + } + return bits; + } + +} diff --git a/src/com/google/zxing/common/DetectorResult.java b/src/com/google/zxing/common/DetectorResult.java new file mode 100644 index 0000000..5e3786a --- /dev/null +++ b/src/com/google/zxing/common/DetectorResult.java @@ -0,0 +1,46 @@ +/* + * Copyright 2007 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.common; + +import com.google.zxing.ResultPoint; + +/** + *Encapsulates the result of detecting a barcode in an image. This includes the raw + * matrix of black/white pixels corresponding to the barcode, and possibly points of interest + * in the image, like the location of finder patterns or corners of the barcode in the image.
+ * + * @author Sean Owen + */ +public final class DetectorResult { + + private final BitMatrix bits; + private final ResultPoint[] points; + + public DetectorResult(BitMatrix bits, ResultPoint[] points) { + this.bits = bits; + this.points = points; + } + + public BitMatrix getBits() { + return bits; + } + + public ResultPoint[] getPoints() { + return points; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/common/ECI.java b/src/com/google/zxing/common/ECI.java new file mode 100644 index 0000000..9d725db --- /dev/null +++ b/src/com/google/zxing/common/ECI.java @@ -0,0 +1,52 @@ +/* + * 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.common; + +/** + * Superclass of classes encapsulating types ECIs, according to "Extended Channel Interpretations" + * 5.3 of ISO 18004. + * + * @author Sean Owen + */ +public abstract class ECI { + + private final int value; + + ECI(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + /** + * @param value ECI value + * @return {@link ECI} representing ECI of given value, or null if it is legal but unsupported + * @throws IllegalArgumentException if ECI value is invalid + */ + public static ECI getECIByValue(int value) { + if (value < 0 || value > 999999) { + throw new IllegalArgumentException("Bad ECI value: " + value); + } + if (value < 900) { // Character set ECIs use 000000 - 000899 + return CharacterSetECI.getCharacterSetECIByValue(value); + } + return null; + } + +} diff --git a/src/com/google/zxing/common/GlobalHistogramBinarizer.java b/src/com/google/zxing/common/GlobalHistogramBinarizer.java new file mode 100644 index 0000000..144c099 --- /dev/null +++ b/src/com/google/zxing/common/GlobalHistogramBinarizer.java @@ -0,0 +1,196 @@ +/* + * Copyright 2009 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.common; + +import com.google.zxing.Binarizer; +import com.google.zxing.LuminanceSource; +import com.google.zxing.NotFoundException; + +/** + * This Binarizer implementation uses the old ZXing global histogram approach. It is suitable + * for low-end mobile devices which don't have enough CPU or memory to use a local thresholding + * algorithm. However, because it picks a global black point, it cannot handle difficult shadows + * and gradients. + * + * Faster mobile devices and all desktop applications should probably use HybridBinarizer instead. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public class GlobalHistogramBinarizer extends Binarizer { + + private static final int LUMINANCE_BITS = 5; + private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS; + private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS; + + private byte[] luminances = null; + private int[] buckets = null; + + public GlobalHistogramBinarizer(LuminanceSource source) { + super(source); + } + + // Applies simple sharpening to the row data to improve performance of the 1D Readers. + public BitArray getBlackRow(int y, BitArray row) throws NotFoundException { + LuminanceSource source = getLuminanceSource(); + int width = source.getWidth(); + if (row == null || row.getSize() < width) { + row = new BitArray(width); + } else { + row.clear(); + } + + initArrays(width); + byte[] localLuminances = source.getRow(y, luminances); + int[] localBuckets = buckets; + for (int x = 0; x < width; x++) { + int pixel = localLuminances[x] & 0xff; + localBuckets[pixel >> LUMINANCE_SHIFT]++; + } + int blackPoint = estimateBlackPoint(localBuckets); + + int left = localLuminances[0] & 0xff; + int center = localLuminances[1] & 0xff; + for (int x = 1; x < width - 1; x++) { + int right = localLuminances[x + 1] & 0xff; + // A simple -1 4 -1 box filter with a weight of 2. + int luminance = ((center << 2) - left - right) >> 1; + if (luminance < blackPoint) { + row.set(x); + } + left = center; + center = right; + } + return row; + } + + // Does not sharpen the data, as this call is intended to only be used by 2D Readers. + public BitMatrix getBlackMatrix() throws NotFoundException { + LuminanceSource source = getLuminanceSource(); + int width = source.getWidth(); + int height = source.getHeight(); + BitMatrix matrix = new BitMatrix(width, height); + + // Quickly calculates the histogram by sampling four rows from the image. This proved to be + // more robust on the blackbox tests than sampling a diagonal as we used to do. + initArrays(width); + int[] localBuckets = buckets; + for (int y = 1; y < 5; y++) { + int row = height * y / 5; + byte[] localLuminances = source.getRow(row, luminances); + int right = (width << 2) / 5; + for (int x = width / 5; x < right; x++) { + int pixel = localLuminances[x] & 0xff; + localBuckets[pixel >> LUMINANCE_SHIFT]++; + } + } + int blackPoint = estimateBlackPoint(localBuckets); + + // We delay reading the entire image luminance until the black point estimation succeeds. + // Although we end up reading four rows twice, it is consistent with our motto of + // "fail quickly" which is necessary for continuous scanning. + byte[] localLuminances = source.getMatrix(); + for (int y = 0; y < height; y++) { + int offset = y * width; + for (int x = 0; x< width; x++) { + int pixel = localLuminances[offset + x] & 0xff; + if (pixel < blackPoint) { + matrix.set(x, y); + } + } + } + + return matrix; + } + + public Binarizer createBinarizer(LuminanceSource source) { + return new GlobalHistogramBinarizer(source); + } + + private void initArrays(int luminanceSize) { + if (luminances == null || luminances.length < luminanceSize) { + luminances = new byte[luminanceSize]; + } + if (buckets == null) { + buckets = new int[LUMINANCE_BUCKETS]; + } else { + for (int x = 0; x < LUMINANCE_BUCKETS; x++) { + buckets[x] = 0; + } + } + } + + private static int estimateBlackPoint(int[] buckets) throws NotFoundException { + // Find the tallest peak in the histogram. + int numBuckets = buckets.length; + int maxBucketCount = 0; + int firstPeak = 0; + int firstPeakSize = 0; + for (int x = 0; x < numBuckets; x++) { + if (buckets[x] > firstPeakSize) { + firstPeak = x; + firstPeakSize = buckets[x]; + } + if (buckets[x] > maxBucketCount) { + maxBucketCount = buckets[x]; + } + } + + // Find the second-tallest peak which is somewhat far from the tallest peak. + int secondPeak = 0; + int secondPeakScore = 0; + for (int x = 0; x < numBuckets; x++) { + int distanceToBiggest = x - firstPeak; + // Encourage more distant second peaks by multiplying by square of distance. + int score = buckets[x] * distanceToBiggest * distanceToBiggest; + if (score > secondPeakScore) { + secondPeak = x; + secondPeakScore = score; + } + } + + // Make sure firstPeak corresponds to the black peak. + if (firstPeak > secondPeak) { + int temp = firstPeak; + firstPeak = secondPeak; + secondPeak = temp; + } + + // If there is too little contrast in the image to pick a meaningful black point, throw rather + // than waste time trying to decode the image, and risk false positives. + // TODO: It might be worth comparing the brightest and darkest pixels seen, rather than the + // two peaks, to determine the contrast. + if (secondPeak - firstPeak <= numBuckets >> 4) { + throw NotFoundException.getNotFoundInstance(); + } + + // Find a valley between them that is low and closer to the white peak. + int bestValley = secondPeak - 1; + int bestValleyScore = -1; + for (int x = secondPeak - 1; x > firstPeak; x--) { + int fromFirst = x - firstPeak; + int score = fromFirst * fromFirst * (secondPeak - x) * (maxBucketCount - buckets[x]); + if (score > bestValleyScore) { + bestValley = x; + bestValleyScore = score; + } + } + + return bestValley << LUMINANCE_SHIFT; + } + +} diff --git a/src/com/google/zxing/common/GridSampler.java b/src/com/google/zxing/common/GridSampler.java new file mode 100644 index 0000000..3b55eb6 --- /dev/null +++ b/src/com/google/zxing/common/GridSampler.java @@ -0,0 +1,171 @@ +/* + * Copyright 2007 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.common; + +import com.google.zxing.NotFoundException; + +/** + * Implementations of this class can, given locations of finder patterns for a QR code in an + * image, sample the right points in the image to reconstruct the QR code, accounting for + * perspective distortion. It is abstracted since it is relatively expensive and should be allowed + * to take advantage of platform-specific optimized implementations, like Sun's Java Advanced + * Imaging library, but which may not be available in other environments such as J2ME, and vice + * versa. + * + * The implementation used can be controlled by calling {@link #setGridSampler(GridSampler)} + * with an instance of a class which implements this interface. + * + * @author Sean Owen + */ +public abstract class GridSampler { + + private static GridSampler gridSampler = new DefaultGridSampler(); + + /** + * Sets the implementation of {@link GridSampler} used by the library. One global + * instance is stored, which may sound problematic. But, the implementation provided + * ought to be appropriate for the entire platform, and all uses of this library + * in the whole lifetime of the JVM. For instance, an Android activity can swap in + * an implementation that takes advantage of native platform libraries. + * + * @param newGridSampler The platform-specific object to install. + */ + public static void setGridSampler(GridSampler newGridSampler) { + if (newGridSampler == null) { + throw new IllegalArgumentException(); + } + gridSampler = newGridSampler; + } + + /** + * @return the current implementation of {@link GridSampler} + */ + public static GridSampler getInstance() { + return gridSampler; + } + + /** + *Samples an image for a square matrix of bits of the given dimension. This is used to extract + * the black/white modules of a 2D barcode like a QR Code found in an image. Because this barcode + * may be rotated or perspective-distorted, the caller supplies four points in the source image + * that define known points in the barcode, so that the image may be sampled appropriately.
+ * + *The last eight "from" parameters are four X/Y coordinate pairs of locations of points in + * the image that define some significant points in the image to be sample. For example, + * these may be the location of finder pattern in a QR Code.
+ * + *The first eight "to" parameters are four X/Y coordinate pairs measured in the destination + * {@link BitMatrix}, from the top left, where the known points in the image given by the "from" + * parameters map to.
+ * + *These 16 parameters define the transformation needed to sample the image.
+ * + * @param image image to sample + * @param dimension width/height of {@link BitMatrix} to sample from image + * @return {@link BitMatrix} representing a grid of points sampled from the image within a region + * defined by the "from" parameters + * @throws NotFoundException if image can't be sampled, for example, if the transformation defined + * by the given points is invalid or results in sampling outside the image boundaries + */ + public abstract BitMatrix sampleGrid(BitMatrix image, + int dimension, + float p1ToX, float p1ToY, + float p2ToX, float p2ToY, + float p3ToX, float p3ToY, + float p4ToX, float p4ToY, + float p1FromX, float p1FromY, + float p2FromX, float p2FromY, + float p3FromX, float p3FromY, + float p4FromX, float p4FromY) throws NotFoundException; + + public BitMatrix sampleGrid(BitMatrix image, + int dimension, + PerspectiveTransform transform) throws NotFoundException { + throw new IllegalStateException(); // Can't use UnsupportedOperationException + } + + + /** + *Checks a set of points that have been transformed to sample points on an image against + * the image's dimensions to see if the point are even within the image.
+ * + *This method will actually "nudge" the endpoints back onto the image if they are found to be + * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder + * patterns in an image where the QR Code runs all the way to the image border.
+ * + *For efficiency, the method will check points from either end of the line until one is found + * to be within the image. Because the set of points are assumed to be linear, this is valid.
+ * + * @param image image into which the points should map + * @param points actual points in x1,y1,...,xn,yn form + * @throws NotFoundException if an endpoint is lies outside the image boundaries + */ + protected static void checkAndNudgePoints(BitMatrix image, float[] points) + throws NotFoundException { + int width = image.getWidth(); + int height = image.getHeight(); + // Check and nudge points from start until we see some that are OK: + boolean nudged = true; + for (int offset = 0; offset < points.length && nudged; offset += 2) { + int x = (int) points[offset]; + int y = (int) points[offset + 1]; + if (x < -1 || x > width || y < -1 || y > height) { + throw NotFoundException.getNotFoundInstance(); + } + nudged = false; + if (x == -1) { + points[offset] = 0.0f; + nudged = true; + } else if (x == width) { + points[offset] = width - 1; + nudged = true; + } + if (y == -1) { + points[offset + 1] = 0.0f; + nudged = true; + } else if (y == height) { + points[offset + 1] = height - 1; + nudged = true; + } + } + // Check and nudge points from end: + nudged = true; + for (int offset = points.length - 2; offset >= 0 && nudged; offset -= 2) { + int x = (int) points[offset]; + int y = (int) points[offset + 1]; + if (x < -1 || x > width || y < -1 || y > height) { + throw NotFoundException.getNotFoundInstance(); + } + nudged = false; + if (x == -1) { + points[offset] = 0.0f; + nudged = true; + } else if (x == width) { + points[offset] = width - 1; + nudged = true; + } + if (y == -1) { + points[offset + 1] = 0.0f; + nudged = true; + } else if (y == height) { + points[offset + 1] = height - 1; + nudged = true; + } + } + } + +} diff --git a/src/com/google/zxing/common/HybridBinarizer.java b/src/com/google/zxing/common/HybridBinarizer.java new file mode 100644 index 0000000..e89b06f --- /dev/null +++ b/src/com/google/zxing/common/HybridBinarizer.java @@ -0,0 +1,185 @@ +/* + * Copyright 2009 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.common; + +import com.google.zxing.Binarizer; +import com.google.zxing.LuminanceSource; +import com.google.zxing.NotFoundException; + +/** + * This class implements a local thresholding algorithm, which while slower than the + * GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for + * high frequency images of barcodes with black data on white backgrounds. For this application, + * it does a much better job than a global blackpoint with severe shadows and gradients. + * However it tends to produce artifacts on lower frequency images and is therefore not + * a good general purpose binarizer for uses outside ZXing. + * + * This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers, + * and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already + * inherently local, and only fails for horizontal gradients. We can revisit that problem later, + * but for now it was not a win to use local blocks for 1D. + * + * This Binarizer is the default for the unit tests and the recommended class for library users. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class HybridBinarizer extends GlobalHistogramBinarizer { + + // This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels. + // So this is the smallest dimension in each axis we can accept. + private static final int MINIMUM_DIMENSION = 40; + + private BitMatrix matrix = null; + + public HybridBinarizer(LuminanceSource source) { + super(source); + } + + public BitMatrix getBlackMatrix() throws NotFoundException { + binarizeEntireImage(); + return matrix; + } + + public Binarizer createBinarizer(LuminanceSource source) { + return new HybridBinarizer(source); + } + + // Calculates the final BitMatrix once for all requests. This could be called once from the + // constructor instead, but there are some advantages to doing it lazily, such as making + // profiling easier, and not doing heavy lifting when callers don't expect it. + private void binarizeEntireImage() throws NotFoundException { + if (matrix == null) { + LuminanceSource source = getLuminanceSource(); + if (source.getWidth() >= MINIMUM_DIMENSION && source.getHeight() >= MINIMUM_DIMENSION) { + byte[] luminances = source.getMatrix(); + int width = source.getWidth(); + int height = source.getHeight(); + int subWidth = width >> 3; + if ((width & 0x07) != 0) { + subWidth++; + } + int subHeight = height >> 3; + if ((height & 0x07) != 0) { + subHeight++; + } + int[][] blackPoints = calculateBlackPoints(luminances, subWidth, subHeight, width, height); + + matrix = new BitMatrix(width, height); + calculateThresholdForBlock(luminances, subWidth, subHeight, width, height, blackPoints, matrix); + } else { + // If the image is too small, fall back to the global histogram approach. + matrix = super.getBlackMatrix(); + } + } + } + + // For each 8x8 block in the image, calculate the average black point using a 5x5 grid + // of the blocks around it. Also handles the corner cases (fractional blocks are computed based + // on the last 8 pixels in the row/column which are also used in the previous block). + private static void calculateThresholdForBlock(byte[] luminances, int subWidth, int subHeight, + int width, int height, int[][] blackPoints, BitMatrix matrix) { + for (int y = 0; y < subHeight; y++) { + int yoffset = y << 3; + if ((yoffset + 8) >= height) { + yoffset = height - 8; + } + for (int x = 0; x < subWidth; x++) { + int xoffset = x << 3; + if ((xoffset + 8) >= width) { + xoffset = width - 8; + } + int left = (x > 1) ? x : 2; + left = (left < subWidth - 2) ? left : subWidth - 3; + int top = (y > 1) ? y : 2; + top = (top < subHeight - 2) ? top : subHeight - 3; + int sum = 0; + for (int z = -2; z <= 2; z++) { + int[] blackRow = blackPoints[top + z]; + sum += blackRow[left - 2]; + sum += blackRow[left - 1]; + sum += blackRow[left]; + sum += blackRow[left + 1]; + sum += blackRow[left + 2]; + } + int average = sum / 25; + threshold8x8Block(luminances, xoffset, yoffset, average, width, matrix); + } + } + } + + // Applies a single threshold to an 8x8 block of pixels. + private static void threshold8x8Block(byte[] luminances, int xoffset, int yoffset, int threshold, + int stride, BitMatrix matrix) { + for (int y = 0; y < 8; y++) { + int offset = (yoffset + y) * stride + xoffset; + for (int x = 0; x < 8; x++) { + int pixel = luminances[offset + x] & 0xff; + if (pixel < threshold) { + matrix.set(xoffset + x, yoffset + y); + } + } + } + } + + // Calculates a single black point for each 8x8 block of pixels and saves it away. + private static int[][] calculateBlackPoints(byte[] luminances, int subWidth, int subHeight, + int width, int height) { + int[][] blackPoints = new int[subHeight][subWidth]; + for (int y = 0; y < subHeight; y++) { + int yoffset = y << 3; + if ((yoffset + 8) >= height) { + yoffset = height - 8; + } + for (int x = 0; x < subWidth; x++) { + int xoffset = x << 3; + if ((xoffset + 8) >= width) { + xoffset = width - 8; + } + int sum = 0; + int min = 255; + int max = 0; + for (int yy = 0; yy < 8; yy++) { + int offset = (yoffset + yy) * width + xoffset; + for (int xx = 0; xx < 8; xx++) { + int pixel = luminances[offset + xx] & 0xff; + sum += pixel; + if (pixel < min) { + min = pixel; + } + if (pixel > max) { + max = pixel; + } + } + } + + // If the contrast is inadequate, use half the minimum, so that this block will be + // treated as part of the white background, but won't drag down neighboring blocks + // too much. + int average; + if (max - min > 24) { + average = sum >> 6; + } else { + // When min == max == 0, let average be 1 so all is black + average = max == 0 ? 1 : min >> 1; + } + blackPoints[y][x] = average; + } + } + return blackPoints; + } + +} diff --git a/src/com/google/zxing/common/PerspectiveTransform.java b/src/com/google/zxing/common/PerspectiveTransform.java new file mode 100644 index 0000000..19743fc --- /dev/null +++ b/src/com/google/zxing/common/PerspectiveTransform.java @@ -0,0 +1,148 @@ +/* + * Copyright 2007 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.common; + +/** + *This class implements a perspective transform in two dimensions. Given four source and four + * destination points, it will compute the transformation implied between them. The code is based + * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.
+ * + * @author Sean Owen + */ +public final class PerspectiveTransform { + + private final float a11, a12, a13, a21, a22, a23, a31, a32, a33; + + private PerspectiveTransform(float a11, float a21, float a31, + float a12, float a22, float a32, + float a13, float a23, float a33) { + this.a11 = a11; + this.a12 = a12; + this.a13 = a13; + this.a21 = a21; + this.a22 = a22; + this.a23 = a23; + this.a31 = a31; + this.a32 = a32; + this.a33 = a33; + } + + public static PerspectiveTransform quadrilateralToQuadrilateral(float x0, float y0, + float x1, float y1, + float x2, float y2, + float x3, float y3, + float x0p, float y0p, + float x1p, float y1p, + float x2p, float y2p, + float x3p, float y3p) { + + PerspectiveTransform qToS = quadrilateralToSquare(x0, y0, x1, y1, x2, y2, x3, y3); + PerspectiveTransform sToQ = squareToQuadrilateral(x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p); + return sToQ.times(qToS); + } + + public void transformPoints(float[] points) { + int max = points.length; + float a11 = this.a11; + float a12 = this.a12; + float a13 = this.a13; + float a21 = this.a21; + float a22 = this.a22; + float a23 = this.a23; + float a31 = this.a31; + float a32 = this.a32; + float a33 = this.a33; + for (int i = 0; i < max; i += 2) { + float x = points[i]; + float y = points[i + 1]; + float denominator = a13 * x + a23 * y + a33; + points[i] = (a11 * x + a21 * y + a31) / denominator; + points[i + 1] = (a12 * x + a22 * y + a32) / denominator; + } + } + + /** Convenience method, not optimized for performance. */ + public void transformPoints(float[] xValues, float[] yValues) { + int n = xValues.length; + for (int i = 0; i < n; i ++) { + float x = xValues[i]; + float y = yValues[i]; + float denominator = a13 * x + a23 * y + a33; + xValues[i] = (a11 * x + a21 * y + a31) / denominator; + yValues[i] = (a12 * x + a22 * y + a32) / denominator; + } + } + + public static PerspectiveTransform squareToQuadrilateral(float x0, float y0, + float x1, float y1, + float x2, float y2, + float x3, float y3) { + float dy2 = y3 - y2; + float dy3 = y0 - y1 + y2 - y3; + if (dy2 == 0.0f && dy3 == 0.0f) { + return new PerspectiveTransform(x1 - x0, x2 - x1, x0, + y1 - y0, y2 - y1, y0, + 0.0f, 0.0f, 1.0f); + } else { + float dx1 = x1 - x2; + float dx2 = x3 - x2; + float dx3 = x0 - x1 + x2 - x3; + float dy1 = y1 - y2; + float denominator = dx1 * dy2 - dx2 * dy1; + float a13 = (dx3 * dy2 - dx2 * dy3) / denominator; + float a23 = (dx1 * dy3 - dx3 * dy1) / denominator; + return new PerspectiveTransform(x1 - x0 + a13 * x1, x3 - x0 + a23 * x3, x0, + y1 - y0 + a13 * y1, y3 - y0 + a23 * y3, y0, + a13, a23, 1.0f); + } + } + + public static PerspectiveTransform quadrilateralToSquare(float x0, float y0, + float x1, float y1, + float x2, float y2, + float x3, float y3) { + // Here, the adjoint serves as the inverse: + return squareToQuadrilateral(x0, y0, x1, y1, x2, y2, x3, y3).buildAdjoint(); + } + + PerspectiveTransform buildAdjoint() { + // Adjoint is the transpose of the cofactor matrix: + return new PerspectiveTransform(a22 * a33 - a23 * a32, + a23 * a31 - a21 * a33, + a21 * a32 - a22 * a31, + a13 * a32 - a12 * a33, + a11 * a33 - a13 * a31, + a12 * a31 - a11 * a32, + a12 * a23 - a13 * a22, + a13 * a21 - a11 * a23, + a11 * a22 - a12 * a21); + } + + PerspectiveTransform times(PerspectiveTransform other) { + return new PerspectiveTransform(a11 * other.a11 + a21 * other.a12 + a31 * other.a13, + a11 * other.a21 + a21 * other.a22 + a31 * other.a23, + a11 * other.a31 + a21 * other.a32 + a31 * other.a33, + a12 * other.a11 + a22 * other.a12 + a32 * other.a13, + a12 * other.a21 + a22 * other.a22 + a32 * other.a23, + a12 * other.a31 + a22 * other.a32 + a32 * other.a33, + a13 * other.a11 + a23 * other.a12 + a33 * other.a13, + a13 * other.a21 + a23 * other.a22 + a33 * other.a23, + a13 * other.a31 + a23 * other.a32 + a33 * other.a33); + + } + +} diff --git a/src/com/google/zxing/common/StringUtils.java b/src/com/google/zxing/common/StringUtils.java new file mode 100644 index 0000000..d3a7c2b --- /dev/null +++ b/src/com/google/zxing/common/StringUtils.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2010 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.common; + +import java.util.Hashtable; + +import com.google.zxing.DecodeHintType; + +/** + * Common string-related functions. + * + * @author Sean Owen + */ +public final class StringUtils { + + private static final String PLATFORM_DEFAULT_ENCODING = + System.getProperty("file.encoding"); + public static final String SHIFT_JIS = "SJIS"; + private static final String EUC_JP = "EUC_JP"; + private static final String UTF8 = "UTF8"; + private static final String ISO88591 = "ISO8859_1"; + private static final boolean ASSUME_SHIFT_JIS = + SHIFT_JIS.equalsIgnoreCase(PLATFORM_DEFAULT_ENCODING) || + EUC_JP.equalsIgnoreCase(PLATFORM_DEFAULT_ENCODING); + + private StringUtils() {} + + /** + * @param bytes bytes encoding a string, whose encoding should be guessed + * @param hints decode hints if applicable + * @return name of guessed encoding; at the moment will only guess one of: + * {@link #SHIFT_JIS}, {@link #UTF8}, {@link #ISO88591}, or the platform + * default encoding if none of these can possibly be correct + */ + public static String guessEncoding(byte[] bytes, Hashtable hints) { + if (hints != null) { + String characterSet = (String) hints.get(DecodeHintType.CHARACTER_SET); + if (characterSet != null) { + return characterSet; + } + } + // 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 canBeShiftJIS = true; + boolean canBeUTF8 = true; + int utf8BytesLeft = 0; + int maybeDoubleByteCount = 0; + int maybeSingleByteKatakanaCount = 0; + boolean sawLatin1Supplement = false; + boolean sawUTF8Start = false; + boolean lastWasPossibleDoubleByteStart = false; + + for (int i = 0; + i < length && (canBeISO88591 || canBeShiftJIS || canBeUTF8); + i++) { + + int value = bytes[i] & 0xFF; + + // UTF-8 stuff + if (value >= 0x80 && value <= 0xBF) { + if (utf8BytesLeft > 0) { + utf8BytesLeft--; + } + } else { + if (utf8BytesLeft > 0) { + canBeUTF8 = false; + } + if (value >= 0xC0 && value <= 0xFD) { + sawUTF8Start = true; + int valueCopy = value; + while ((valueCopy & 0x40) != 0) { + utf8BytesLeft++; + valueCopy <<= 1; + } + } + } + + // ISO-8859-1 stuff + + if ((value == 0xC2 || value == 0xC3) && i < length - 1) { + // This is really a poor hack. The slightly more exotic characters people might want to put in + // a QR Code, by which I mean the Latin-1 supplement characters (e.g. u-umlaut) have encodings + // that start with 0xC2 followed by [0xA0,0xBF], or start with 0xC3 followed by [0x80,0xBF]. + int nextValue = bytes[i + 1] & 0xFF; + if (nextValue <= 0xBF && + ((value == 0xC2 && nextValue >= 0xA0) || (value == 0xC3 && nextValue >= 0x80))) { + sawLatin1Supplement = true; + } + } + if (value >= 0x7F && value <= 0x9F) { + canBeISO88591 = false; + } + + // Shift_JIS stuff + + if (value >= 0xA1 && value <= 0xDF) { + // count the number of characters that might be a Shift_JIS single-byte Katakana character + if (!lastWasPossibleDoubleByteStart) { + maybeSingleByteKatakanaCount++; + } + } + if (!lastWasPossibleDoubleByteStart && + ((value >= 0xF0 && value <= 0xFF) || value == 0x80 || value == 0xA0)) { + canBeShiftJIS = false; + } + if (((value >= 0x81 && value <= 0x9F) || (value >= 0xE0 && value <= 0xEF))) { + // These start double-byte characters in Shift_JIS. Let's see if it's followed by a valid + // second byte. + 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 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 { + // ... otherwise do check to see if this plus the next byte form a valid + // double byte pair encoding a character. + lastWasPossibleDoubleByteStart = true; + if (i >= bytes.length - 1) { + canBeShiftJIS = false; + } else { + int nextValue = bytes[i + 1] & 0xFF; + if (nextValue < 0x40 || nextValue > 0xFC) { + canBeShiftJIS = false; + } else { + maybeDoubleByteCount++; + } + // There is some conflicting information out there about which bytes can follow which in + // double-byte Shift_JIS characters. The rule above seems to be the one that matches practice. + } + } + } else { + lastWasPossibleDoubleByteStart = false; + } + } + if (utf8BytesLeft > 0) { + canBeUTF8 = false; + } + + // Easy -- if assuming Shift_JIS and no evidence it can't be, done + if (canBeShiftJIS && ASSUME_SHIFT_JIS) { + return SHIFT_JIS; + } + if (canBeUTF8 && sawUTF8Start) { + return UTF8; + } + // Distinguishing Shift_JIS and ISO-8859-1 can be a little tough. The crude heuristic is: + // - If we saw + // - at least 3 bytes that starts a double-byte value (bytes that are rare in ISO-8859-1), or + // - over 5% of bytes could be single-byte Katakana (also rare in ISO-8859-1), + // - and, saw no sequences that are invalid in Shift_JIS, then we conclude Shift_JIS + if (canBeShiftJIS && (maybeDoubleByteCount >= 3 || 20 * maybeSingleByteKatakanaCount > length)) { + return SHIFT_JIS; + } + // Otherwise, we default to ISO-8859-1 unless we know it can't be + if (!sawLatin1Supplement && canBeISO88591) { + return ISO88591; + } + // Otherwise, we take a wild guess with platform encoding + return PLATFORM_DEFAULT_ENCODING; + } + +} diff --git a/src/com/google/zxing/common/detector/MonochromeRectangleDetector.java b/src/com/google/zxing/common/detector/MonochromeRectangleDetector.java new file mode 100644 index 0000000..950a223 --- /dev/null +++ b/src/com/google/zxing/common/detector/MonochromeRectangleDetector.java @@ -0,0 +1,209 @@ +/* + * Copyright 2009 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.common.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; + +/** + *A somewhat generic detector that looks for a barcode-like rectangular region within an image. + * It looks within a mostly white region of an image for a region of black and white, but mostly + * black. It returns the four corners of the region, as best it can determine.
+ * + * @author Sean Owen + */ +public final class MonochromeRectangleDetector { + + private static final int MAX_MODULES = 32; + + private final BitMatrix image; + + public MonochromeRectangleDetector(BitMatrix image) { + this.image = image; + } + + /** + *Detects a rectangular region of black and white -- mostly black -- with a region of mostly + * white, in an image.
+ * + * @return {@link ResultPoint}[] describing the corners of the rectangular region. The first and + * last points are opposed on the diagonal, as are the second and third. The first point will be + * the topmost point and the last, the bottommost. The second point will be leftmost and the + * third, the rightmost + * @throws NotFoundException if no Data Matrix Code can be found + */ + public ResultPoint[] detect() throws NotFoundException { + int height = image.getHeight(); + int width = image.getWidth(); + int halfHeight = height >> 1; + int halfWidth = width >> 1; + int deltaY = Math.max(1, height / (MAX_MODULES << 3)); + int deltaX = Math.max(1, width / (MAX_MODULES << 3)); + + int top = 0; + int bottom = height; + int left = 0; + int right = width; + ResultPoint pointA = findCornerFromCenter(halfWidth, 0, left, right, + halfHeight, -deltaY, top, bottom, halfWidth >> 1); + top = (int) pointA.getY() - 1; + ResultPoint pointB = findCornerFromCenter(halfWidth, -deltaX, left, right, + halfHeight, 0, top, bottom, halfHeight >> 1); + left = (int) pointB.getX() - 1; + ResultPoint pointC = findCornerFromCenter(halfWidth, deltaX, left, right, + halfHeight, 0, top, bottom, halfHeight >> 1); + right = (int) pointC.getX() + 1; + ResultPoint pointD = findCornerFromCenter(halfWidth, 0, left, right, + halfHeight, deltaY, top, bottom, halfWidth >> 1); + bottom = (int) pointD.getY() + 1; + + // Go try to find point A again with better information -- might have been off at first. + pointA = findCornerFromCenter(halfWidth, 0, left, right, + halfHeight, -deltaY, top, bottom, halfWidth >> 2); + + return new ResultPoint[] { pointA, pointB, pointC, pointD }; + } + + /** + * Attempts to locate a corner of the barcode by scanning up, down, left or right from a center + * point which should be within the barcode. + * + * @param centerX center's x component (horizontal) + * @param deltaX same as deltaY but change in x per step instead + * @param left minimum value of x + * @param right maximum value of x + * @param centerY center's y component (vertical) + * @param deltaY change in y per step. If scanning up this is negative; down, positive; + * left or right, 0 + * @param top minimum value of y to search through (meaningless when di == 0) + * @param bottom maximum value of y + * @param maxWhiteRun maximum run of white pixels that can still be considered to be within + * the barcode + * @return a {@link com.google.zxing.ResultPoint} encapsulating the corner that was found + * @throws NotFoundException if such a point cannot be found + */ + private ResultPoint findCornerFromCenter(int centerX, int deltaX, int left, int right, + int centerY, int deltaY, int top, int bottom, int maxWhiteRun) throws NotFoundException { + int[] lastRange = null; + for (int y = centerY, x = centerX; + y < bottom && y >= top && x < right && x >= left; + y += deltaY, x += deltaX) { + int[] range; + if (deltaX == 0) { + // horizontal slices, up and down + range = blackWhiteRange(y, maxWhiteRun, left, right, true); + } else { + // vertical slices, left and right + range = blackWhiteRange(x, maxWhiteRun, top, bottom, false); + } + if (range == null) { + if (lastRange == null) { + throw NotFoundException.getNotFoundInstance(); + } + // lastRange was found + if (deltaX == 0) { + int lastY = y - deltaY; + if (lastRange[0] < centerX) { + if (lastRange[1] > centerX) { + // straddle, choose one or the other based on direction + return new ResultPoint(deltaY > 0 ? lastRange[0] : lastRange[1], lastY); + } + return new ResultPoint(lastRange[0], lastY); + } else { + return new ResultPoint(lastRange[1], lastY); + } + } else { + int lastX = x - deltaX; + if (lastRange[0] < centerY) { + if (lastRange[1] > centerY) { + return new ResultPoint(lastX, deltaX < 0 ? lastRange[0] : lastRange[1]); + } + return new ResultPoint(lastX, lastRange[0]); + } else { + return new ResultPoint(lastX, lastRange[1]); + } + } + } + lastRange = range; + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Computes the start and end of a region of pixels, either horizontally or vertically, that could + * be part of a Data Matrix barcode. + * + * @param fixedDimension if scanning horizontally, this is the row (the fixed vertical location) + * where we are scanning. If scanning vertically it's the column, the fixed horizontal location + * @param maxWhiteRun largest run of white pixels that can still be considered part of the + * barcode region + * @param minDim minimum pixel location, horizontally or vertically, to consider + * @param maxDim maximum pixel location, horizontally or vertically, to consider + * @param horizontal if true, we're scanning left-right, instead of up-down + * @return int[] with start and end of found range, or null if no such range is found + * (e.g. only white was found) + */ + private int[] blackWhiteRange(int fixedDimension, int maxWhiteRun, int minDim, int maxDim, + boolean horizontal) { + + int center = (minDim + maxDim) >> 1; + + // Scan left/up first + int start = center; + while (start >= minDim) { + if (horizontal ? image.get(start, fixedDimension) : image.get(fixedDimension, start)) { + start--; + } else { + int whiteRunStart = start; + do { + start--; + } while (start >= minDim && !(horizontal ? image.get(start, fixedDimension) : + image.get(fixedDimension, start))); + int whiteRunSize = whiteRunStart - start; + if (start < minDim || whiteRunSize > maxWhiteRun) { + start = whiteRunStart; + break; + } + } + } + start++; + + // Then try right/down + int end = center; + while (end < maxDim) { + if (horizontal ? image.get(end, fixedDimension) : image.get(fixedDimension, end)) { + end++; + } else { + int whiteRunStart = end; + do { + end++; + } while (end < maxDim && !(horizontal ? image.get(end, fixedDimension) : + image.get(fixedDimension, end))); + int whiteRunSize = end - whiteRunStart; + if (end >= maxDim || whiteRunSize > maxWhiteRun) { + end = whiteRunStart; + break; + } + } + } + end--; + + return end > start ? new int[]{start, end} : null; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/common/detector/WhiteRectangleDetector.java b/src/com/google/zxing/common/detector/WhiteRectangleDetector.java new file mode 100644 index 0000000..93af0bc --- /dev/null +++ b/src/com/google/zxing/common/detector/WhiteRectangleDetector.java @@ -0,0 +1,316 @@ +/* + * Copyright 2010 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.common.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; + +/** + *+ * Detects a candidate barcode-like rectangular region within an image. It + * starts around the center of the image, increases the size of the candidate + * region until it finds a white rectangular region. By keeping track of the + * last black points it encountered, it determines the corners of the barcode. + *
+ * + * @author David Olivier + */ +public final class WhiteRectangleDetector { + + private static final int INIT_SIZE = 40; + private static final int CORR = 1; + + private final BitMatrix image; + private final int height; + private final int width; + + public WhiteRectangleDetector(BitMatrix image) { + this.image = image; + height = image.getHeight(); + width = image.getWidth(); + } + + /** + *+ * Detects a candidate barcode-like rectangular region within an image. It + * starts around the center of the image, increases the size of the candidate + * region until it finds a white rectangular region. + *
+ * + * @return {@link ResultPoint}[] describing the corners of the rectangular + * region. The first and last points are opposed on the diagonal, as + * are the second and third. The first point will be the topmost + * point and the last, the bottommost. The second point will be + * leftmost and the third, the rightmost + * @throws NotFoundException if no Data Matrix Code can be found + */ + public ResultPoint[] detect() throws NotFoundException { + + int left = (width - INIT_SIZE) >> 1; + int right = (width + INIT_SIZE) >> 1; + int up = (height - INIT_SIZE) >> 1; + int down = (height + INIT_SIZE) >> 1; + boolean sizeExceeded = false; + boolean aBlackPointFoundOnBorder = true; + boolean atLeastOneBlackPointFoundOnBorder = false; + + while (aBlackPointFoundOnBorder) { + + aBlackPointFoundOnBorder = false; + + // ..... + // . | + // ..... + boolean rightBorderNotWhite = true; + while (rightBorderNotWhite && right < width) { + rightBorderNotWhite = containsBlackPoint(up, down, right, false); + if (rightBorderNotWhite) { + right++; + aBlackPointFoundOnBorder = true; + } + } + + if (right >= width) { + sizeExceeded = true; + break; + } + + // ..... + // . . + // .___. + boolean bottomBorderNotWhite = true; + while (bottomBorderNotWhite && down < height) { + bottomBorderNotWhite = containsBlackPoint(left, right, down, true); + if (bottomBorderNotWhite) { + down++; + aBlackPointFoundOnBorder = true; + } + } + + if (down >= height) { + sizeExceeded = true; + break; + } + + // ..... + // | . + // ..... + boolean leftBorderNotWhite = true; + while (leftBorderNotWhite && left >= 0) { + leftBorderNotWhite = containsBlackPoint(up, down, left, false); + if (leftBorderNotWhite) { + left--; + aBlackPointFoundOnBorder = true; + } + } + + if (left < 0) { + sizeExceeded = true; + break; + } + + // .___. + // . . + // ..... + boolean topBorderNotWhite = true; + while (topBorderNotWhite && up >= 0) { + topBorderNotWhite = containsBlackPoint(left, right, up, true); + if (topBorderNotWhite) { + up--; + aBlackPointFoundOnBorder = true; + } + } + + if (up < 0) { + sizeExceeded = true; + break; + } + + if (aBlackPointFoundOnBorder) { + atLeastOneBlackPointFoundOnBorder = true; + } + + } + + if (!sizeExceeded && atLeastOneBlackPointFoundOnBorder) { + + int maxSize = right - left; + + ResultPoint z = null; + for (int i = 1; i < maxSize; i++) { + z = getBlackPointOnSegment(left, down - i, left + i, down); + if (z != null) { + break; + } + } + + if (z == null) { + throw NotFoundException.getNotFoundInstance(); + } + + ResultPoint t = null; + //go down right + for (int i = 1; i < maxSize; i++) { + t = getBlackPointOnSegment(left, up + i, left + i, up); + if (t != null) { + break; + } + } + + if (t == null) { + throw NotFoundException.getNotFoundInstance(); + } + + ResultPoint x = null; + //go down left + for (int i = 1; i < maxSize; i++) { + x = getBlackPointOnSegment(right, up + i, right - i, up); + if (x != null) { + break; + } + } + + if (x == null) { + throw NotFoundException.getNotFoundInstance(); + } + + ResultPoint y = null; + //go up left + for (int i = 1; i < maxSize; i++) { + y = getBlackPointOnSegment(right, down - i, right - i, down); + if (y != null) { + break; + } + } + + if (y == null) { + throw NotFoundException.getNotFoundInstance(); + } + + return centerEdges(y, z, x, t); + + } else { + throw NotFoundException.getNotFoundInstance(); + } + } + + /** + * Ends up being a bit faster than Math.round(). This merely rounds its + * argument to the nearest int, where x.5 rounds up. + */ + private static int round(float d) { + return (int) (d + 0.5f); + } + + private ResultPoint getBlackPointOnSegment(float aX, float aY, float bX, float bY) { + int dist = distanceL2(aX, aY, bX, bY); + float xStep = (bX - aX) / dist; + float yStep = (bY - aY) / dist; + + for (int i = 0; i < dist; i++) { + int x = round(aX + i * xStep); + int y = round(aY + i * yStep); + if (image.get(x, y)) { + return new ResultPoint(x, y); + } + } + return null; + } + + private static int distanceL2(float aX, float aY, float bX, float bY) { + float xDiff = aX - bX; + float yDiff = aY - bY; + return round((float) Math.sqrt(xDiff * xDiff + yDiff * yDiff)); + } + + /** + * recenters the points of a constant distance towards the center + * + * @param y bottom most point + * @param z left most point + * @param x right most point + * @param t top most point + * @return {@link ResultPoint}[] describing the corners of the rectangular + * region. The first and last points are opposed on the diagonal, as + * are the second and third. The first point will be the topmost + * point and the last, the bottommost. The second point will be + * leftmost and the third, the rightmost + */ + private ResultPoint[] centerEdges(ResultPoint y, ResultPoint z, + ResultPoint x, ResultPoint t) { + + // + // t t + // z x + // x OR z + // y y + // + + float yi = y.getX(); + float yj = y.getY(); + float zi = z.getX(); + float zj = z.getY(); + float xi = x.getX(); + float xj = x.getY(); + float ti = t.getX(); + float tj = t.getY(); + + if (yi < width / 2) { + return new ResultPoint[]{ + new ResultPoint(ti - CORR, tj + CORR), + new ResultPoint(zi + CORR, zj + CORR), + new ResultPoint(xi - CORR, xj - CORR), + new ResultPoint(yi + CORR, yj - CORR)}; + } else { + return new ResultPoint[]{ + new ResultPoint(ti + CORR, tj + CORR), + new ResultPoint(zi + CORR, zj - CORR), + new ResultPoint(xi - CORR, xj + CORR), + new ResultPoint(yi - CORR, yj - CORR)}; + } + } + + /** + * Determines whether a segment contains a black point + * + * @param a min value of the scanned coordinate + * @param b max value of the scanned coordinate + * @param fixed value of fixed coordinate + * @param horizontal set to true if scan must be horizontal, false if vertical + * @return true if a black point has been found, else false. + */ + private boolean containsBlackPoint(int a, int b, int fixed, boolean horizontal) { + + if (horizontal) { + for (int x = a; x <= b; x++) { + if (image.get(x, fixed)) { + return true; + } + } + } else { + for (int y = a; y <= b; y++) { + if (image.get(fixed, y)) { + return true; + } + } + } + + return false; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/common/reedsolomon/GF256.java b/src/com/google/zxing/common/reedsolomon/GF256.java new file mode 100644 index 0000000..9c58275 --- /dev/null +++ b/src/com/google/zxing/common/reedsolomon/GF256.java @@ -0,0 +1,139 @@ +/* + * Copyright 2007 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.common.reedsolomon; + +/** + *This class contains utility methods for performing mathematical operations over + * the Galois Field GF(256). Operations use a given primitive polynomial in calculations.
+ * + *Throughout this package, elements of GF(256) are represented as an int
+ * for convenience and speed (but at the cost of memory).
+ * Only the bottom 8 bits are really used.
Represents a polynomial whose coefficients are elements of GF(256). + * Instances of this class are immutable.
+ * + *Much credit is due to William Rucklidge since portions of this code are an indirect + * port of his C++ Reed-Solomon implementation.
+ * + * @author Sean Owen + */ +final class GF256Poly { + + private final GF256 field; + private final int[] coefficients; + + /** + * @param field the {@link GF256} instance representing the field to use + * to perform computations + * @param coefficients coefficients as ints representing elements of GF(256), arranged + * from most significant (highest-power term) coefficient to least significant + * @throws IllegalArgumentException if argument is null or empty, + * or if leading coefficient is 0 and this is not a + * constant polynomial (that is, it is not the monomial "0") + */ + GF256Poly(GF256 field, int[] coefficients) { + if (coefficients == null || coefficients.length == 0) { + throw new IllegalArgumentException(); + } + this.field = field; + int coefficientsLength = coefficients.length; + if (coefficientsLength > 1 && coefficients[0] == 0) { + // Leading term must be non-zero for anything except the constant polynomial "0" + int firstNonZero = 1; + while (firstNonZero < coefficientsLength && coefficients[firstNonZero] == 0) { + firstNonZero++; + } + if (firstNonZero == coefficientsLength) { + this.coefficients = field.getZero().coefficients; + } else { + this.coefficients = new int[coefficientsLength - firstNonZero]; + System.arraycopy(coefficients, + firstNonZero, + this.coefficients, + 0, + this.coefficients.length); + } + } else { + this.coefficients = coefficients; + } + } + + int[] getCoefficients() { + return coefficients; + } + + /** + * @return degree of this polynomial + */ + int getDegree() { + return coefficients.length - 1; + } + + /** + * @return true iff this polynomial is the monomial "0" + */ + boolean isZero() { + return coefficients[0] == 0; + } + + /** + * @return coefficient of x^degree term in this polynomial + */ + int getCoefficient(int degree) { + return coefficients[coefficients.length - 1 - degree]; + } + + /** + * @return evaluation of this polynomial at a given point + */ + int evaluateAt(int a) { + if (a == 0) { + // Just return the x^0 coefficient + return getCoefficient(0); + } + int size = coefficients.length; + if (a == 1) { + // Just the sum of the coefficients + int result = 0; + for (int i = 0; i < size; i++) { + result = GF256.addOrSubtract(result, coefficients[i]); + } + return result; + } + int result = coefficients[0]; + for (int i = 1; i < size; i++) { + result = GF256.addOrSubtract(field.multiply(a, result), coefficients[i]); + } + return result; + } + + GF256Poly addOrSubtract(GF256Poly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("GF256Polys do not have same GF256 field"); + } + if (isZero()) { + return other; + } + if (other.isZero()) { + return this; + } + + int[] smallerCoefficients = this.coefficients; + int[] largerCoefficients = other.coefficients; + if (smallerCoefficients.length > largerCoefficients.length) { + int[] temp = smallerCoefficients; + smallerCoefficients = largerCoefficients; + largerCoefficients = temp; + } + int[] sumDiff = new int[largerCoefficients.length]; + int lengthDiff = largerCoefficients.length - smallerCoefficients.length; + // Copy high-order terms only found in higher-degree polynomial's coefficients + System.arraycopy(largerCoefficients, 0, sumDiff, 0, lengthDiff); + + for (int i = lengthDiff; i < largerCoefficients.length; i++) { + sumDiff[i] = GF256.addOrSubtract(smallerCoefficients[i - lengthDiff], largerCoefficients[i]); + } + + return new GF256Poly(field, sumDiff); + } + + GF256Poly multiply(GF256Poly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("GF256Polys do not have same GF256 field"); + } + if (isZero() || other.isZero()) { + return field.getZero(); + } + int[] aCoefficients = this.coefficients; + int aLength = aCoefficients.length; + int[] bCoefficients = other.coefficients; + int bLength = bCoefficients.length; + int[] product = new int[aLength + bLength - 1]; + for (int i = 0; i < aLength; i++) { + int aCoeff = aCoefficients[i]; + for (int j = 0; j < bLength; j++) { + product[i + j] = GF256.addOrSubtract(product[i + j], + field.multiply(aCoeff, bCoefficients[j])); + } + } + return new GF256Poly(field, product); + } + + GF256Poly multiply(int scalar) { + if (scalar == 0) { + return field.getZero(); + } + if (scalar == 1) { + return this; + } + int size = coefficients.length; + int[] product = new int[size]; + for (int i = 0; i < size; i++) { + product[i] = field.multiply(coefficients[i], scalar); + } + return new GF256Poly(field, product); + } + + GF256Poly multiplyByMonomial(int degree, int coefficient) { + if (degree < 0) { + throw new IllegalArgumentException(); + } + if (coefficient == 0) { + return field.getZero(); + } + int size = coefficients.length; + int[] product = new int[size + degree]; + for (int i = 0; i < size; i++) { + product[i] = field.multiply(coefficients[i], coefficient); + } + return new GF256Poly(field, product); + } + + GF256Poly[] divide(GF256Poly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("GF256Polys do not have same GF256 field"); + } + if (other.isZero()) { + throw new IllegalArgumentException("Divide by 0"); + } + + GF256Poly quotient = field.getZero(); + GF256Poly remainder = this; + + int denominatorLeadingTerm = other.getCoefficient(other.getDegree()); + int inverseDenominatorLeadingTerm = field.inverse(denominatorLeadingTerm); + + while (remainder.getDegree() >= other.getDegree() && !remainder.isZero()) { + int degreeDifference = remainder.getDegree() - other.getDegree(); + int scale = field.multiply(remainder.getCoefficient(remainder.getDegree()), inverseDenominatorLeadingTerm); + GF256Poly term = other.multiplyByMonomial(degreeDifference, scale); + GF256Poly iterationQuotient = field.buildMonomial(degreeDifference, scale); + quotient = quotient.addOrSubtract(iterationQuotient); + remainder = remainder.addOrSubtract(term); + } + + return new GF256Poly[] { quotient, remainder }; + } + + public String toString() { + StringBuffer result = new StringBuffer(8 * getDegree()); + for (int degree = getDegree(); degree >= 0; degree--) { + int coefficient = getCoefficient(degree); + if (coefficient != 0) { + if (coefficient < 0) { + result.append(" - "); + coefficient = -coefficient; + } else { + if (result.length() > 0) { + result.append(" + "); + } + } + if (degree == 0 || coefficient != 1) { + int alphaPower = field.log(coefficient); + if (alphaPower == 0) { + result.append('1'); + } else if (alphaPower == 1) { + result.append('a'); + } else { + result.append("a^"); + result.append(alphaPower); + } + } + if (degree != 0) { + if (degree == 1) { + result.append('x'); + } else { + result.append("x^"); + result.append(degree); + } + } + } + } + return result.toString(); + } + +} diff --git a/src/com/google/zxing/common/reedsolomon/ReedSolomonDecoder.java b/src/com/google/zxing/common/reedsolomon/ReedSolomonDecoder.java new file mode 100644 index 0000000..e7d05de --- /dev/null +++ b/src/com/google/zxing/common/reedsolomon/ReedSolomonDecoder.java @@ -0,0 +1,194 @@ +/* + * Copyright 2007 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.common.reedsolomon; + +/** + *Implements Reed-Solomon decoding, as the name implies.
+ * + *The algorithm will not be explained here, but the following references were helpful + * in creating this implementation:
+ * + *Much credit is due to William Rucklidge since portions of this code are an indirect + * port of his C++ Reed-Solomon implementation.
+ * + * @author Sean Owen + * @author William Rucklidge + * @author sanfordsquires + */ +public final class ReedSolomonDecoder { + + private final GF256 field; + + public ReedSolomonDecoder(GF256 field) { + this.field = field; + } + + /** + *Decodes given set of received codewords, which include both data and error-correction + * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place, + * in the input.
+ * + * @param received data and error-correction codewords + * @param twoS number of error-correction codewords available + * @throws ReedSolomonException if decoding fails for any reason + */ + public void decode(int[] received, int twoS) throws ReedSolomonException { + GF256Poly poly = new GF256Poly(field, received); + int[] syndromeCoefficients = new int[twoS]; + boolean dataMatrix = field.equals(GF256.DATA_MATRIX_FIELD); + boolean noError = true; + for (int i = 0; i < twoS; i++) { + // Thanks to sanfordsquires for this fix: + int eval = poly.evaluateAt(field.exp(dataMatrix ? i + 1 : i)); + syndromeCoefficients[syndromeCoefficients.length - 1 - i] = eval; + if (eval != 0) { + noError = false; + } + } + if (noError) { + return; + } + GF256Poly syndrome = new GF256Poly(field, syndromeCoefficients); + GF256Poly[] sigmaOmega = + runEuclideanAlgorithm(field.buildMonomial(twoS, 1), syndrome, twoS); + GF256Poly sigma = sigmaOmega[0]; + GF256Poly omega = sigmaOmega[1]; + int[] errorLocations = findErrorLocations(sigma); + int[] errorMagnitudes = findErrorMagnitudes(omega, errorLocations, dataMatrix); + for (int i = 0; i < errorLocations.length; i++) { + int position = received.length - 1 - field.log(errorLocations[i]); + if (position < 0) { + throw new ReedSolomonException("Bad error location"); + } + received[position] = GF256.addOrSubtract(received[position], errorMagnitudes[i]); + } + } + + private GF256Poly[] runEuclideanAlgorithm(GF256Poly a, GF256Poly b, int R) + throws ReedSolomonException { + // Assume a's degree is >= b's + if (a.getDegree() < b.getDegree()) { + GF256Poly temp = a; + a = b; + b = temp; + } + + GF256Poly rLast = a; + GF256Poly r = b; + GF256Poly sLast = field.getOne(); + GF256Poly s = field.getZero(); + GF256Poly tLast = field.getZero(); + GF256Poly t = field.getOne(); + + // Run Euclidean algorithm until r's degree is less than R/2 + while (r.getDegree() >= R / 2) { + GF256Poly rLastLast = rLast; + GF256Poly sLastLast = sLast; + GF256Poly tLastLast = tLast; + rLast = r; + sLast = s; + tLast = t; + + // Divide rLastLast by rLast, with quotient in q and remainder in r + if (rLast.isZero()) { + // Oops, Euclidean algorithm already terminated? + throw new ReedSolomonException("r_{i-1} was zero"); + } + r = rLastLast; + GF256Poly q = field.getZero(); + int denominatorLeadingTerm = rLast.getCoefficient(rLast.getDegree()); + int dltInverse = field.inverse(denominatorLeadingTerm); + while (r.getDegree() >= rLast.getDegree() && !r.isZero()) { + int degreeDiff = r.getDegree() - rLast.getDegree(); + int scale = field.multiply(r.getCoefficient(r.getDegree()), dltInverse); + q = q.addOrSubtract(field.buildMonomial(degreeDiff, scale)); + r = r.addOrSubtract(rLast.multiplyByMonomial(degreeDiff, scale)); + } + + s = q.multiply(sLast).addOrSubtract(sLastLast); + t = q.multiply(tLast).addOrSubtract(tLastLast); + } + + int sigmaTildeAtZero = t.getCoefficient(0); + if (sigmaTildeAtZero == 0) { + throw new ReedSolomonException("sigmaTilde(0) was zero"); + } + + int inverse = field.inverse(sigmaTildeAtZero); + GF256Poly sigma = t.multiply(inverse); + GF256Poly omega = r.multiply(inverse); + return new GF256Poly[]{sigma, omega}; + } + + private int[] findErrorLocations(GF256Poly errorLocator) throws ReedSolomonException { + // This is a direct application of Chien's search + int numErrors = errorLocator.getDegree(); + if (numErrors == 1) { // shortcut + return new int[] { errorLocator.getCoefficient(1) }; + } + int[] result = new int[numErrors]; + int e = 0; + for (int i = 1; i < 256 && e < numErrors; i++) { + if (errorLocator.evaluateAt(i) == 0) { + result[e] = field.inverse(i); + e++; + } + } + if (e != numErrors) { + throw new ReedSolomonException("Error locator degree does not match number of roots"); + } + return result; + } + + private int[] findErrorMagnitudes(GF256Poly errorEvaluator, int[] errorLocations, boolean dataMatrix) { + // This is directly applying Forney's Formula + int s = errorLocations.length; + int[] result = new int[s]; + for (int i = 0; i < s; i++) { + int xiInverse = field.inverse(errorLocations[i]); + int denominator = 1; + for (int j = 0; j < s; j++) { + if (i != j) { + //denominator = field.multiply(denominator, + // GF256.addOrSubtract(1, field.multiply(errorLocations[j], xiInverse))); + // Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug. + // Below is a funny-looking workaround from Steven Parkes + int term = field.multiply(errorLocations[j], xiInverse); + int termPlus1 = ((term & 0x1) == 0) ? (term | 1) : (term & ~1); + denominator = field.multiply(denominator, termPlus1); + } + } + result[i] = field.multiply(errorEvaluator.evaluateAt(xiInverse), + field.inverse(denominator)); + // Thanks to sanfordsquires for this fix: + if (dataMatrix) { + result[i] = field.multiply(result[i], xiInverse); + } + } + return result; + } + +} diff --git a/src/com/google/zxing/common/reedsolomon/ReedSolomonEncoder.java b/src/com/google/zxing/common/reedsolomon/ReedSolomonEncoder.java new file mode 100644 index 0000000..5ce80c8 --- /dev/null +++ b/src/com/google/zxing/common/reedsolomon/ReedSolomonEncoder.java @@ -0,0 +1,75 @@ +/* + * 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.common.reedsolomon; + +import java.util.Vector; + +/** + *Implements Reed-Solomon enbcoding, as the name implies.
+ * + * @author Sean Owen + * @author William Rucklidge + */ +public final class ReedSolomonEncoder { + + private final GF256 field; + private final Vector cachedGenerators; + + public ReedSolomonEncoder(GF256 field) { + if (!GF256.QR_CODE_FIELD.equals(field)) { + throw new IllegalArgumentException("Only QR Code is supported at this time"); + } + this.field = field; + this.cachedGenerators = new Vector(); + cachedGenerators.addElement(new GF256Poly(field, new int[] { 1 })); + } + + private GF256Poly buildGenerator(int degree) { + if (degree >= cachedGenerators.size()) { + GF256Poly lastGenerator = (GF256Poly) cachedGenerators.elementAt(cachedGenerators.size() - 1); + for (int d = cachedGenerators.size(); d <= degree; d++) { + GF256Poly nextGenerator = lastGenerator.multiply(new GF256Poly(field, new int[] { 1, field.exp(d - 1) })); + cachedGenerators.addElement(nextGenerator); + lastGenerator = nextGenerator; + } + } + return (GF256Poly) cachedGenerators.elementAt(degree); + } + + public void encode(int[] toEncode, int ecBytes) { + if (ecBytes == 0) { + throw new IllegalArgumentException("No error correction bytes"); + } + int dataBytes = toEncode.length - ecBytes; + if (dataBytes <= 0) { + throw new IllegalArgumentException("No data bytes provided"); + } + GF256Poly generator = buildGenerator(ecBytes); + int[] infoCoefficients = new int[dataBytes]; + System.arraycopy(toEncode, 0, infoCoefficients, 0, dataBytes); + GF256Poly info = new GF256Poly(field, infoCoefficients); + info = info.multiplyByMonomial(ecBytes, 1); + GF256Poly remainder = info.divide(generator)[1]; + int[] coefficients = remainder.getCoefficients(); + int numZeroCoefficients = ecBytes - coefficients.length; + for (int i = 0; i < numZeroCoefficients; i++) { + toEncode[dataBytes + i] = 0; + } + System.arraycopy(coefficients, 0, toEncode, dataBytes + numZeroCoefficients, coefficients.length); + } + +} diff --git a/src/com/google/zxing/common/reedsolomon/ReedSolomonException.java b/src/com/google/zxing/common/reedsolomon/ReedSolomonException.java new file mode 100644 index 0000000..d5b45a6 --- /dev/null +++ b/src/com/google/zxing/common/reedsolomon/ReedSolomonException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2007 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.common.reedsolomon; + +/** + *Thrown when an exception occurs during Reed-Solomon decoding, such as when + * there are too many errors to correct.
+ * + * @author Sean Owen + */ +public final class ReedSolomonException extends Exception { + + public ReedSolomonException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/datamatrix/DataMatrixReader.java b/src/com/google/zxing/datamatrix/DataMatrixReader.java new file mode 100644 index 0000000..25f8fa4 --- /dev/null +++ b/src/com/google/zxing/datamatrix/DataMatrixReader.java @@ -0,0 +1,161 @@ +/* + * Copyright 2007 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.datamatrix; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.datamatrix.decoder.Decoder; +import com.google.zxing.datamatrix.detector.Detector; + +import java.util.Hashtable; + +/** + * This implementation can detect and decode Data Matrix codes in an image. + * + * @author bbrown@google.com (Brian Brown) + */ +public final class DataMatrixReader implements Reader { + + private static final ResultPoint[] NO_POINTS = new ResultPoint[0]; + + private final Decoder decoder = new Decoder(); + + /** + * Locates and decodes a Data Matrix code in an image. + * + * @return a String representing the content encoded by the Data Matrix code + * @throws NotFoundException if a Data Matrix code cannot be found + * @throws FormatException if a Data Matrix code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public Result decode(BinaryBitmap image) throws NotFoundException, ChecksumException, FormatException { + return decode(image, null); + } + + public Result decode(BinaryBitmap image, Hashtable hints) + throws NotFoundException, ChecksumException, FormatException { + DecoderResult decoderResult; + ResultPoint[] points; + if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) { + BitMatrix bits = extractPureBits(image.getBlackMatrix()); + decoderResult = decoder.decode(bits); + points = NO_POINTS; + } else { + DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(); + decoderResult = decoder.decode(detectorResult.getBits()); + points = detectorResult.getPoints(); + } + Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, + BarcodeFormat.DATA_MATRIX); + if (decoderResult.getByteSegments() != null) { + result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, decoderResult.getByteSegments()); + } + if (decoderResult.getECLevel() != null) { + result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, decoderResult.getECLevel().toString()); + } + return result; + } + + public void reset() { + // do nothing + } + + /** + * This method detects a Data Matrix code in a "pure" image -- that is, pure monochrome image + * which contains only an unrotated, unskewed, image of a Data Matrix code, with some white border + * around it. This is a specialized method that works exceptionally fast in this special + * case. + * + * @see com.google.zxing.qrcode.QRCodeReader#extractPureBits(BitMatrix) + */ + private static BitMatrix extractPureBits(BitMatrix image) throws NotFoundException { + + int height = image.getHeight(); + int width = image.getWidth(); + int minDimension = Math.min(height, width); + + // And then keep tracking across the top-left black module to determine module size + //int moduleEnd = borderWidth; + int[] leftTopBlack = image.getTopLeftOnBit(); + if (leftTopBlack == null) { + throw NotFoundException.getNotFoundInstance(); + } + int x = leftTopBlack[0]; + int y = leftTopBlack[1]; + while (x < minDimension && y < minDimension && image.get(x, y)) { + x++; + } + if (x == minDimension) { + throw NotFoundException.getNotFoundInstance(); + } + + int moduleSize = x - leftTopBlack[0]; + + // And now find where the rightmost black module on the first row ends + int rowEndOfSymbol = width - 1; + while (rowEndOfSymbol >= 0 && !image.get(rowEndOfSymbol, y)) { + rowEndOfSymbol--; + } + if (rowEndOfSymbol < 0) { + throw NotFoundException.getNotFoundInstance(); + } + rowEndOfSymbol++; + + // Make sure width of barcode is a multiple of module size + if ((rowEndOfSymbol - x) % moduleSize != 0) { + throw NotFoundException.getNotFoundInstance(); + } + int dimension = 2 + ((rowEndOfSymbol - x) / moduleSize); + + y += moduleSize; + + // Push in the "border" by half the module width so that we start + // sampling in the middle of the module. Just in case the image is a + // little off, this will help recover. + x -= moduleSize >> 1; + y -= moduleSize >> 1; + + if ((x + (dimension - 1) * moduleSize) >= width || + (y + (dimension - 1) * moduleSize) >= height) { + throw NotFoundException.getNotFoundInstance(); + } + + // Now just read off the bits + BitMatrix bits = new BitMatrix(dimension); + for (int i = 0; i < dimension; i++) { + int iOffset = y + i * moduleSize; + for (int j = 0; j < dimension; j++) { + if (image.get(x + j * moduleSize, iOffset)) { + bits.set(j, i); + } + } + } + return bits; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/datamatrix/decoder/BitMatrixParser.java b/src/com/google/zxing/datamatrix/decoder/BitMatrixParser.java new file mode 100644 index 0000000..c33839b --- /dev/null +++ b/src/com/google/zxing/datamatrix/decoder/BitMatrixParser.java @@ -0,0 +1,446 @@ +/* + * Copyright 2007 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.datamatrix.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; + +/** + * @author bbrown@google.com (Brian Brown) + */ +final class BitMatrixParser { + + private final BitMatrix mappingBitMatrix; + private final BitMatrix readMappingMatrix; + private final Version version; + + /** + * @param bitMatrix {@link BitMatrix} to parse + * @throws FormatException if dimension is < 10 or > 144 or not 0 mod 2 + */ + BitMatrixParser(BitMatrix bitMatrix) throws FormatException { + int dimension = bitMatrix.getHeight(); + if (dimension < 10 || dimension > 144 || (dimension & 0x01) != 0) { + throw FormatException.getFormatInstance(); + } + + version = readVersion(bitMatrix); + this.mappingBitMatrix = extractDataRegion(bitMatrix); + // TODO(bbrown): Make this work for rectangular symbols + this.readMappingMatrix = new BitMatrix(this.mappingBitMatrix.getHeight()); + } + + /** + *Creates the version object based on the dimension of the original bit matrix from + * the datamatrix code.
+ * + *See ISO 16022:2006 Table 7 - ECC 200 symbol attributes
+ * + * @param bitMatrix Original {@link BitMatrix} including alignment patterns + * @return {@link Version} encapsulating the Data Matrix Code's "version" + * @throws FormatException if the dimensions of the mapping matrix are not valid + * Data Matrix dimensions. + */ + Version readVersion(BitMatrix bitMatrix) throws FormatException { + + if (version != null) { + return version; + } + + // TODO(bbrown): make this work for rectangular dimensions as well. + int numRows = bitMatrix.getHeight(); + int numColumns = numRows; + + return Version.getVersionForDimensions(numRows, numColumns); + } + + /** + *Reads the bits in the {@link BitMatrix} representing the mapping matrix (No alignment patterns) + * in the correct order in order to reconstitute the codewords bytes contained within the + * Data Matrix Code.
+ * + * @return bytes encoded within the Data Matrix Code + * @throws FormatException if the exact number of bytes expected is not read + */ + byte[] readCodewords() throws FormatException { + + byte[] result = new byte[version.getTotalCodewords()]; + int resultOffset = 0; + + int row = 4; + int column = 0; + // TODO(bbrown): Data Matrix can be rectangular, assuming square for now + int numRows = mappingBitMatrix.getHeight(); + int numColumns = numRows; + + boolean corner1Read = false; + boolean corner2Read = false; + boolean corner3Read = false; + boolean corner4Read = false; + + // Read all of the codewords + do { + // Check the four corner cases + if ((row == numRows) && (column == 0) && !corner1Read) { + result[resultOffset++] = (byte) readCorner1(numRows, numColumns); + row -= 2; + column +=2; + corner1Read = true; + } else if ((row == numRows-2) && (column == 0) && ((numColumns & 0x03) != 0) && !corner2Read) { + result[resultOffset++] = (byte) readCorner2(numRows, numColumns); + row -= 2; + column +=2; + corner2Read = true; + } else if ((row == numRows+4) && (column == 2) && ((numColumns & 0x07) == 0) && !corner3Read) { + result[resultOffset++] = (byte) readCorner3(numRows, numColumns); + row -= 2; + column +=2; + corner3Read = true; + } else if ((row == numRows-2) && (column == 0) && ((numColumns & 0x07) == 4) && !corner4Read) { + result[resultOffset++] = (byte) readCorner4(numRows, numColumns); + row -= 2; + column +=2; + corner4Read = true; + } else { + // Sweep upward diagonally to the right + do { + if ((row < numRows) && (column >= 0) && !readMappingMatrix.get(column, row)) { + result[resultOffset++] = (byte) readUtah(row, column, numRows, numColumns); + } + row -= 2; + column +=2; + } while ((row >= 0) && (column < numColumns)); + row += 1; + column +=3; + + // Sweep downward diagonally to the left + do { + if ((row >= 0) && (column < numColumns) && !readMappingMatrix.get(column, row)) { + result[resultOffset++] = (byte) readUtah(row, column, numRows, numColumns); + } + row += 2; + column -=2; + } while ((row < numRows) && (column >= 0)); + row += 3; + column +=1; + } + } while ((row < numRows) || (column < numColumns)); + + if (resultOffset != version.getTotalCodewords()) { + throw FormatException.getFormatInstance(); + } + return result; + } + + /** + *Reads a bit of the mapping matrix accounting for boundary wrapping.
+ * + * @param row Row to read in the mapping matrix + * @param column Column to read in the mapping matrix + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return value of the given bit in the mapping matrix + */ + boolean readModule(int row, int column, int numRows, int numColumns) { + // Adjust the row and column indices based on boundary wrapping + if (row < 0) { + row += numRows; + column += 4 - ((numRows + 4) & 0x07); + } + if (column < 0) { + column += numColumns; + row += 4 - ((numColumns + 4) & 0x07); + } + readMappingMatrix.set(column, row); + return mappingBitMatrix.get(column, row); + } + + /** + *Reads the 8 bits of the standard Utah-shaped pattern.
+ * + *See ISO 16022:2006, 5.8.1 Figure 6
+ * + * @param row Current row in the mapping matrix, anchored at the 8th bit (LSB) of the pattern + * @param column Current column in the mapping matrix, anchored at the 8th bit (LSB) of the pattern + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the utah shape + */ + int readUtah(int row, int column, int numRows, int numColumns) { + int currentByte = 0; + if (readModule(row - 2, column - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row - 2, column - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row - 1, column - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row - 1, column - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row - 1, column, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row, column - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row, column - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row, column, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *Reads the 8 bits of the special corner condition 1.
+ * + *See ISO 16022:2006, Figure F.3
+ * + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the Corner condition 1 + */ + int readCorner1(int numRows, int numColumns) { + int currentByte = 0; + if (readModule(numRows - 1, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(2, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(3, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *Reads the 8 bits of the special corner condition 2.
+ * + *See ISO 16022:2006, Figure F.4
+ * + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the Corner condition 2 + */ + int readCorner2(int numRows, int numColumns) { + int currentByte = 0; + if (readModule(numRows - 3, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 2, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 4, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 3, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *Reads the 8 bits of the special corner condition 3.
+ * + *See ISO 16022:2006, Figure F.5
+ * + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the Corner condition 3 + */ + int readCorner3(int numRows, int numColumns) { + int currentByte = 0; + if (readModule(numRows - 1, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 3, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 3, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *Reads the 8 bits of the special corner condition 4.
+ * + *See ISO 16022:2006, Figure F.6
+ * + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the Corner condition 4 + */ + int readCorner4(int numRows, int numColumns) { + int currentByte = 0; + if (readModule(numRows - 3, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 2, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(2, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(3, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *Extracts the data region from a {@link BitMatrix} that contains + * alignment patterns.
+ * + * @param bitMatrix Original {@link BitMatrix} with alignment patterns + * @return BitMatrix that has the alignment patterns removed + */ + BitMatrix extractDataRegion(BitMatrix bitMatrix) { + int symbolSizeRows = version.getSymbolSizeRows(); + int symbolSizeColumns = version.getSymbolSizeColumns(); + + // TODO(bbrown): Make this work with rectangular codes + if (bitMatrix.getHeight() != symbolSizeRows) { + throw new IllegalArgumentException("Dimension of bitMarix must match the version size"); + } + + int dataRegionSizeRows = version.getDataRegionSizeRows(); + int dataRegionSizeColumns = version.getDataRegionSizeColumns(); + + int numDataRegionsRow = symbolSizeRows / dataRegionSizeRows; + int numDataRegionsColumn = symbolSizeColumns / dataRegionSizeColumns; + + int sizeDataRegionRow = numDataRegionsRow * dataRegionSizeRows; + //int sizeDataRegionColumn = numDataRegionsColumn * dataRegionSizeColumns; + + // TODO(bbrown): Make this work with rectangular codes + BitMatrix bitMatrixWithoutAlignment = new BitMatrix(sizeDataRegionRow); + for (int dataRegionRow = 0; dataRegionRow < numDataRegionsRow; ++dataRegionRow) { + int dataRegionRowOffset = dataRegionRow * dataRegionSizeRows; + for (int dataRegionColumn = 0; dataRegionColumn < numDataRegionsColumn; ++dataRegionColumn) { + int dataRegionColumnOffset = dataRegionColumn * dataRegionSizeColumns; + for (int i = 0; i < dataRegionSizeRows; ++i) { + int readRowOffset = dataRegionRow * (dataRegionSizeRows + 2) + 1 + i; + int writeRowOffset = dataRegionRowOffset + i; + for (int j = 0; j < dataRegionSizeColumns; ++j) { + int readColumnOffset = dataRegionColumn * (dataRegionSizeColumns + 2) + 1 + j; + if (bitMatrix.get(readColumnOffset, readRowOffset)) { + int writeColumnOffset = dataRegionColumnOffset + j; + bitMatrixWithoutAlignment.set(writeColumnOffset, writeRowOffset); + } + } + } + } + } + return bitMatrixWithoutAlignment; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/datamatrix/decoder/DataBlock.java b/src/com/google/zxing/datamatrix/decoder/DataBlock.java new file mode 100644 index 0000000..1e605ac --- /dev/null +++ b/src/com/google/zxing/datamatrix/decoder/DataBlock.java @@ -0,0 +1,118 @@ +/* + * 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.datamatrix.decoder; + +/** + *Encapsulates a block of data within a Data Matrix Code. Data Matrix Codes may split their data into + * multiple blocks, each of which is a unit of data and error-correction codewords. Each + * is represented by an instance of this class.
+ * + * @author bbrown@google.com (Brian Brown) + */ +final class DataBlock { + + private final int numDataCodewords; + private final byte[] codewords; + + private DataBlock(int numDataCodewords, byte[] codewords) { + this.numDataCodewords = numDataCodewords; + this.codewords = codewords; + } + + /** + *When Data Matrix Codes use multiple data blocks, they actually interleave the bytes of each of them. + * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This + * method will separate the data into original blocks.
+ * + * @param rawCodewords bytes as read directly from the Data Matrix Code + * @param version version of the Data Matrix Code + * @return {@link DataBlock}s containing original bytes, "de-interleaved" from representation in the + * Data Matrix Code + */ + static DataBlock[] getDataBlocks(byte[] rawCodewords, + Version version) { + // Figure out the number and size of data blocks used by this version + Version.ECBlocks ecBlocks = version.getECBlocks(); + + // First count the total number of data blocks + int totalBlocks = 0; + Version.ECB[] ecBlockArray = ecBlocks.getECBlocks(); + for (int i = 0; i < ecBlockArray.length; i++) { + totalBlocks += ecBlockArray[i].getCount(); + } + + // Now establish DataBlocks of the appropriate size and number of data codewords + DataBlock[] result = new DataBlock[totalBlocks]; + int numResultBlocks = 0; + for (int j = 0; j < ecBlockArray.length; j++) { + Version.ECB ecBlock = ecBlockArray[j]; + for (int i = 0; i < ecBlock.getCount(); i++) { + int numDataCodewords = ecBlock.getDataCodewords(); + int numBlockCodewords = ecBlocks.getECCodewords() + numDataCodewords; + result[numResultBlocks++] = new DataBlock(numDataCodewords, new byte[numBlockCodewords]); + } + } + + // All blocks have the same amount of data, except that the last n + // (where n may be 0) have 1 less byte. Figure out where these start. + // TODO(bbrown): There is only one case where there is a difference for Data Matrix for size 144 + int longerBlocksTotalCodewords = result[0].codewords.length; + //int shorterBlocksTotalCodewords = longerBlocksTotalCodewords - 1; + + int longerBlocksNumDataCodewords = longerBlocksTotalCodewords - ecBlocks.getECCodewords(); + int shorterBlocksNumDataCodewords = longerBlocksNumDataCodewords - 1; + // The last elements of result may be 1 element shorter for 144 matrix + // first fill out as many elements as all of them have minus 1 + int rawCodewordsOffset = 0; + for (int i = 0; i < shorterBlocksNumDataCodewords; i++) { + for (int j = 0; j < numResultBlocks; j++) { + result[j].codewords[i] = rawCodewords[rawCodewordsOffset++]; + } + } + + // Fill out the last data block in the longer ones + boolean specialVersion = version.getVersionNumber() == 24; + int numLongerBlocks = specialVersion ? 8 : numResultBlocks; + for (int j = 0; j < numLongerBlocks; j++) { + result[j].codewords[longerBlocksNumDataCodewords - 1] = rawCodewords[rawCodewordsOffset++]; + } + + // Now add in error correction blocks + int max = result[0].codewords.length; + for (int i = longerBlocksNumDataCodewords; i < max; i++) { + for (int j = 0; j < numResultBlocks; j++) { + int iOffset = (specialVersion && j > 7) ? i - 1 : i; + result[j].codewords[iOffset] = rawCodewords[rawCodewordsOffset++]; + } + } + + if (rawCodewordsOffset != rawCodewords.length) { + throw new IllegalArgumentException(); + } + + return result; + } + + int getNumDataCodewords() { + return numDataCodewords; + } + + byte[] getCodewords() { + return codewords; + } + +} diff --git a/src/com/google/zxing/datamatrix/decoder/DecodedBitStreamParser.java b/src/com/google/zxing/datamatrix/decoder/DecodedBitStreamParser.java new file mode 100644 index 0000000..16ce63d --- /dev/null +++ b/src/com/google/zxing/datamatrix/decoder/DecodedBitStreamParser.java @@ -0,0 +1,462 @@ +/* + * 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.datamatrix.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitSource; +import com.google.zxing.common.DecoderResult; + +import java.io.UnsupportedEncodingException; +import java.util.Vector; + +/** + *Data Matrix Codes can encode text as bits in one of several modes, and can use multiple modes + * in one Data Matrix Code. This class decodes the bits back into text.
+ * + *See ISO 16022:2006, 5.2.1 - 5.2.9.2
+ * + * @author bbrown@google.com (Brian Brown) + * @author Sean Owen + */ +final class DecodedBitStreamParser { + + /** + * See ISO 16022:2006, Annex C Table C.1 + * The C40 Basic Character Set (*'s used for placeholders for the shift values) + */ + private static final char[] C40_BASIC_SET_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 char[] C40_SHIFT2_SET_CHARS = { + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', + '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_' + }; + + /** + * See ISO 16022:2006, Annex C Table C.2 + * The Text Basic Character Set (*'s used for placeholders for the shift values) + */ + private static final char[] TEXT_BASIC_SET_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 char[] TEXT_SHIFT3_SET_CHARS = { + '\'', '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', '{', '|', '}', '~', (char) 127 + }; + + private static final int PAD_ENCODE = 0; // Not really an encoding + private static final int ASCII_ENCODE = 1; + private static final int C40_ENCODE = 2; + private static final int TEXT_ENCODE = 3; + private static final int ANSIX12_ENCODE = 4; + private static final int EDIFACT_ENCODE = 5; + private static final int BASE256_ENCODE = 6; + + private DecodedBitStreamParser() { + } + + static DecoderResult decode(byte[] bytes) throws FormatException { + BitSource bits = new BitSource(bytes); + StringBuffer result = new StringBuffer(100); + StringBuffer resultTrailer = new StringBuffer(0); + Vector byteSegments = new Vector(1); + int mode = ASCII_ENCODE; + do { + if (mode == ASCII_ENCODE) { + mode = decodeAsciiSegment(bits, result, resultTrailer); + } else { + switch (mode) { + case C40_ENCODE: + decodeC40Segment(bits, result); + break; + case TEXT_ENCODE: + decodeTextSegment(bits, result); + break; + case ANSIX12_ENCODE: + decodeAnsiX12Segment(bits, result); + break; + case EDIFACT_ENCODE: + decodeEdifactSegment(bits, result); + break; + case BASE256_ENCODE: + decodeBase256Segment(bits, result, byteSegments); + break; + default: + throw FormatException.getFormatInstance(); + } + mode = ASCII_ENCODE; + } + } while (mode != PAD_ENCODE && bits.available() > 0); + if (resultTrailer.length() > 0) { + result.append(resultTrailer.toString()); + } + return new DecoderResult(bytes, result.toString(), byteSegments.isEmpty() ? null : byteSegments, null); + } + + /** + * See ISO 16022:2006, 5.2.3 and Annex C, Table C.2 + */ + private static int decodeAsciiSegment(BitSource bits, StringBuffer result, StringBuffer resultTrailer) + throws FormatException { + boolean upperShift = false; + do { + int oneByte = bits.readBits(8); + if (oneByte == 0) { + throw FormatException.getFormatInstance(); + } else if (oneByte <= 128) { // ASCII data (ASCII value + 1) + oneByte = upperShift ? (oneByte + 128) : oneByte; + upperShift = false; + result.append((char) (oneByte - 1)); + return ASCII_ENCODE; + } else if (oneByte == 129) { // Pad + return PAD_ENCODE; + } else if (oneByte <= 229) { // 2-digit data 00-99 (Numeric Value + 130) + int value = oneByte - 130; + if (value < 10) { // padd with '0' for single digit values + result.append('0'); + } + result.append(value); + } else if (oneByte == 230) { // Latch to C40 encodation + return C40_ENCODE; + } else if (oneByte == 231) { // Latch to Base 256 encodation + return BASE256_ENCODE; + } else if (oneByte == 232) { // FNC1 + //throw ReaderException.getInstance(); + // Ignore this symbol for now + } else if (oneByte == 233) { // Structured Append + //throw ReaderException.getInstance(); + // Ignore this symbol for now + } else if (oneByte == 234) { // Reader Programming + //throw ReaderException.getInstance(); + // Ignore this symbol for now + } else if (oneByte == 235) { // Upper Shift (shift to Extended ASCII) + upperShift = true; + } else if (oneByte == 236) { // 05 Macro + result.append("[)>\u001E05\u001D"); + resultTrailer.insert(0, "\u001E\u0004"); + } else if (oneByte == 237) { // 06 Macro + result.append("[)>\u001E06\u001D"); + resultTrailer.insert(0, "\u001E\u0004"); + } else if (oneByte == 238) { // Latch to ANSI X12 encodation + return ANSIX12_ENCODE; + } else if (oneByte == 239) { // Latch to Text encodation + return TEXT_ENCODE; + } else if (oneByte == 240) { // Latch to EDIFACT encodation + return EDIFACT_ENCODE; + } else if (oneByte == 241) { // ECI Character + // TODO(bbrown): I think we need to support ECI + //throw ReaderException.getInstance(); + // Ignore this symbol for now + } else if (oneByte >= 242) { // Not to be used in ASCII encodation + throw FormatException.getFormatInstance(); + } + } while (bits.available() > 0); + return ASCII_ENCODE; + } + + /** + * See ISO 16022:2006, 5.2.5 and Annex C, Table C.1 + */ + private static void decodeC40Segment(BitSource bits, StringBuffer result) throws FormatException { + // Three C40 values are encoded in a 16-bit value as + // (1600 * C1) + (40 * C2) + C3 + 1 + // TODO(bbrown): The Upper Shift with C40 doesn't work in the 4 value scenario all the time + boolean upperShift = false; + + int[] cValues = new int[3]; + do { + // If there is only one byte left then it will be encoded as ASCII + if (bits.available() == 8) { + return; + } + int firstByte = bits.readBits(8); + if (firstByte == 254) { // Unlatch codeword + return; + } + + parseTwoBytes(firstByte, bits.readBits(8), cValues); + + int shift = 0; + for (int i = 0; i < 3; i++) { + int cValue = cValues[i]; + switch (shift) { + case 0: + if (cValue < 3) { + shift = cValue + 1; + } else { + if (upperShift) { + result.append((char) (C40_BASIC_SET_CHARS[cValue] + 128)); + upperShift = false; + } else { + result.append(C40_BASIC_SET_CHARS[cValue]); + } + } + break; + case 1: + if (upperShift) { + result.append((char) (cValue + 128)); + upperShift = false; + } else { + result.append(cValue); + } + shift = 0; + break; + case 2: + if (cValue < 27) { + if (upperShift) { + result.append((char) (C40_SHIFT2_SET_CHARS[cValue] + 128)); + upperShift = false; + } else { + result.append(C40_SHIFT2_SET_CHARS[cValue]); + } + } else if (cValue == 27) { // FNC1 + throw FormatException.getFormatInstance(); + } else if (cValue == 30) { // Upper Shift + upperShift = true; + } else { + throw FormatException.getFormatInstance(); + } + shift = 0; + break; + case 3: + if (upperShift) { + result.append((char) (cValue + 224)); + upperShift = false; + } else { + result.append((char) (cValue + 96)); + } + shift = 0; + break; + default: + throw FormatException.getFormatInstance(); + } + } + } while (bits.available() > 0); + } + + /** + * See ISO 16022:2006, 5.2.6 and Annex C, Table C.2 + */ + private static void decodeTextSegment(BitSource bits, StringBuffer result) throws FormatException { + // Three Text values are encoded in a 16-bit value as + // (1600 * C1) + (40 * C2) + C3 + 1 + // TODO(bbrown): The Upper Shift with Text doesn't work in the 4 value scenario all the time + boolean upperShift = false; + + int[] cValues = new int[3]; + do { + // If there is only one byte left then it will be encoded as ASCII + if (bits.available() == 8) { + return; + } + int firstByte = bits.readBits(8); + if (firstByte == 254) { // Unlatch codeword + return; + } + + parseTwoBytes(firstByte, bits.readBits(8), cValues); + + int shift = 0; + for (int i = 0; i < 3; i++) { + int cValue = cValues[i]; + switch (shift) { + case 0: + if (cValue < 3) { + shift = cValue + 1; + } else { + if (upperShift) { + result.append((char) (TEXT_BASIC_SET_CHARS[cValue] + 128)); + upperShift = false; + } else { + result.append(TEXT_BASIC_SET_CHARS[cValue]); + } + } + break; + case 1: + if (upperShift) { + result.append((char) (cValue + 128)); + upperShift = false; + } else { + result.append(cValue); + } + shift = 0; + break; + case 2: + // Shift 2 for Text is the same encoding as C40 + if (cValue < 27) { + if (upperShift) { + result.append((char) (C40_SHIFT2_SET_CHARS[cValue] + 128)); + upperShift = false; + } else { + result.append(C40_SHIFT2_SET_CHARS[cValue]); + } + } else if (cValue == 27) { // FNC1 + throw FormatException.getFormatInstance(); + } else if (cValue == 30) { // Upper Shift + upperShift = true; + } else { + throw FormatException.getFormatInstance(); + } + shift = 0; + break; + case 3: + if (upperShift) { + result.append((char) (TEXT_SHIFT3_SET_CHARS[cValue] + 128)); + upperShift = false; + } else { + result.append(TEXT_SHIFT3_SET_CHARS[cValue]); + } + shift = 0; + break; + default: + throw FormatException.getFormatInstance(); + } + } + } while (bits.available() > 0); + } + + /** + * See ISO 16022:2006, 5.2.7 + */ + private static void decodeAnsiX12Segment(BitSource bits, StringBuffer result) throws FormatException { + // Three ANSI X12 values are encoded in a 16-bit value as + // (1600 * C1) + (40 * C2) + C3 + 1 + + int[] cValues = new int[3]; + do { + // If there is only one byte left then it will be encoded as ASCII + if (bits.available() == 8) { + return; + } + int firstByte = bits.readBits(8); + if (firstByte == 254) { // Unlatch codeword + return; + } + + parseTwoBytes(firstByte, bits.readBits(8), cValues); + + for (int i = 0; i < 3; i++) { + int cValue = cValues[i]; + if (cValue == 0) { // X12 segment terminatorThe main class which implements Data Matrix Code decoding -- as opposed to locating and extracting + * the Data Matrix Code from an image.
+ * + * @author bbrown@google.com (Brian Brown) + */ +public final class Decoder { + + private final ReedSolomonDecoder rsDecoder; + + public Decoder() { + rsDecoder = new ReedSolomonDecoder(GF256.DATA_MATRIX_FIELD); + } + + /** + *Convenience method that can decode a Data Matrix Code represented as a 2D array of booleans. + * "true" is taken to mean a black module.
+ * + * @param image booleans representing white/black Data Matrix Code modules + * @return text and bytes encoded within the Data Matrix Code + * @throws FormatException if the Data Matrix Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public DecoderResult decode(boolean[][] image) throws FormatException, ChecksumException { + int dimension = image.length; + BitMatrix bits = new BitMatrix(dimension); + for (int i = 0; i < dimension; i++) { + for (int j = 0; j < dimension; j++) { + if (image[i][j]) { + bits.set(j, i); + } + } + } + return decode(bits); + } + + /** + *Decodes a Data Matrix Code represented as a {@link BitMatrix}. A 1 or "true" is taken + * to mean a black module.
+ * + * @param bits booleans representing white/black Data Matrix Code modules + * @return text and bytes encoded within the Data Matrix Code + * @throws FormatException if the Data Matrix Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public DecoderResult decode(BitMatrix bits) throws FormatException, ChecksumException { + + // Construct a parser and read version, error-correction level + BitMatrixParser parser = new BitMatrixParser(bits); + Version version = parser.readVersion(bits); + + // Read codewords + byte[] codewords = parser.readCodewords(); + // Separate into data blocks + DataBlock[] dataBlocks = DataBlock.getDataBlocks(codewords, version); + + // Count total number of data bytes + int totalBytes = 0; + for (int i = 0; i < dataBlocks.length; i++) { + totalBytes += dataBlocks[i].getNumDataCodewords(); + } + byte[] resultBytes = new byte[totalBytes]; + int resultOffset = 0; + + // Error-correct and copy data blocks together into a stream of bytes + for (int j = 0; j < dataBlocks.length; j++) { + DataBlock dataBlock = dataBlocks[j]; + byte[] codewordBytes = dataBlock.getCodewords(); + int numDataCodewords = dataBlock.getNumDataCodewords(); + correctErrors(codewordBytes, numDataCodewords); + for (int i = 0; i < numDataCodewords; i++) { + resultBytes[resultOffset++] = codewordBytes[i]; + } + } + + // Decode the contents of that stream of bytes + return DecodedBitStreamParser.decode(resultBytes); + } + + /** + *Given data and error-correction codewords received, possibly corrupted by errors, attempts to + * correct the errors in-place using Reed-Solomon error correction.
+ * + * @param codewordBytes data and error correction codewords + * @param numDataCodewords number of codewords that are data bytes + * @throws ChecksumException if error correction fails + */ + private void correctErrors(byte[] codewordBytes, int numDataCodewords) throws ChecksumException { + int numCodewords = codewordBytes.length; + // First read into an array of ints + int[] codewordsInts = new int[numCodewords]; + for (int i = 0; i < numCodewords; i++) { + codewordsInts[i] = codewordBytes[i] & 0xFF; + } + int numECCodewords = codewordBytes.length - numDataCodewords; + try { + rsDecoder.decode(codewordsInts, numECCodewords); + } catch (ReedSolomonException rse) { + throw ChecksumException.getChecksumInstance(); + } + // Copy back into array of bytes -- only need to worry about the bytes that were data + // We don't care about errors in the error-correction codewords + for (int i = 0; i < numDataCodewords; i++) { + codewordBytes[i] = (byte) codewordsInts[i]; + } + } + +} diff --git a/src/com/google/zxing/datamatrix/decoder/Version.java b/src/com/google/zxing/datamatrix/decoder/Version.java new file mode 100644 index 0000000..45f9e35 --- /dev/null +++ b/src/com/google/zxing/datamatrix/decoder/Version.java @@ -0,0 +1,242 @@ +/* + * Copyright 2007 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.datamatrix.decoder; + +import com.google.zxing.FormatException; + +/** + * The Version object encapsulates attributes about a particular + * size Data Matrix Code. + * + * @author bbrown@google.com (Brian Brown) + */ +public final class Version { + + private static final Version[] VERSIONS = buildVersions(); + + private final int versionNumber; + private final int symbolSizeRows; + private final int symbolSizeColumns; + private final int dataRegionSizeRows; + private final int dataRegionSizeColumns; + private final ECBlocks ecBlocks; + private final int totalCodewords; + + private Version(int versionNumber, + int symbolSizeRows, + int symbolSizeColumns, + int dataRegionSizeRows, + int dataRegionSizeColumns, + ECBlocks ecBlocks) { + this.versionNumber = versionNumber; + this.symbolSizeRows = symbolSizeRows; + this.symbolSizeColumns = symbolSizeColumns; + this.dataRegionSizeRows = dataRegionSizeRows; + this.dataRegionSizeColumns = dataRegionSizeColumns; + this.ecBlocks = ecBlocks; + + // Calculate the total number of codewords + int total = 0; + int ecCodewords = ecBlocks.getECCodewords(); + ECB[] ecbArray = ecBlocks.getECBlocks(); + for (int i = 0; i < ecbArray.length; i++) { + ECB ecBlock = ecbArray[i]; + total += ecBlock.getCount() * (ecBlock.getDataCodewords() + ecCodewords); + } + this.totalCodewords = total; + } + + public int getVersionNumber() { + return versionNumber; + } + + public int getSymbolSizeRows() { + return symbolSizeRows; + } + + public int getSymbolSizeColumns() { + return symbolSizeColumns; + } + + public int getDataRegionSizeRows() { + return dataRegionSizeRows; + } + + public int getDataRegionSizeColumns() { + return dataRegionSizeColumns; + } + + public int getTotalCodewords() { + return totalCodewords; + } + + ECBlocks getECBlocks() { + return ecBlocks; + } + + /** + *Deduces version information from Data Matrix dimensions.
+ * + * @param numRows Number of rows in modules + * @param numColumns Number of columns in modules + * @return {@link Version} for a Data Matrix Code of those dimensions + * @throws FormatException if dimensions do correspond to a valid Data Matrix size + */ + public static Version getVersionForDimensions(int numRows, int numColumns) throws FormatException { + if ((numRows & 0x01) != 0 || (numColumns & 0x01) != 0) { + throw FormatException.getFormatInstance(); + } + + // TODO(bbrown): This is doing a linear search through the array of versions. + // If we interleave the rectangular versions with the square versions we could + // do a binary search. + int numVersions = VERSIONS.length; + for (int i = 0; i < numVersions; ++i){ + Version version = VERSIONS[i]; + if (version.symbolSizeRows == numRows && version.symbolSizeColumns == numColumns) { + return version; + } + } + + throw FormatException.getFormatInstance(); + } + + /** + *Encapsulates a set of error-correction blocks in one symbol version. Most versions will + * use blocks of differing sizes within one version, so, this encapsulates the parameters for + * each set of blocks. It also holds the number of error-correction codewords per block since it + * will be the same across all blocks within one version.
+ */ + static final class ECBlocks { + private final int ecCodewords; + private final ECB[] ecBlocks; + + private ECBlocks(int ecCodewords, ECB ecBlocks) { + this.ecCodewords = ecCodewords; + this.ecBlocks = new ECB[] { ecBlocks }; + } + + private ECBlocks(int ecCodewords, ECB ecBlocks1, ECB ecBlocks2) { + this.ecCodewords = ecCodewords; + this.ecBlocks = new ECB[] { ecBlocks1, ecBlocks2 }; + } + + int getECCodewords() { + return ecCodewords; + } + + ECB[] getECBlocks() { + return ecBlocks; + } + } + + /** + *Encapsualtes the parameters for one error-correction block in one symbol version. + * This includes the number of data codewords, and the number of times a block with these + * parameters is used consecutively in the Data Matrix code version's format.
+ */ + static final class ECB { + private final int count; + private final int dataCodewords; + + private ECB(int count, int dataCodewords) { + this.count = count; + this.dataCodewords = dataCodewords; + } + + int getCount() { + return count; + } + + int getDataCodewords() { + return dataCodewords; + } + } + + public String toString() { + return String.valueOf(versionNumber); + } + + /** + * See ISO 16022:2006 5.5.1 Table 7 + */ + private static Version[] buildVersions() { + return new Version[]{ + new Version(1, 10, 10, 8, 8, + new ECBlocks(5, new ECB(1, 3))), + new Version(2, 12, 12, 10, 10, + new ECBlocks(7, new ECB(1, 5))), + new Version(3, 14, 14, 12, 12, + new ECBlocks(10, new ECB(1, 8))), + new Version(4, 16, 16, 14, 14, + new ECBlocks(12, new ECB(1, 12))), + new Version(5, 18, 18, 16, 16, + new ECBlocks(14, new ECB(1, 18))), + new Version(6, 20, 20, 18, 18, + new ECBlocks(18, new ECB(1, 22))), + new Version(7, 22, 22, 20, 20, + new ECBlocks(20, new ECB(1, 30))), + new Version(8, 24, 24, 22, 22, + new ECBlocks(24, new ECB(1, 36))), + new Version(9, 26, 26, 24, 24, + new ECBlocks(28, new ECB(1, 44))), + new Version(10, 32, 32, 14, 14, + new ECBlocks(36, new ECB(1, 62))), + new Version(11, 36, 36, 16, 16, + new ECBlocks(42, new ECB(1, 86))), + new Version(12, 40, 40, 18, 18, + new ECBlocks(48, new ECB(1, 114))), + new Version(13, 44, 44, 20, 20, + new ECBlocks(56, new ECB(1, 144))), + new Version(14, 48, 48, 22, 22, + new ECBlocks(68, new ECB(1, 174))), + new Version(15, 52, 52, 24, 24, + new ECBlocks(42, new ECB(2, 102))), + new Version(16, 64, 64, 14, 14, + new ECBlocks(56, new ECB(2, 140))), + new Version(17, 72, 72, 16, 16, + new ECBlocks(36, new ECB(4, 92))), + new Version(18, 80, 80, 18, 18, + new ECBlocks(48, new ECB(4, 114))), + new Version(19, 88, 88, 20, 20, + new ECBlocks(56, new ECB(4, 144))), + new Version(20, 96, 96, 22, 22, + new ECBlocks(68, new ECB(4, 174))), + new Version(21, 104, 104, 24, 24, + new ECBlocks(56, new ECB(6, 136))), + new Version(22, 120, 120, 18, 18, + new ECBlocks(68, new ECB(6, 175))), + new Version(23, 132, 132, 20, 20, + new ECBlocks(62, new ECB(8, 163))), + new Version(24, 144, 144, 22, 22, + new ECBlocks(62, new ECB(8, 156), new ECB(2, 155))), + new Version(25, 8, 18, 6, 16, + new ECBlocks(7, new ECB(1, 5))), + new Version(26, 8, 32, 6, 14, + new ECBlocks(11, new ECB(1, 10))), + new Version(27, 12, 26, 10, 24, + new ECBlocks(14, new ECB(1, 16))), + new Version(28, 12, 36, 10, 16, + new ECBlocks(18, new ECB(1, 22))), + new Version(29, 16, 36, 10, 16, + new ECBlocks(24, new ECB(1, 32))), + new Version(30, 16, 48, 14, 22, + new ECBlocks(28, new ECB(1, 49))) + }; + } + +} diff --git a/src/com/google/zxing/datamatrix/detector/Detector.java b/src/com/google/zxing/datamatrix/detector/Detector.java new file mode 100644 index 0000000..59ffbad --- /dev/null +++ b/src/com/google/zxing/datamatrix/detector/Detector.java @@ -0,0 +1,348 @@ +/* + * 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.datamatrix.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.Collections; +import com.google.zxing.common.Comparator; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.common.GridSampler; +import com.google.zxing.common.detector.WhiteRectangleDetector; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +/** + *Encapsulates logic that can detect a Data Matrix Code in an image, even if the Data Matrix Code + * is rotated or skewed, or partially obscured.
+ * + * @author Sean Owen + */ +public final class Detector { + + // Trick to avoid creating new Integer objects below -- a sort of crude copy of + // the Integer.valueOf(int) optimization added in Java 5, not in J2ME + private static final Integer[] INTEGERS = + { new Integer(0), new Integer(1), new Integer(2), new Integer(3), new Integer(4) }; + // No, can't use valueOf() + + private final BitMatrix image; + private final WhiteRectangleDetector rectangleDetector; + + public Detector(BitMatrix image) { + this.image = image; + rectangleDetector = new WhiteRectangleDetector(image); + } + + /** + *Detects a Data Matrix Code in an image.
+ * + * @return {@link DetectorResult} encapsulating results of detecting a Data Matrix Code + * @throws NotFoundException if no Data Matrix Code can be found + */ + public DetectorResult detect() throws NotFoundException { + + ResultPoint[] cornerPoints = rectangleDetector.detect(); + ResultPoint pointA = cornerPoints[0]; + ResultPoint pointB = cornerPoints[1]; + ResultPoint pointC = cornerPoints[2]; + ResultPoint pointD = cornerPoints[3]; + + // Point A and D are across the diagonal from one another, + // as are B and C. Figure out which are the solid black lines + // by counting transitions + Vector transitions = new Vector(4); + transitions.addElement(transitionsBetween(pointA, pointB)); + transitions.addElement(transitionsBetween(pointA, pointC)); + transitions.addElement(transitionsBetween(pointB, pointD)); + transitions.addElement(transitionsBetween(pointC, pointD)); + Collections.insertionSort(transitions, new ResultPointsAndTransitionsComparator()); + + // Sort by number of transitions. First two will be the two solid sides; last two + // will be the two alternating black/white sides + ResultPointsAndTransitions lSideOne = (ResultPointsAndTransitions) transitions.elementAt(0); + ResultPointsAndTransitions lSideTwo = (ResultPointsAndTransitions) transitions.elementAt(1); + + // Figure out which point is their intersection by tallying up the number of times we see the + // endpoints in the four endpoints. One will show up twice. + Hashtable pointCount = new Hashtable(); + increment(pointCount, lSideOne.getFrom()); + increment(pointCount, lSideOne.getTo()); + increment(pointCount, lSideTwo.getFrom()); + increment(pointCount, lSideTwo.getTo()); + + ResultPoint maybeTopLeft = null; + ResultPoint bottomLeft = null; + ResultPoint maybeBottomRight = null; + Enumeration points = pointCount.keys(); + while (points.hasMoreElements()) { + ResultPoint point = (ResultPoint) points.nextElement(); + Integer value = (Integer) pointCount.get(point); + if (value.intValue() == 2) { + bottomLeft = point; // this is definitely the bottom left, then -- end of two L sides + } else { + // Otherwise it's either top left or bottom right -- just assign the two arbitrarily now + if (maybeTopLeft == null) { + maybeTopLeft = point; + } else { + maybeBottomRight = point; + } + } + } + + if (maybeTopLeft == null || bottomLeft == null || maybeBottomRight == null) { + throw NotFoundException.getNotFoundInstance(); + } + + // Bottom left is correct but top left and bottom right might be switched + ResultPoint[] corners = { maybeTopLeft, bottomLeft, maybeBottomRight }; + // Use the dot product trick to sort them out + ResultPoint.orderBestPatterns(corners); + + // Now we know which is which: + ResultPoint bottomRight = corners[0]; + bottomLeft = corners[1]; + ResultPoint topLeft = corners[2]; + + // Which point didn't we find in relation to the "L" sides? that's the top right corner + ResultPoint topRight; + if (!pointCount.containsKey(pointA)) { + topRight = pointA; + } else if (!pointCount.containsKey(pointB)) { + topRight = pointB; + } else if (!pointCount.containsKey(pointC)) { + topRight = pointC; + } else { + topRight = pointD; + } + + // Next determine the dimension by tracing along the top or right side and counting black/white + // transitions. Since we start inside a black module, we should see a number of transitions + // equal to 1 less than the code dimension. Well, actually 2 less, because we are going to + // end on a black module: + + // The top right point is actually the corner of a module, which is one of the two black modules + // adjacent to the white module at the top right. Tracing to that corner from either the top left + // or bottom right should work here. + int dimension = Math.min(transitionsBetween(topLeft, topRight).getTransitions(), + transitionsBetween(bottomRight, topRight).getTransitions()); + if ((dimension & 0x01) == 1) { + // it can't be odd, so, round... up? + dimension++; + } + dimension += 2; + + //correct top right point to match the white module + ResultPoint correctedTopRight = correctTopRight(bottomLeft, bottomRight, topLeft, topRight, dimension); + if (correctedTopRight == null){ + correctedTopRight = topRight; + } + + //We redetermine the dimension using the corrected top right point + int dimension2 = Math.max(transitionsBetween(topLeft, correctedTopRight).getTransitions(), + transitionsBetween(bottomRight, correctedTopRight).getTransitions()); + dimension2++; + if ((dimension2 & 0x01) == 1) { + dimension2++; + } + + BitMatrix bits = sampleGrid(image, topLeft, bottomLeft, bottomRight, correctedTopRight, dimension2); + + return new DetectorResult(bits, new ResultPoint[]{topLeft, bottomLeft, bottomRight, correctedTopRight}); + } + + /** + * Calculates the position of the white top right module using the output of the rectangle detector + */ + private ResultPoint correctTopRight(ResultPoint bottomLeft, + ResultPoint bottomRight, + ResultPoint topLeft, + ResultPoint topRight, + int dimension) { + + float corr = distance(bottomLeft, bottomRight) / (float)dimension; + int norm = distance(topLeft, topRight); + float cos = (topRight.getX() - topLeft.getX()) / norm; + float sin = (topRight.getY() - topLeft.getY()) / norm; + + ResultPoint c1 = new ResultPoint(topRight.getX()+corr*cos, topRight.getY()+corr*sin); + + corr = distance(bottomLeft, bottomRight) / (float)dimension; + norm = distance(bottomRight, topRight); + cos = (topRight.getX() - bottomRight.getX()) / norm; + sin = (topRight.getY() - bottomRight.getY()) / norm; + + ResultPoint c2 = new ResultPoint(topRight.getX()+corr*cos, topRight.getY()+corr*sin); + + if (!isValid(c1)){ + if (isValid(c2)){ + return c2; + } + return null; + } else if (!isValid(c2)){ + return c1; + } + + int l1 = Math.abs(transitionsBetween(topLeft, c1).getTransitions() - transitionsBetween(bottomRight, c1).getTransitions()); + int l2 = Math.abs(transitionsBetween(topLeft, c2).getTransitions() - transitionsBetween(bottomRight, c2).getTransitions()); + + if (l1 <= l2){ + return c1; + } + + return c2; + } + + private boolean isValid(ResultPoint p) { + return (p.getX() >= 0 && p.getX() < image.width && p.getY() > 0 && p.getY() < image.height); + } + + /** + * Ends up being a bit faster than Math.round(). This merely rounds its + * argument to the nearest int, where x.5 rounds up. + */ + private static int round(float d) { + return (int) (d + 0.5f); + } + +// L2 distance + private static int distance(ResultPoint a, ResultPoint b) { + return round((float) Math.sqrt((a.getX() - b.getX()) + * (a.getX() - b.getX()) + (a.getY() - b.getY()) + * (a.getY() - b.getY()))); + } + + /** + * Increments the Integer associated with a key by one. + */ + private static void increment(Hashtable table, ResultPoint key) { + Integer value = (Integer) table.get(key); + table.put(key, value == null ? INTEGERS[1] : INTEGERS[value.intValue() + 1]); + } + + private static BitMatrix sampleGrid(BitMatrix image, + ResultPoint topLeft, + ResultPoint bottomLeft, + ResultPoint bottomRight, + ResultPoint topRight, + int dimension) throws NotFoundException { + + GridSampler sampler = GridSampler.getInstance(); + + return sampler.sampleGrid(image, + dimension, + 0.5f, + 0.5f, + dimension - 0.5f, + 0.5f, + dimension - 0.5f, + dimension - 0.5f, + 0.5f, + dimension - 0.5f, + topLeft.getX(), + topLeft.getY(), + topRight.getX(), + topRight.getY(), + bottomRight.getX(), + bottomRight.getY(), + bottomLeft.getX(), + bottomLeft.getY()); + } + + /** + * Counts the number of black/white transitions between two points, using something like Bresenham's algorithm. + */ + private ResultPointsAndTransitions transitionsBetween(ResultPoint from, ResultPoint to) { + // See QR Code Detector, sizeOfBlackWhiteBlackRun() + int fromX = (int) from.getX(); + int fromY = (int) from.getY(); + int toX = (int) to.getX(); + int toY = (int) to.getY(); + boolean steep = Math.abs(toY - fromY) > Math.abs(toX - fromX); + if (steep) { + int temp = fromX; + fromX = fromY; + fromY = temp; + temp = toX; + toX = toY; + toY = temp; + } + + int dx = Math.abs(toX - fromX); + int dy = Math.abs(toY - fromY); + int error = -dx >> 1; + int ystep = fromY < toY ? 1 : -1; + int xstep = fromX < toX ? 1 : -1; + int transitions = 0; + boolean inBlack = image.get(steep ? fromY : fromX, steep ? fromX : fromY); + for (int x = fromX, y = fromY; x != toX; x += xstep) { + boolean isBlack = image.get(steep ? y : x, steep ? x : y); + if (isBlack != inBlack) { + transitions++; + inBlack = isBlack; + } + error += dy; + if (error > 0) { + if (y == toY) { + break; + } + y += ystep; + error -= dx; + } + } + return new ResultPointsAndTransitions(from, to, transitions); + } + + /** + * Simply encapsulates two points and a number of transitions between them. + */ + private static class ResultPointsAndTransitions { + private final ResultPoint from; + private final ResultPoint to; + private final int transitions; + private ResultPointsAndTransitions(ResultPoint from, ResultPoint to, int transitions) { + this.from = from; + this.to = to; + this.transitions = transitions; + } + public ResultPoint getFrom() { + return from; + } + public ResultPoint getTo() { + return to; + } + public int getTransitions() { + return transitions; + } + public String toString() { + return from + "/" + to + '/' + transitions; + } + } + + /** + * Orders ResultPointsAndTransitions by number of transitions, ascending. + */ + private static class ResultPointsAndTransitionsComparator implements Comparator { + public int compare(Object o1, Object o2) { + return ((ResultPointsAndTransitions) o1).getTransitions() - ((ResultPointsAndTransitions) o2).getTransitions(); + } + } + +} diff --git a/src/com/google/zxing/multi/ByQuadrantReader.java b/src/com/google/zxing/multi/ByQuadrantReader.java new file mode 100644 index 0000000..35904d3 --- /dev/null +++ b/src/com/google/zxing/multi/ByQuadrantReader.java @@ -0,0 +1,96 @@ +/* + * Copyright 2009 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.multi; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; + +import java.util.Hashtable; + +/** + * This class attempts to decode a barcode from an image, not by scanning the whole image, + * but by scanning subsets of the image. This is important when there may be multiple barcodes in + * an image, and detecting a barcode may find parts of multiple barcode and fail to decode + * (e.g. QR Codes). Instead this scans the four quadrants of the image -- and also the center + * 'quadrant' to cover the case where a barcode is found in the center. + * + * @see GenericMultipleBarcodeReader + */ +public final class ByQuadrantReader implements Reader { + + private final Reader delegate; + + public ByQuadrantReader(Reader delegate) { + this.delegate = delegate; + } + + public Result decode(BinaryBitmap image) + throws NotFoundException, ChecksumException, FormatException { + return decode(image, null); + } + + public Result decode(BinaryBitmap image, Hashtable hints) + throws NotFoundException, ChecksumException, FormatException { + + int width = image.getWidth(); + int height = image.getHeight(); + int halfWidth = width / 2; + int halfHeight = height / 2; + + BinaryBitmap topLeft = image.crop(0, 0, halfWidth, halfHeight); + try { + return delegate.decode(topLeft, hints); + } catch (NotFoundException re) { + // continue + } + + BinaryBitmap topRight = image.crop(halfWidth, 0, halfWidth, halfHeight); + try { + return delegate.decode(topRight, hints); + } catch (NotFoundException re) { + // continue + } + + BinaryBitmap bottomLeft = image.crop(0, halfHeight, halfWidth, halfHeight); + try { + return delegate.decode(bottomLeft, hints); + } catch (NotFoundException re) { + // continue + } + + BinaryBitmap bottomRight = image.crop(halfWidth, halfHeight, halfWidth, halfHeight); + try { + return delegate.decode(bottomRight, hints); + } catch (NotFoundException re) { + // continue + } + + int quarterWidth = halfWidth / 2; + int quarterHeight = halfHeight / 2; + BinaryBitmap center = image.crop(quarterWidth, quarterHeight, halfWidth, halfHeight); + return delegate.decode(center, hints); + } + + public void reset() { + delegate.reset(); + } + +} diff --git a/src/com/google/zxing/multi/GenericMultipleBarcodeReader.java b/src/com/google/zxing/multi/GenericMultipleBarcodeReader.java new file mode 100644 index 0000000..70d4542 --- /dev/null +++ b/src/com/google/zxing/multi/GenericMultipleBarcodeReader.java @@ -0,0 +1,156 @@ +/* + * Copyright 2009 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.multi; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; + +import java.util.Hashtable; +import java.util.Vector; + +/** + *Attempts to locate multiple barcodes in an image by repeatedly decoding portion of the image. + * After one barcode is found, the areas left, above, right and below the barcode's + * {@link com.google.zxing.ResultPoint}s are scanned, recursively.
+ * + *A caller may want to also employ {@link ByQuadrantReader} when attempting to find multiple + * 2D barcodes, like QR Codes, in an image, where the presence of multiple barcodes might prevent + * detecting any one of them.
+ * + *That is, instead of passing a {@link Reader} a caller might pass
+ * new ByQuadrantReader(reader)
.
Encapsulates logic that can detect one or more QR Codes in an image, even if the QR Code + * is rotated or skewed, or partially obscured.
+ * + * @author Sean Owen + * @author Hannes Erven + */ +public final class MultiDetector extends Detector { + + private static final DetectorResult[] EMPTY_DETECTOR_RESULTS = new DetectorResult[0]; + + public MultiDetector(BitMatrix image) { + super(image); + } + + public DetectorResult[] detectMulti(Hashtable hints) throws NotFoundException { + BitMatrix image = getImage(); + MultiFinderPatternFinder finder = new MultiFinderPatternFinder(image); + FinderPatternInfo[] info = finder.findMulti(hints); + + if (info == null || info.length == 0) { + throw NotFoundException.getNotFoundInstance(); + } + + Vector result = new Vector(); + for (int i = 0; i < info.length; i++) { + try { + result.addElement(processFinderPatternInfo(info[i])); + } catch (ReaderException e) { + // ignore + } + } + if (result.isEmpty()) { + return EMPTY_DETECTOR_RESULTS; + } else { + DetectorResult[] resultArray = new DetectorResult[result.size()]; + for (int i = 0; i < result.size(); i++) { + resultArray[i] = (DetectorResult) result.elementAt(i); + } + return resultArray; + } + } + +} diff --git a/src/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java b/src/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java new file mode 100644 index 0000000..6f19918 --- /dev/null +++ b/src/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java @@ -0,0 +1,324 @@ +/* + * Copyright 2009 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.multi.qrcode.detector; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.Collections; +import com.google.zxing.common.Comparator; +import com.google.zxing.qrcode.detector.FinderPattern; +import com.google.zxing.qrcode.detector.FinderPatternFinder; +import com.google.zxing.qrcode.detector.FinderPatternInfo; + +import java.util.Hashtable; +import java.util.Vector; + +/** + *This class attempts to find finder patterns in a QR Code. Finder patterns are the square + * markers at three corners of a QR Code.
+ * + *This class is thread-safe but not reentrant. Each thread must allocate its own object. + * + *
In contrast to {@link FinderPatternFinder}, this class will return an array of all possible + * QR code locations in the image.
+ * + *Use the TRY_HARDER hint to ask for a more thorough detection.
+ * + * @author Sean Owen + * @author Hannes Erven + */ +final class MultiFinderPatternFinder extends FinderPatternFinder { + + private static final FinderPatternInfo[] EMPTY_RESULT_ARRAY = new FinderPatternInfo[0]; + + // TODO MIN_MODULE_COUNT and MAX_MODULE_COUNT would be great hints to ask the user for + // since it limits the number of regions to decode + + // max. legal count of modules per QR code edge (177) + private static final float MAX_MODULE_COUNT_PER_EDGE = 180; + // min. legal count per modules per QR code edge (11) + private static final float MIN_MODULE_COUNT_PER_EDGE = 9; + + /** + * More or less arbitrary cutoff point for determining if two finder patterns might belong + * to the same code if they differ less than DIFF_MODSIZE_CUTOFF_PERCENT percent in their + * estimated modules sizes. + */ + private static final float DIFF_MODSIZE_CUTOFF_PERCENT = 0.05f; + + /** + * More or less arbitrary cutoff point for determining if two finder patterns might belong + * to the same code if they differ less than DIFF_MODSIZE_CUTOFF pixels/module in their + * estimated modules sizes. + */ + private static final float DIFF_MODSIZE_CUTOFF = 0.5f; + + + /** + * A comparator that orders FinderPatterns by their estimated module size. + */ + private static class ModuleSizeComparator implements Comparator { + public int compare(Object center1, Object center2) { + float value = ((FinderPattern) center2).getEstimatedModuleSize() - + ((FinderPattern) center1).getEstimatedModuleSize(); + return value < 0.0 ? -1 : value > 0.0 ? 1 : 0; + } + } + + /** + *Creates a finder that will search the image for three finder patterns.
+ * + * @param image image to search + */ + MultiFinderPatternFinder(BitMatrix image) { + super(image); + } + + MultiFinderPatternFinder(BitMatrix image, ResultPointCallback resultPointCallback) { + super(image, resultPointCallback); + } + + /** + * @return the 3 best {@link FinderPattern}s from our list of candidates. The "best" are + * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module + * size differs from the average among those patterns the least + * @throws NotFoundException if 3 such finder patterns do not exist + */ + private FinderPattern[][] selectBestPatterns() throws NotFoundException { + Vector possibleCenters = getPossibleCenters(); + int size = possibleCenters.size(); + + if (size < 3) { + // Couldn't find enough finder patterns + throw NotFoundException.getNotFoundInstance(); + } + + /* + * Begin HE modifications to safely detect multiple codes of equal size + */ + if (size == 3) { + return new FinderPattern[][]{ + new FinderPattern[]{ + (FinderPattern) possibleCenters.elementAt(0), + (FinderPattern) possibleCenters.elementAt(1), + (FinderPattern) possibleCenters.elementAt(2) + } + }; + } + + // Sort by estimated module size to speed up the upcoming checks + Collections.insertionSort(possibleCenters, new ModuleSizeComparator()); + + /* + * Now lets start: build a list of tuples of three finder locations that + * - feature similar module sizes + * - are placed in a distance so the estimated module count is within the QR specification + * - have similar distance between upper left/right and left top/bottom finder patterns + * - form a triangle with 90° angle (checked by comparing top right/bottom left distance + * with pythagoras) + * + * Note: we allow each point to be used for more than one code region: this might seem + * counterintuitive at first, but the performance penalty is not that big. At this point, + * we cannot make a good quality decision whether the three finders actually represent + * a QR code, or are just by chance layouted so it looks like there might be a QR code there. + * So, if the layout seems right, lets have the decoder try to decode. + */ + + Vector results = new Vector(); // holder for the results + + for (int i1 = 0; i1 < (size - 2); i1++) { + FinderPattern p1 = (FinderPattern) possibleCenters.elementAt(i1); + if (p1 == null) { + continue; + } + + for (int i2 = i1 + 1; i2 < (size - 1); i2++) { + FinderPattern p2 = (FinderPattern) possibleCenters.elementAt(i2); + if (p2 == null) { + continue; + } + + // Compare the expected module sizes; if they are really off, skip + float vModSize12 = (p1.getEstimatedModuleSize() - p2.getEstimatedModuleSize()) / + (Math.min(p1.getEstimatedModuleSize(), p2.getEstimatedModuleSize())); + float vModSize12A = Math.abs(p1.getEstimatedModuleSize() - p2.getEstimatedModuleSize()); + if (vModSize12A > DIFF_MODSIZE_CUTOFF && vModSize12 >= DIFF_MODSIZE_CUTOFF_PERCENT) { + // break, since elements are ordered by the module size deviation there cannot be + // any more interesting elements for the given p1. + break; + } + + for (int i3 = i2 + 1; i3 < size; i3++) { + FinderPattern p3 = (FinderPattern) possibleCenters.elementAt(i3); + if (p3 == null) { + continue; + } + + // Compare the expected module sizes; if they are really off, skip + float vModSize23 = (p2.getEstimatedModuleSize() - p3.getEstimatedModuleSize()) / + (Math.min(p2.getEstimatedModuleSize(), p3.getEstimatedModuleSize())); + float vModSize23A = Math.abs(p2.getEstimatedModuleSize() - p3.getEstimatedModuleSize()); + if (vModSize23A > DIFF_MODSIZE_CUTOFF && vModSize23 >= DIFF_MODSIZE_CUTOFF_PERCENT) { + // break, since elements are ordered by the module size deviation there cannot be + // any more interesting elements for the given p1. + break; + } + + FinderPattern[] test = {p1, p2, p3}; + ResultPoint.orderBestPatterns(test); + + // Calculate the distances: a = topleft-bottomleft, b=topleft-topright, c = diagonal + FinderPatternInfo info = new FinderPatternInfo(test); + float dA = ResultPoint.distance(info.getTopLeft(), info.getBottomLeft()); + float dC = ResultPoint.distance(info.getTopRight(), info.getBottomLeft()); + float dB = ResultPoint.distance(info.getTopLeft(), info.getTopRight()); + + // Check the sizes + float estimatedModuleCount = ((dA + dB) / p1.getEstimatedModuleSize()) / 2; + if (estimatedModuleCount > MAX_MODULE_COUNT_PER_EDGE || + estimatedModuleCount < MIN_MODULE_COUNT_PER_EDGE) { + continue; + } + + // Calculate the difference of the edge lengths in percent + float vABBC = Math.abs(((dA - dB) / Math.min(dA, dB))); + if (vABBC >= 0.1f) { + continue; + } + + // Calculate the diagonal length by assuming a 90° angle at topleft + float dCpy = (float) Math.sqrt(dA * dA + dB * dB); + // Compare to the real distance in % + float vPyC = Math.abs(((dC - dCpy) / Math.min(dC, dCpy))); + + if (vPyC >= 0.1f) { + continue; + } + + // All tests passed! + results.addElement(test); + } // end iterate p3 + } // end iterate p2 + } // end iterate p1 + + if (!results.isEmpty()) { + FinderPattern[][] resultArray = new FinderPattern[results.size()][]; + for (int i = 0; i < results.size(); i++) { + resultArray[i] = (FinderPattern[]) results.elementAt(i); + } + return resultArray; + } + + // Nothing found! + throw NotFoundException.getNotFoundInstance(); + } + + public FinderPatternInfo[] findMulti(Hashtable hints) throws NotFoundException { + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + BitMatrix image = getImage(); + int maxI = image.getHeight(); + int maxJ = image.getWidth(); + // We are looking for black/white/black/white/black modules in + // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far + + // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the + // image, and then account for the center being 3 modules in size. This gives the smallest + // number of pixels the center could be, so skip this often. When trying harder, look for all + // QR versions regardless of how dense they are. + int iSkip = (int) (maxI / (MAX_MODULES * 4.0f) * 3); + if (iSkip < MIN_SKIP || tryHarder) { + iSkip = MIN_SKIP; + } + + int[] stateCount = new int[5]; + for (int i = iSkip - 1; i < maxI; i += iSkip) { + // Get a row of black/white values + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + int currentState = 0; + for (int j = 0; j < maxJ; j++) { + if (image.get(j, i)) { + // Black pixel + if ((currentState & 1) == 1) { // Counting white pixels + currentState++; + } + stateCount[currentState]++; + } else { // White pixel + if ((currentState & 1) == 0) { // Counting black pixels + if (currentState == 4) { // A winner? + if (foundPatternCross(stateCount)) { // Yes + boolean confirmed = handlePossibleCenter(stateCount, i, j); + if (!confirmed) { + do { // Advance to next black pixel + j++; + } while (j < maxJ && !image.get(j, i)); + j--; // back up to that last white pixel + } + // Clear state to start looking again + currentState = 0; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + } else { // No, shift counts back by two + stateCount[0] = stateCount[2]; + stateCount[1] = stateCount[3]; + stateCount[2] = stateCount[4]; + stateCount[3] = 1; + stateCount[4] = 0; + currentState = 3; + } + } else { + stateCount[++currentState]++; + } + } else { // Counting white pixels + stateCount[currentState]++; + } + } + } // for j=... + + if (foundPatternCross(stateCount)) { + handlePossibleCenter(stateCount, i, maxJ); + } // end if foundPatternCross + } // for i=iSkip-1 ... + FinderPattern[][] patternInfo = selectBestPatterns(); + Vector result = new Vector(); + for (int i = 0; i < patternInfo.length; i++) { + FinderPattern[] pattern = patternInfo[i]; + ResultPoint.orderBestPatterns(pattern); + result.addElement(new FinderPatternInfo(pattern)); + } + + if (result.isEmpty()) { + return EMPTY_RESULT_ARRAY; + } else { + FinderPatternInfo[] resultArray = new FinderPatternInfo[result.size()]; + for (int i = 0; i < result.size(); i++) { + resultArray[i] = (FinderPatternInfo) result.elementAt(i); + } + return resultArray; + } + } + +} diff --git a/src/com/google/zxing/oned/CodaBarReader.java b/src/com/google/zxing/oned/CodaBarReader.java new file mode 100644 index 0000000..16ccad5 --- /dev/null +++ b/src/com/google/zxing/oned/CodaBarReader.java @@ -0,0 +1,263 @@ +/* + * 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 java.util.Hashtable; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +/** + *Decodes Codabar barcodes.
+ * + * @author Bas Vijfwinkel + */ +public final class CodaBarReader extends OneDReader { + + private static final String ALPHABET_STRING = "0123456789-$:/.+ABCDTN"; + private static final char[] ALPHABET = ALPHABET_STRING.toCharArray(); + + /** + * These represent the encodings of characters, as patterns of wide and narrow bars. The 7 least-significant bits of + * each int correspond to the pattern of wide and narrow, with 1s representing "wide" and 0s representing narrow. NOTE + * : c is equal to the * pattern NOTE : d is equal to the e pattern + */ + private static final int[] CHARACTER_ENCODINGS = { + 0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, // 0-9 + 0x00c, 0x018, 0x025, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E, // -$:/.+ABCD + 0x01A, 0x029 //TN + }; + + // minimal number of characters that should be present (inclusing start and stop characters) + // this check has been added to reduce the number of false positive on other formats + // until the cause for this behaviour has been determined + // under normal circumstances this should be set to 3 + private static final int minCharacterLength = 6; + + // multiple start/end patterns + // official start and end patterns + private static final char[] STARTEND_ENCODING = {'E', '*', 'A', 'B', 'C', 'D', 'T', 'N'}; + // some codabar generator allow the codabar string to be closed by every character + //private static final char[] STARTEND_ENCODING = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '$', ':', '/', '.', '+', 'A', 'B', 'C', 'D', 'T', 'N'}; + + // some industries use a checksum standard but this is not part of the original codabar standard + // for more information see : http://www.mecsw.com/specs/codabar.html + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) throws NotFoundException { + int[] start = findAsteriskPattern(row); + start[1] = 0; // BAS: settings this to 0 improves the recognition rate somehow? + int nextStart = start[1]; + int end = row.getSize(); + + // Read off white space + while (nextStart < end && !row.get(nextStart)) { + nextStart++; + } + + StringBuffer result = new StringBuffer(); + //int[] counters = new int[7]; + int[] counters; + int lastStart; + + do { + counters = new int[]{0, 0, 0, 0, 0, 0, 0}; // reset counters + recordPattern(row, nextStart, counters); + + char decodedChar = toNarrowWidePattern(counters); + if (decodedChar == '!') { + throw NotFoundException.getNotFoundInstance(); + } + result.append(decodedChar); + lastStart = nextStart; + for (int i = 0; i < counters.length; i++) { + nextStart += counters[i]; + } + + // Read off white space + while (nextStart < end && !row.get(nextStart)) { + nextStart++; + } + } while (nextStart < end); // no fixed end pattern so keep on reading while data is available + + // Look for whitespace after pattern: + int lastPatternSize = 0; + for (int i = 0; i < counters.length; i++) { + lastPatternSize += counters[i]; + } + + int whiteSpaceAfterEnd = nextStart - lastStart - lastPatternSize; + // If 50% of last pattern size, following last pattern, is not whitespace, fail + // (but if it's whitespace to the very end of the image, that's OK) + if ((nextStart) != end && (whiteSpaceAfterEnd / 2 < lastPatternSize)) { + throw NotFoundException.getNotFoundInstance(); + } + + // valid result? + if (result.length() < 2) + { + throw NotFoundException.getNotFoundInstance(); + } + + char startchar = result.charAt(0); + if (!arrayContains(STARTEND_ENCODING, startchar)) + { + //invalid start character + throw NotFoundException.getNotFoundInstance(); + } + + // find stop character + for (int k = 1;k < result.length() ;k++) + { + if (result.charAt(k) == startchar) + { + // found stop character -> discard rest of the string + if ((k+1) != result.length()) + { + result.delete(k+1,result.length()-1); + k = result.length();// break out of loop + } + } + } + + // remove stop/start characters character and check if a string longer than 5 characters is contained + if (result.length() > minCharacterLength) + { + result.deleteCharAt(result.length()-1); + result.deleteCharAt(0); + } + else + { + // Almost surely a false positive ( start + stop + at least 1 character) + throw NotFoundException.getNotFoundInstance(); + } + + float left = (float) (start[1] + start[0]) / 2.0f; + float right = (float) (nextStart + lastStart) / 2.0f; + return new Result( + result.toString(), + null, + new ResultPoint[]{ + new ResultPoint(left, (float) rowNumber), + new ResultPoint(right, (float) rowNumber)}, + BarcodeFormat.CODABAR); + } + + private static int[] findAsteriskPattern(BitArray row) throws NotFoundException { + int width = row.getSize(); + int rowOffset = 0; + while (rowOffset < width) { + if (row.get(rowOffset)) { + break; + } + rowOffset++; + } + + int counterPosition = 0; + int[] counters = new int[7]; + int patternStart = rowOffset; + boolean isWhite = false; + int patternLength = counters.length; + + for (int i = rowOffset; i < width; i++) { + boolean pixel = row.get(i); + if (pixel ^ isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + try { + if (arrayContains(STARTEND_ENCODING, toNarrowWidePattern(counters))) { + // Look for whitespace before start pattern, >= 50% of width of start pattern + if (row.isRange(Math.max(0, patternStart - (i - patternStart) / 2), patternStart, false)) { + return new int[]{patternStart, i}; + } + } + } catch (IllegalArgumentException re) { + // no match, continue + } + 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 ^= true; // isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static boolean arrayContains(char[] array, char key) { + if (array != null) { + for (int i = 0; i < array.length; i++) { + if (array[i] == key) { + return true; + } + } + } + return false; + } + + private static char toNarrowWidePattern(int[] counters) { + // BAS : I have changed the following part because some codabar images would fail with the original routine + // I took from the Code39Reader.java file + // ----------- change start + int numCounters = counters.length; + int maxNarrowCounter = 0; + + int minCounter = Integer.MAX_VALUE; + for (int i = 0; i < numCounters; i++) { + if (counters[i] < minCounter) { + minCounter = counters[i]; + } + if (counters[i] > maxNarrowCounter) { + maxNarrowCounter = counters[i]; + } + } + // ---------- change end + + + do { + int wideCounters = 0; + int pattern = 0; + for (int i = 0; i < numCounters; i++) { + if (counters[i] > maxNarrowCounter) { + pattern |= 1 << (numCounters - 1 - i); + wideCounters++; + } + } + + if ((wideCounters == 2) || (wideCounters == 3)) { + for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) { + if (CHARACTER_ENCODINGS[i] == pattern) { + return ALPHABET[i]; + } + } + } + maxNarrowCounter--; + } while (maxNarrowCounter > minCounter); + return '!'; + } + +} diff --git a/src/com/google/zxing/oned/Code128Reader.java b/src/com/google/zxing/oned/Code128Reader.java new file mode 100644 index 0000000..b41d954 --- /dev/null +++ b/src/com/google/zxing/oned/Code128Reader.java @@ -0,0 +1,473 @@ +/* + * 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.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Hashtable; + +/** + *Decodes Code 128 barcodes.
+ * + * @author Sean Owen + */ +public final class Code128Reader extends OneDReader { + + static final int[][] CODE_PATTERNS = { + {2, 1, 2, 2, 2, 2}, // 0 + {2, 2, 2, 1, 2, 2}, + {2, 2, 2, 2, 2, 1}, + {1, 2, 1, 2, 2, 3}, + {1, 2, 1, 3, 2, 2}, + {1, 3, 1, 2, 2, 2}, // 5 + {1, 2, 2, 2, 1, 3}, + {1, 2, 2, 3, 1, 2}, + {1, 3, 2, 2, 1, 2}, + {2, 2, 1, 2, 1, 3}, + {2, 2, 1, 3, 1, 2}, // 10 + {2, 3, 1, 2, 1, 2}, + {1, 1, 2, 2, 3, 2}, + {1, 2, 2, 1, 3, 2}, + {1, 2, 2, 2, 3, 1}, + {1, 1, 3, 2, 2, 2}, // 15 + {1, 2, 3, 1, 2, 2}, + {1, 2, 3, 2, 2, 1}, + {2, 2, 3, 2, 1, 1}, + {2, 2, 1, 1, 3, 2}, + {2, 2, 1, 2, 3, 1}, // 20 + {2, 1, 3, 2, 1, 2}, + {2, 2, 3, 1, 1, 2}, + {3, 1, 2, 1, 3, 1}, + {3, 1, 1, 2, 2, 2}, + {3, 2, 1, 1, 2, 2}, // 25 + {3, 2, 1, 2, 2, 1}, + {3, 1, 2, 2, 1, 2}, + {3, 2, 2, 1, 1, 2}, + {3, 2, 2, 2, 1, 1}, + {2, 1, 2, 1, 2, 3}, // 30 + {2, 1, 2, 3, 2, 1}, + {2, 3, 2, 1, 2, 1}, + {1, 1, 1, 3, 2, 3}, + {1, 3, 1, 1, 2, 3}, + {1, 3, 1, 3, 2, 1}, // 35 + {1, 1, 2, 3, 1, 3}, + {1, 3, 2, 1, 1, 3}, + {1, 3, 2, 3, 1, 1}, + {2, 1, 1, 3, 1, 3}, + {2, 3, 1, 1, 1, 3}, // 40 + {2, 3, 1, 3, 1, 1}, + {1, 1, 2, 1, 3, 3}, + {1, 1, 2, 3, 3, 1}, + {1, 3, 2, 1, 3, 1}, + {1, 1, 3, 1, 2, 3}, // 45 + {1, 1, 3, 3, 2, 1}, + {1, 3, 3, 1, 2, 1}, + {3, 1, 3, 1, 2, 1}, + {2, 1, 1, 3, 3, 1}, + {2, 3, 1, 1, 3, 1}, // 50 + {2, 1, 3, 1, 1, 3}, + {2, 1, 3, 3, 1, 1}, + {2, 1, 3, 1, 3, 1}, + {3, 1, 1, 1, 2, 3}, + {3, 1, 1, 3, 2, 1}, // 55 + {3, 3, 1, 1, 2, 1}, + {3, 1, 2, 1, 1, 3}, + {3, 1, 2, 3, 1, 1}, + {3, 3, 2, 1, 1, 1}, + {3, 1, 4, 1, 1, 1}, // 60 + {2, 2, 1, 4, 1, 1}, + {4, 3, 1, 1, 1, 1}, + {1, 1, 1, 2, 2, 4}, + {1, 1, 1, 4, 2, 2}, + {1, 2, 1, 1, 2, 4}, // 65 + {1, 2, 1, 4, 2, 1}, + {1, 4, 1, 1, 2, 2}, + {1, 4, 1, 2, 2, 1}, + {1, 1, 2, 2, 1, 4}, + {1, 1, 2, 4, 1, 2}, // 70 + {1, 2, 2, 1, 1, 4}, + {1, 2, 2, 4, 1, 1}, + {1, 4, 2, 1, 1, 2}, + {1, 4, 2, 2, 1, 1}, + {2, 4, 1, 2, 1, 1}, // 75 + {2, 2, 1, 1, 1, 4}, + {4, 1, 3, 1, 1, 1}, + {2, 4, 1, 1, 1, 2}, + {1, 3, 4, 1, 1, 1}, + {1, 1, 1, 2, 4, 2}, // 80 + {1, 2, 1, 1, 4, 2}, + {1, 2, 1, 2, 4, 1}, + {1, 1, 4, 2, 1, 2}, + {1, 2, 4, 1, 1, 2}, + {1, 2, 4, 2, 1, 1}, // 85 + {4, 1, 1, 2, 1, 2}, + {4, 2, 1, 1, 1, 2}, + {4, 2, 1, 2, 1, 1}, + {2, 1, 2, 1, 4, 1}, + {2, 1, 4, 1, 2, 1}, // 90 + {4, 1, 2, 1, 2, 1}, + {1, 1, 1, 1, 4, 3}, + {1, 1, 1, 3, 4, 1}, + {1, 3, 1, 1, 4, 1}, + {1, 1, 4, 1, 1, 3}, // 95 + {1, 1, 4, 3, 1, 1}, + {4, 1, 1, 1, 1, 3}, + {4, 1, 1, 3, 1, 1}, + {1, 1, 3, 1, 4, 1}, + {1, 1, 4, 1, 3, 1}, // 100 + {3, 1, 1, 1, 4, 1}, + {4, 1, 1, 1, 3, 1}, + {2, 1, 1, 4, 1, 2}, + {2, 1, 1, 2, 1, 4}, + {2, 1, 1, 2, 3, 2}, // 105 + {2, 3, 3, 1, 1, 1, 2} + }; + + private static final int MAX_AVG_VARIANCE = (int) (PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.25f); + private static final int MAX_INDIVIDUAL_VARIANCE = (int) (PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.7f); + + private static final int CODE_SHIFT = 98; + + private static final int CODE_CODE_C = 99; + private static final int CODE_CODE_B = 100; + private static final int CODE_CODE_A = 101; + + private static final int CODE_FNC_1 = 102; + private static final int CODE_FNC_2 = 97; + private static final int CODE_FNC_3 = 96; + private static final int CODE_FNC_4_A = 101; + private static final int CODE_FNC_4_B = 100; + + private static final int CODE_START_A = 103; + private static final int CODE_START_B = 104; + private static final int CODE_START_C = 105; + private static final int CODE_STOP = 106; + + private static int[] findStartPattern(BitArray row) throws NotFoundException { + int width = row.getSize(); + int rowOffset = 0; + while (rowOffset < width) { + if (row.get(rowOffset)) { + break; + } + rowOffset++; + } + + int counterPosition = 0; + int[] counters = new int[6]; + int patternStart = rowOffset; + boolean isWhite = false; + int patternLength = counters.length; + + for (int i = rowOffset; i < width; i++) { + boolean pixel = row.get(i); + if (pixel ^ isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + int bestVariance = MAX_AVG_VARIANCE; + int bestMatch = -1; + for (int startCode = CODE_START_A; startCode <= CODE_START_C; startCode++) { + int variance = patternMatchVariance(counters, CODE_PATTERNS[startCode], + MAX_INDIVIDUAL_VARIANCE); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = startCode; + } + } + if (bestMatch >= 0) { + // Look for whitespace before start pattern, >= 50% of width of start pattern + if (row.isRange(Math.max(0, patternStart - (i - patternStart) / 2), patternStart, + false)) { + return new int[]{patternStart, i, bestMatch}; + } + } + 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 NotFoundException.getNotFoundInstance(); + } + + private static int decodeCode(BitArray row, int[] counters, int rowOffset) throws NotFoundException { + recordPattern(row, rowOffset, counters); + int bestVariance = MAX_AVG_VARIANCE; // worst variance we'll accept + int bestMatch = -1; + for (int d = 0; d < CODE_PATTERNS.length; d++) { + int[] pattern = CODE_PATTERNS[d]; + int variance = patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = d; + } + } + // TODO We're overlooking the fact that the STOP pattern has 7 values, not 6. + if (bestMatch >= 0) { + return bestMatch; + } else { + throw NotFoundException.getNotFoundInstance(); + } + } + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) + throws NotFoundException, FormatException, ChecksumException { + + int[] startPatternInfo = findStartPattern(row); + int startCode = startPatternInfo[2]; + int codeSet; + switch (startCode) { + case CODE_START_A: + codeSet = CODE_CODE_A; + break; + case CODE_START_B: + codeSet = CODE_CODE_B; + break; + case CODE_START_C: + codeSet = CODE_CODE_C; + break; + default: + throw FormatException.getFormatInstance(); + } + + boolean done = false; + boolean isNextShifted = false; + + StringBuffer result = new StringBuffer(20); + int lastStart = startPatternInfo[0]; + int nextStart = startPatternInfo[1]; + int[] counters = new int[6]; + + int lastCode = 0; + int code = 0; + int checksumTotal = startCode; + int multiplier = 0; + boolean lastCharacterWasPrintable = true; + + while (!done) { + + boolean unshift = isNextShifted; + isNextShifted = false; + + // Save off last code + lastCode = code; + + // Decode another code from image + code = decodeCode(row, counters, nextStart); + + // Remember whether the last code was printable or not (excluding CODE_STOP) + if (code != CODE_STOP) { + lastCharacterWasPrintable = true; + } + + // Add to checksum computation (if not CODE_STOP of course) + if (code != CODE_STOP) { + multiplier++; + checksumTotal += multiplier * code; + } + + // Advance to where the next code will to start + lastStart = nextStart; + for (int i = 0; i < counters.length; i++) { + nextStart += counters[i]; + } + + // Take care of illegal start codes + switch (code) { + case CODE_START_A: + case CODE_START_B: + case CODE_START_C: + throw FormatException.getFormatInstance(); + } + + switch (codeSet) { + + case CODE_CODE_A: + if (code < 64) { + result.append((char) (' ' + code)); + } else if (code < 96) { + result.append((char) (code - 64)); + } else { + // Don't let CODE_STOP, which always appears, affect whether whether we think the last + // code was printable or not. + if (code != CODE_STOP) { + lastCharacterWasPrintable = false; + } + switch (code) { + case CODE_FNC_1: + case CODE_FNC_2: + case CODE_FNC_3: + case CODE_FNC_4_A: + // do nothing? + break; + case CODE_SHIFT: + isNextShifted = true; + codeSet = CODE_CODE_B; + break; + case CODE_CODE_B: + codeSet = CODE_CODE_B; + break; + case CODE_CODE_C: + codeSet = CODE_CODE_C; + break; + case CODE_STOP: + done = true; + break; + } + } + break; + case CODE_CODE_B: + if (code < 96) { + result.append((char) (' ' + code)); + } else { + if (code != CODE_STOP) { + lastCharacterWasPrintable = false; + } + switch (code) { + case CODE_FNC_1: + case CODE_FNC_2: + case CODE_FNC_3: + case CODE_FNC_4_B: + // do nothing? + break; + case CODE_SHIFT: + isNextShifted = true; + codeSet = CODE_CODE_C; + break; + case CODE_CODE_A: + codeSet = CODE_CODE_A; + break; + case CODE_CODE_C: + codeSet = CODE_CODE_C; + break; + case CODE_STOP: + done = true; + break; + } + } + break; + case CODE_CODE_C: + if (code < 100) { + if (code < 10) { + result.append('0'); + } + result.append(code); + } else { + if (code != CODE_STOP) { + lastCharacterWasPrintable = false; + } + switch (code) { + case CODE_FNC_1: + // do nothing? + break; + case CODE_CODE_A: + codeSet = CODE_CODE_A; + break; + case CODE_CODE_B: + codeSet = CODE_CODE_B; + break; + case CODE_STOP: + done = true; + break; + } + } + break; + } + + // Unshift back to another code set if we were shifted + if (unshift) { + switch (codeSet) { + case CODE_CODE_A: + codeSet = CODE_CODE_C; + break; + case CODE_CODE_B: + codeSet = CODE_CODE_A; + break; + case CODE_CODE_C: + codeSet = CODE_CODE_B; + break; + } + } + + } + + // Check for ample whitespace following pattern, but, to do this we first need to remember that + // we fudged decoding CODE_STOP since it actually has 7 bars, not 6. There is a black bar left + // to read off. Would be slightly better to properly read. Here we just skip it: + int width = row.getSize(); + while (nextStart < width && row.get(nextStart)) { + nextStart++; + } + if (!row.isRange(nextStart, Math.min(width, nextStart + (nextStart - lastStart) / 2), + false)) { + throw NotFoundException.getNotFoundInstance(); + } + + // Pull out from sum the value of the penultimate check code + checksumTotal -= multiplier * lastCode; + // lastCode is the checksum then: + if (checksumTotal % 103 != lastCode) { + throw ChecksumException.getChecksumInstance(); + } + + // Need to pull out the check digits from string + int resultLength = result.length(); + // Only bother if the result had at least one character, and if the checksum digit happened to + // be a printable character. If it was just interpreted as a control code, nothing to remove. + if (resultLength > 0 && lastCharacterWasPrintable) { + if (codeSet == CODE_CODE_C) { + result.delete(resultLength - 2, resultLength); + } else { + result.delete(resultLength - 1, resultLength); + } + } + + String resultString = result.toString(); + + if (resultString.length() == 0) { + // Almost surely a false positive + throw FormatException.getFormatInstance(); + } + + float left = (float) (startPatternInfo[1] + startPatternInfo[0]) / 2.0f; + float right = (float) (nextStart + lastStart) / 2.0f; + return new Result( + resultString, + null, + new ResultPoint[]{ + new ResultPoint(left, (float) rowNumber), + new ResultPoint(right, (float) rowNumber)}, + BarcodeFormat.CODE_128); + + } + +} diff --git a/src/com/google/zxing/oned/Code128Writer.java b/src/com/google/zxing/oned/Code128Writer.java new file mode 100644 index 0000000..22a5d66 --- /dev/null +++ b/src/com/google/zxing/oned/Code128Writer.java @@ -0,0 +1,73 @@ +/* + * Copyright 2010 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 java.util.Hashtable; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +/** + * This object renders a CODE128 code as a {@link BitMatrix}. + * + * @author erik.barbara@gmail.com (Erik Barbara) + */ +public final class Code128Writer extends UPCEANWriter { + + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Hashtable hints) throws WriterException { + if (format != BarcodeFormat.CODE_128) { + throw new IllegalArgumentException("Can only encode CODE_128, but got " + format); + } + return super.encode(contents, format, width, height, hints); + } + + public byte[] encode(String contents) { + int length = contents.length(); + if (length > 80) { + throw new IllegalArgumentException( + "Requested contents should be less than 80 digits long, but got " + length); + } + + int codeWidth = 11 + 11 + 13; //start plus check plus stop character + //get total code width for this barcode + for (int i = 0; i < length; i++) { + int[] patterns = Code128Reader.CODE_PATTERNS[contents.charAt(i) - ' ']; + for (int j = 0; j < patterns.length; j++) { + codeWidth += patterns[j]; + } + } + byte[] result = new byte[codeWidth]; + int pos = appendPattern(result, 0, Code128Reader.CODE_PATTERNS[104], 1); + int check = 104; + //append next character to bytematrix + for(int i = 0; i < length; i++) { + check += (contents.charAt(i) - ' ') * (i + 1); + pos += appendPattern(result, pos, Code128Reader.CODE_PATTERNS[contents.charAt(i) - ' '],1); + } + //compute checksum and append it along with end character and quiet space + check %= 103; + pos += appendPattern(result,pos,Code128Reader.CODE_PATTERNS[check],1); + pos += appendPattern(result,pos,Code128Reader.CODE_PATTERNS[106],1); + + return result; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/oned/Code39Reader.java b/src/com/google/zxing/oned/Code39Reader.java new file mode 100644 index 0000000..0bc66bf --- /dev/null +++ b/src/com/google/zxing/oned/Code39Reader.java @@ -0,0 +1,333 @@ +/* + * 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.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Hashtable; + +/** + *Decodes Code 39 barcodes. This does not support "Full ASCII Code 39" yet.
+ * + * @author Sean Owen + * @see Code93Reader + */ +public final class Code39Reader extends OneDReader { + + static final String ALPHABET_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. *$/+%"; + private static final char[] ALPHABET = ALPHABET_STRING.toCharArray(); + + /** + * These represent the encodings of characters, as patterns of wide and narrow bars. + * The 9 least-significant bits of each int correspond to the pattern of wide and narrow, + * with 1s representing "wide" and 0s representing narrow. + */ + static final int[] CHARACTER_ENCODINGS = { + 0x034, 0x121, 0x061, 0x160, 0x031, 0x130, 0x070, 0x025, 0x124, 0x064, // 0-9 + 0x109, 0x049, 0x148, 0x019, 0x118, 0x058, 0x00D, 0x10C, 0x04C, 0x01C, // A-J + 0x103, 0x043, 0x142, 0x013, 0x112, 0x052, 0x007, 0x106, 0x046, 0x016, // K-T + 0x181, 0x0C1, 0x1C0, 0x091, 0x190, 0x0D0, 0x085, 0x184, 0x0C4, 0x094, // U-* + 0x0A8, 0x0A2, 0x08A, 0x02A // $-% + }; + + private static final int ASTERISK_ENCODING = CHARACTER_ENCODINGS[39]; + + private final boolean usingCheckDigit; + private final boolean extendedMode; + + /** + * Creates a reader that assumes all encoded data is data, and does not treat the final + * character as a check digit. It will not decoded "extended Code 39" sequences. + */ + public Code39Reader() { + usingCheckDigit = false; + extendedMode = false; + } + + /** + * Creates a reader that can be configured to check the last character as a check digit. + * It will not decoded "extended Code 39" sequences. + * + * @param usingCheckDigit if true, treat the last data character as a check digit, not + * data, and verify that the checksum passes. + */ + public Code39Reader(boolean usingCheckDigit) { + this.usingCheckDigit = usingCheckDigit; + this.extendedMode = false; + } + + /** + * Creates a reader that can be configured to check the last character as a check digit, + * or optionally attempt to decode "extended Code 39" sequences that are used to encode + * the full ASCII character set. + * + * @param usingCheckDigit if true, treat the last data character as a check digit, not + * data, and verify that the checksum passes. + * @param extendedMode if true, will attempt to decode extended Code 39 sequences in the + * text. + */ + public Code39Reader(boolean usingCheckDigit, boolean extendedMode) { + this.usingCheckDigit = usingCheckDigit; + this.extendedMode = extendedMode; + } + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) + throws NotFoundException, ChecksumException, FormatException { + + int[] start = findAsteriskPattern(row); + int nextStart = start[1]; + int end = row.getSize(); + + // Read off white space + while (nextStart < end && !row.get(nextStart)) { + nextStart++; + } + + StringBuffer result = new StringBuffer(20); + int[] counters = new int[9]; + char decodedChar; + int lastStart; + do { + recordPattern(row, nextStart, counters); + int pattern = toNarrowWidePattern(counters); + if (pattern < 0) { + throw NotFoundException.getNotFoundInstance(); + } + decodedChar = patternToChar(pattern); + result.append(decodedChar); + lastStart = nextStart; + for (int i = 0; i < counters.length; i++) { + nextStart += counters[i]; + } + // Read off white space + while (nextStart < end && !row.get(nextStart)) { + nextStart++; + } + } while (decodedChar != '*'); + result.deleteCharAt(result.length() - 1); // remove asterisk + + // Look for whitespace after pattern: + int lastPatternSize = 0; + for (int i = 0; i < counters.length; i++) { + lastPatternSize += counters[i]; + } + int whiteSpaceAfterEnd = nextStart - lastStart - lastPatternSize; + // If 50% of last pattern size, following last pattern, is not whitespace, fail + // (but if it's whitespace to the very end of the image, that's OK) + if (nextStart != end && whiteSpaceAfterEnd / 2 < lastPatternSize) { + throw NotFoundException.getNotFoundInstance(); + } + + if (usingCheckDigit) { + int max = result.length() - 1; + int total = 0; + for (int i = 0; i < max; i++) { + total += ALPHABET_STRING.indexOf(result.charAt(i)); + } + if (result.charAt(max) != ALPHABET[total % 43]) { + throw ChecksumException.getChecksumInstance(); + } + result.deleteCharAt(max); + } + + if (result.length() == 0) { + // Almost surely a false positive + throw NotFoundException.getNotFoundInstance(); + } + + String resultString; + if (extendedMode) { + resultString = decodeExtended(result); + } else { + resultString = result.toString(); + } + + float left = (float) (start[1] + start[0]) / 2.0f; + float right = (float) (nextStart + lastStart) / 2.0f; + return new Result( + resultString, + null, + new ResultPoint[]{ + new ResultPoint(left, (float) rowNumber), + new ResultPoint(right, (float) rowNumber)}, + BarcodeFormat.CODE_39); + + } + + private static int[] findAsteriskPattern(BitArray row) throws NotFoundException { + int width = row.getSize(); + int rowOffset = 0; + while (rowOffset < width) { + if (row.get(rowOffset)) { + break; + } + rowOffset++; + } + + int counterPosition = 0; + int[] counters = new int[9]; + int patternStart = rowOffset; + boolean isWhite = false; + int patternLength = counters.length; + + for (int i = rowOffset; i < width; i++) { + boolean pixel = row.get(i); + if (pixel ^ isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + if (toNarrowWidePattern(counters) == ASTERISK_ENCODING) { + // Look for whitespace before start pattern, >= 50% of width of start pattern + if (row.isRange(Math.max(0, patternStart - (i - patternStart) / 2), patternStart, false)) { + return new int[]{patternStart, i}; + } + } + 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 NotFoundException.getNotFoundInstance(); + } + + // For efficiency, returns -1 on failure. Not throwing here saved as many as 700 exceptions + // per image when using some of our blackbox images. + private static int toNarrowWidePattern(int[] counters) { + int numCounters = counters.length; + int maxNarrowCounter = 0; + int wideCounters; + do { + int minCounter = Integer.MAX_VALUE; + for (int i = 0; i < numCounters; i++) { + int counter = counters[i]; + if (counter < minCounter && counter > maxNarrowCounter) { + minCounter = counter; + } + } + maxNarrowCounter = minCounter; + wideCounters = 0; + int totalWideCountersWidth = 0; + int pattern = 0; + for (int i = 0; i < numCounters; i++) { + int counter = counters[i]; + if (counters[i] > maxNarrowCounter) { + pattern |= 1 << (numCounters - 1 - i); + wideCounters++; + totalWideCountersWidth += counter; + } + } + if (wideCounters == 3) { + // Found 3 wide counters, but are they close enough in width? + // We can perform a cheap, conservative check to see if any individual + // counter is more than 1.5 times the average: + for (int i = 0; i < numCounters && wideCounters > 0; i++) { + int counter = counters[i]; + if (counters[i] > maxNarrowCounter) { + wideCounters--; + // totalWideCountersWidth = 3 * average, so this checks if counter >= 3/2 * average + if ((counter << 1) >= totalWideCountersWidth) { + return -1; + } + } + } + return pattern; + } + } while (wideCounters > 3); + return -1; + } + + private static char patternToChar(int pattern) throws NotFoundException { + for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) { + if (CHARACTER_ENCODINGS[i] == pattern) { + return ALPHABET[i]; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static String decodeExtended(StringBuffer encoded) throws FormatException { + int length = encoded.length(); + StringBuffer decoded = new StringBuffer(length); + for (int i = 0; i < length; i++) { + char c = encoded.charAt(i); + if (c == '+' || c == '$' || c == '%' || c == '/') { + char next = encoded.charAt(i + 1); + char decodedChar = '\0'; + switch (c) { + case '+': + // +A to +Z map to a to z + if (next >= 'A' && next <= 'Z') { + decodedChar = (char) (next + 32); + } else { + throw FormatException.getFormatInstance(); + } + break; + case '$': + // $A to $Z map to control codes SH to SB + if (next >= 'A' && next <= 'Z') { + decodedChar = (char) (next - 64); + } else { + throw FormatException.getFormatInstance(); + } + break; + case '%': + // %A to %E map to control codes ESC to US + if (next >= 'A' && next <= 'E') { + decodedChar = (char) (next - 38); + } else if (next >= 'F' && next <= 'W') { + decodedChar = (char) (next - 11); + } else { + throw FormatException.getFormatInstance(); + } + break; + case '/': + // /A to /O map to ! to , and /Z maps to : + if (next >= 'A' && next <= 'O') { + decodedChar = (char) (next - 32); + } else if (next == 'Z') { + decodedChar = ':'; + } else { + throw FormatException.getFormatInstance(); + } + break; + } + decoded.append(decodedChar); + // bump up i again since we read two characters + i++; + } else { + decoded.append(c); + } + } + return decoded.toString(); + } + +} diff --git a/src/com/google/zxing/oned/Code39Writer.java b/src/com/google/zxing/oned/Code39Writer.java new file mode 100644 index 0000000..dc9cc24 --- /dev/null +++ b/src/com/google/zxing/oned/Code39Writer.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010 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 java.util.Hashtable; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +/** + * This object renders a CODE39 code as a {@link BitMatrix}. + * + * @author erik.barbara@gmail.com (Erik Barbara) + */ +public final class Code39Writer extends UPCEANWriter { + + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Hashtable hints) throws WriterException { + if (format != BarcodeFormat.CODE_39) { + throw new IllegalArgumentException("Can only encode CODE_39, but got " + format); + } + return super.encode(contents, format, width, height, hints); + } + + public byte[] encode(String contents) { + int length = contents.length(); + if (length > 80) { + throw new IllegalArgumentException( + "Requested contents should be less than 80 digits long, but got " + length); + } + + int[] widths = new int[9]; + int codeWidth = 24 + 1 + length; + for (int i = 0; i < length; i++) { + int indexInString = Code39Reader.ALPHABET_STRING.indexOf(contents.charAt(i)); + toIntArray(Code39Reader.CHARACTER_ENCODINGS[indexInString], widths); + for(int j = 0; j < widths.length; j++) { + codeWidth += widths[j]; + } + } + byte[] result = new byte[codeWidth]; + toIntArray(Code39Reader.CHARACTER_ENCODINGS[39], widths); + int pos = appendPattern(result, 0, widths, 1); + int[] narrowWhite = {1}; + pos += appendPattern(result, pos, narrowWhite, 0); + //append next character to bytematrix + for(int i = length-1; i >= 0; i--) { + int indexInString = Code39Reader.ALPHABET_STRING.indexOf(contents.charAt(i)); + toIntArray(Code39Reader.CHARACTER_ENCODINGS[indexInString], widths); + pos += appendPattern(result, pos, widths, 1); + pos += appendPattern(result, pos, narrowWhite, 0); + } + toIntArray(Code39Reader.CHARACTER_ENCODINGS[39], widths); + pos += appendPattern(result, pos, widths, 1); + return result; + } + + private static void toIntArray(int a, int[] toReturn) { + for (int i = 0; i < 9; i++) { + int temp = a & (1 << i); + toReturn[i] = (temp == 0) ? 1 : 2; + } + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/oned/Code93Reader.java b/src/com/google/zxing/oned/Code93Reader.java new file mode 100644 index 0000000..3435169 --- /dev/null +++ b/src/com/google/zxing/oned/Code93Reader.java @@ -0,0 +1,273 @@ +/* + * Copyright 2010 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 java.util.Hashtable; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +/** + *Decodes Code 93 barcodes.
+ * + * @author Sean Owen + * @see Code39Reader + */ +public final class Code93Reader extends OneDReader { + + // Note that 'abcd' are dummy characters in place of control characters. + private static final String ALPHABET_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%abcd*"; + private static final char[] ALPHABET = ALPHABET_STRING.toCharArray(); + + /** + * These represent the encodings of characters, as patterns of wide and narrow bars. + * The 9 least-significant bits of each int correspond to the pattern of wide and narrow. + */ + private static final int[] CHARACTER_ENCODINGS = { + 0x114, 0x148, 0x144, 0x142, 0x128, 0x124, 0x122, 0x150, 0x112, 0x10A, // 0-9 + 0x1A8, 0x1A4, 0x1A2, 0x194, 0x192, 0x18A, 0x168, 0x164, 0x162, 0x134, // A-J + 0x11A, 0x158, 0x14C, 0x146, 0x12C, 0x116, 0x1B4, 0x1B2, 0x1AC, 0x1A6, // K-T + 0x196, 0x19A, 0x16C, 0x166, 0x136, 0x13A, // U-Z + 0x12E, 0x1D4, 0x1D2, 0x1CA, 0x16E, 0x176, 0x1AE, // - - % + 0x126, 0x1DA, 0x1D6, 0x132, 0x15E, // Control chars? $-* + }; + private static final int ASTERISK_ENCODING = CHARACTER_ENCODINGS[47]; + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) + throws NotFoundException, ChecksumException, FormatException { + + int[] start = findAsteriskPattern(row); + int nextStart = start[1]; + int end = row.getSize(); + + // Read off white space + while (nextStart < end && !row.get(nextStart)) { + nextStart++; + } + + StringBuffer result = new StringBuffer(20); + int[] counters = new int[6]; + char decodedChar; + int lastStart; + do { + recordPattern(row, nextStart, counters); + int pattern = toPattern(counters); + if (pattern < 0) { + throw NotFoundException.getNotFoundInstance(); + } + decodedChar = patternToChar(pattern); + result.append(decodedChar); + lastStart = nextStart; + for (int i = 0; i < counters.length; i++) { + nextStart += counters[i]; + } + // Read off white space + while (nextStart < end && !row.get(nextStart)) { + nextStart++; + } + } while (decodedChar != '*'); + result.deleteCharAt(result.length() - 1); // remove asterisk + + // Should be at least one more black module + if (nextStart == end || !row.get(nextStart)) { + throw NotFoundException.getNotFoundInstance(); + } + + if (result.length() < 2) { + // Almost surely a false positive + throw NotFoundException.getNotFoundInstance(); + } + + checkChecksums(result); + // Remove checksum digits + result.setLength(result.length() - 2); + + String resultString = decodeExtended(result); + + float left = (float) (start[1] + start[0]) / 2.0f; + float right = (float) (nextStart + lastStart) / 2.0f; + return new Result( + resultString, + null, + new ResultPoint[]{ + new ResultPoint(left, (float) rowNumber), + new ResultPoint(right, (float) rowNumber)}, + BarcodeFormat.CODE_93); + + } + + private static int[] findAsteriskPattern(BitArray row) throws NotFoundException { + int width = row.getSize(); + int rowOffset = 0; + while (rowOffset < width) { + if (row.get(rowOffset)) { + break; + } + rowOffset++; + } + + int counterPosition = 0; + int[] counters = new int[6]; + int patternStart = rowOffset; + boolean isWhite = false; + int patternLength = counters.length; + + for (int i = rowOffset; i < width; i++) { + boolean pixel = row.get(i); + if (pixel ^ isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + if (toPattern(counters) == ASTERISK_ENCODING) { + return new int[]{patternStart, i}; + } + 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 NotFoundException.getNotFoundInstance(); + } + + private static int toPattern(int[] counters) { + int max = counters.length; + int sum = 0; + for (int i = 0; i < max; i++) { + sum += counters[i]; + } + int pattern = 0; + for (int i = 0; i < max; i++) { + int scaledShifted = (counters[i] << INTEGER_MATH_SHIFT) * 9 / sum; + int scaledUnshifted = scaledShifted >> INTEGER_MATH_SHIFT; + if ((scaledShifted & 0xFF) > 0x7F) { + scaledUnshifted++; + } + if (scaledUnshifted < 1 || scaledUnshifted > 4) { + return -1; + } + if ((i & 0x01) == 0) { + for (int j = 0; j < scaledUnshifted; j++) { + pattern = (pattern << 1) | 0x01; + } + } else { + pattern <<= scaledUnshifted; + } + } + return pattern; + } + + private static char patternToChar(int pattern) throws NotFoundException { + for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) { + if (CHARACTER_ENCODINGS[i] == pattern) { + return ALPHABET[i]; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static String decodeExtended(StringBuffer encoded) throws FormatException { + int length = encoded.length(); + StringBuffer decoded = new StringBuffer(length); + for (int i = 0; i < length; i++) { + char c = encoded.charAt(i); + if (c >= 'a' && c <= 'd') { + char next = encoded.charAt(i + 1); + char decodedChar = '\0'; + switch (c) { + case 'd': + // +A to +Z map to a to z + if (next >= 'A' && next <= 'Z') { + decodedChar = (char) (next + 32); + } else { + throw FormatException.getFormatInstance(); + } + break; + case 'a': + // $A to $Z map to control codes SH to SB + if (next >= 'A' && next <= 'Z') { + decodedChar = (char) (next - 64); + } else { + throw FormatException.getFormatInstance(); + } + break; + case 'b': + // %A to %E map to control codes ESC to US + if (next >= 'A' && next <= 'E') { + decodedChar = (char) (next - 38); + } else if (next >= 'F' && next <= 'W') { + decodedChar = (char) (next - 11); + } else { + throw FormatException.getFormatInstance(); + } + break; + case 'c': + // /A to /O map to ! to , and /Z maps to : + if (next >= 'A' && next <= 'O') { + decodedChar = (char) (next - 32); + } else if (next == 'Z') { + decodedChar = ':'; + } else { + throw FormatException.getFormatInstance(); + } + break; + } + decoded.append(decodedChar); + // bump up i again since we read two characters + i++; + } else { + decoded.append(c); + } + } + return decoded.toString(); + } + + private static void checkChecksums(StringBuffer result) throws ChecksumException { + int length = result.length(); + checkOneChecksum(result, length - 2, 20); + checkOneChecksum(result, length - 1, 15); + } + + private static void checkOneChecksum(StringBuffer result, int checkPosition, int weightMax) + throws ChecksumException { + int weight = 1; + int total = 0; + for (int i = checkPosition - 1; i >= 0; i--) { + total += weight * ALPHABET_STRING.indexOf(result.charAt(i)); + if (++weight > weightMax) { + weight = 1; + } + } + if (result.charAt(checkPosition) != ALPHABET[total % 47]) { + throw ChecksumException.getChecksumInstance(); + } + } + +} diff --git a/src/com/google/zxing/oned/EAN13Reader.java b/src/com/google/zxing/oned/EAN13Reader.java new file mode 100644 index 0000000..9c90e83 --- /dev/null +++ b/src/com/google/zxing/oned/EAN13Reader.java @@ -0,0 +1,135 @@ +/* + * 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.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + *Implements decoding of the EAN-13 format.
+ * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + * @author alasdair@google.com (Alasdair Mackintosh) + */ +public final class EAN13Reader extends UPCEANReader { + + // For an EAN-13 barcode, the first digit is represented by the parities used + // to encode the next six digits, according to the table below. For example, + // if the barcode is 5 123456 789012 then the value of the first digit is + // signified by using odd for '1', even for '2', even for '3', odd for '4', + // odd for '5', and even for '6'. See http://en.wikipedia.org/wiki/EAN-13 + // + // Parity of next 6 digits + // Digit 0 1 2 3 4 5 + // 0 Odd Odd Odd Odd Odd Odd + // 1 Odd Odd Even Odd Even Even + // 2 Odd Odd Even Even Odd Even + // 3 Odd Odd Even Even Even Odd + // 4 Odd Even Odd Odd Even Even + // 5 Odd Even Even Odd Odd Even + // 6 Odd Even Even Even Odd Odd + // 7 Odd Even Odd Even Odd Even + // 8 Odd Even Odd Even Even Odd + // 9 Odd Even Even Odd Even Odd + // + // Note that the encoding for '0' uses the same parity as a UPC barcode. Hence + // a UPC barcode can be converted to an EAN-13 barcode by prepending a 0. + // + // The encoding is represented by the following array, which is a bit pattern + // using Odd = 0 and Even = 1. For example, 5 is represented by: + // + // Odd Even Even Odd Odd Even + // in binary: + // 0 1 1 0 0 1 == 0x19 + // + static final int[] FIRST_DIGIT_ENCODINGS = { + 0x00, 0x0B, 0x0D, 0xE, 0x13, 0x19, 0x1C, 0x15, 0x16, 0x1A + }; + + private final int[] decodeMiddleCounters; + + public EAN13Reader() { + decodeMiddleCounters = new int[4]; + } + + protected int decodeMiddle(BitArray row, int[] startRange, StringBuffer resultString) + throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + int lgPatternFound = 0; + + for (int x = 0; x < 6 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_AND_G_PATTERNS); + resultString.append((char) ('0' + bestMatch % 10)); + for (int i = 0; i < counters.length; i++) { + rowOffset += counters[i]; + } + if (bestMatch >= 10) { + lgPatternFound |= 1 << (5 - x); + } + } + + determineFirstDigit(resultString, lgPatternFound); + + int[] middleRange = findGuardPattern(row, rowOffset, true, MIDDLE_PATTERN); + rowOffset = middleRange[1]; + + for (int x = 0; x < 6 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_PATTERNS); + resultString.append((char) ('0' + bestMatch)); + for (int i = 0; i < counters.length; i++) { + rowOffset += counters[i]; + } + } + + return rowOffset; + } + + BarcodeFormat getBarcodeFormat() { + return BarcodeFormat.EAN_13; + } + + /** + * Based on pattern of odd-even ('L' and 'G') patterns used to encoded the explicitly-encoded + * digits in a barcode, determines the implicitly encoded first digit and adds it to the + * result string. + * + * @param resultString string to insert decoded first digit into + * @param lgPatternFound int whose bits indicates the pattern of odd/even L/G patterns used to + * encode digits + * @throws NotFoundException if first digit cannot be determined + */ + private static void determineFirstDigit(StringBuffer resultString, int lgPatternFound) + throws NotFoundException { + for (int d = 0; d < 10; d++) { + if (lgPatternFound == FIRST_DIGIT_ENCODINGS[d]) { + resultString.insert(0, (char) ('0' + d)); + return; + } + } + throw NotFoundException.getNotFoundInstance(); + } + +} diff --git a/src/com/google/zxing/oned/EAN13Writer.java b/src/com/google/zxing/oned/EAN13Writer.java new file mode 100644 index 0000000..ca6d67a --- /dev/null +++ b/src/com/google/zxing/oned/EAN13Writer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2009 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.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Hashtable; + + +/** + * This object renders an EAN13 code as a {@link BitMatrix}. + * + * @author aripollak@gmail.com (Ari Pollak) + */ +public final class EAN13Writer extends UPCEANWriter { + + private static final int codeWidth = 3 + // start guard + (7 * 6) + // left bars + 5 + // middle guard + (7 * 6) + // right bars + 3; // end guard + + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height, + Hashtable hints) throws WriterException { + if (format != BarcodeFormat.EAN_13) { + throw new IllegalArgumentException("Can only encode EAN_13, but got " + format); + } + + return super.encode(contents, format, width, height, hints); + } + + public byte[] encode(String contents) { + if (contents.length() != 13) { + throw new IllegalArgumentException( + "Requested contents should be 13 digits long, but got " + contents.length()); + } + + int firstDigit = Integer.parseInt(contents.substring(0, 1)); + int parities = EAN13Reader.FIRST_DIGIT_ENCODINGS[firstDigit]; + byte[] result = new byte[codeWidth]; + int pos = 0; + + pos += appendPattern(result, pos, UPCEANReader.START_END_PATTERN, 1); + + // See {@link #EAN13Reader} for a description of how the first digit & left bars are encoded + for (int i = 1; i <= 6; i++) { + int digit = Integer.parseInt(contents.substring(i, i + 1)); + if ((parities >> (6 - i) & 1) == 1) { + digit += 10; + } + pos += appendPattern(result, pos, UPCEANReader.L_AND_G_PATTERNS[digit], 0); + } + + pos += appendPattern(result, pos, UPCEANReader.MIDDLE_PATTERN, 0); + + for (int i = 7; i <= 12; i++) { + int digit = Integer.parseInt(contents.substring(i, i + 1)); + pos += appendPattern(result, pos, UPCEANReader.L_PATTERNS[digit], 1); + } + pos += appendPattern(result, pos, UPCEANReader.START_END_PATTERN, 1); + + return result; + } + +} diff --git a/src/com/google/zxing/oned/EAN8Reader.java b/src/com/google/zxing/oned/EAN8Reader.java new file mode 100644 index 0000000..30fbacb --- /dev/null +++ b/src/com/google/zxing/oned/EAN8Reader.java @@ -0,0 +1,72 @@ +/* + * 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.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + *Implements decoding of the EAN-8 format.
+ * + * @author Sean Owen + */ +public final class EAN8Reader extends UPCEANReader { + + private final int[] decodeMiddleCounters; + + public EAN8Reader() { + decodeMiddleCounters = new int[4]; + } + + protected int decodeMiddle(BitArray row, int[] startRange, StringBuffer result) + throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + for (int x = 0; x < 4 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_PATTERNS); + result.append((char) ('0' + bestMatch)); + for (int i = 0; i < counters.length; i++) { + rowOffset += counters[i]; + } + } + + int[] middleRange = findGuardPattern(row, rowOffset, true, MIDDLE_PATTERN); + rowOffset = middleRange[1]; + + for (int x = 0; x < 4 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_PATTERNS); + result.append((char) ('0' + bestMatch)); + for (int i = 0; i < counters.length; i++) { + rowOffset += counters[i]; + } + } + + return rowOffset; + } + + BarcodeFormat getBarcodeFormat() { + return BarcodeFormat.EAN_8; + } + +} diff --git a/src/com/google/zxing/oned/EAN8Writer.java b/src/com/google/zxing/oned/EAN8Writer.java new file mode 100644 index 0000000..4273707 --- /dev/null +++ b/src/com/google/zxing/oned/EAN8Writer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2009 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.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Hashtable; + +/** + * This object renders an EAN8 code as a {@link BitMatrix}. + * + * @author aripollak@gmail.com (Ari Pollak) + */ +public final class EAN8Writer extends UPCEANWriter { + + private static final int codeWidth = 3 + // start guard + (7 * 4) + // left bars + 5 + // middle guard + (7 * 4) + // right bars + 3; // end guard + + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height, + Hashtable hints) throws WriterException { + if (format != BarcodeFormat.EAN_8) { + throw new IllegalArgumentException("Can only encode EAN_8, but got " + + format); + } + + return super.encode(contents, format, width, height, hints); + } + + /** @return a byte array of horizontal pixels (0 = white, 1 = black) */ + public byte[] encode(String contents) { + if (contents.length() != 8) { + throw new IllegalArgumentException( + "Requested contents should be 8 digits long, but got " + contents.length()); + } + + byte[] result = new byte[codeWidth]; + int pos = 0; + + pos += appendPattern(result, pos, UPCEANReader.START_END_PATTERN, 1); + + for (int i = 0; i <= 3; i++) { + int digit = Integer.parseInt(contents.substring(i, i + 1)); + pos += appendPattern(result, pos, UPCEANReader.L_PATTERNS[digit], 0); + } + + pos += appendPattern(result, pos, UPCEANReader.MIDDLE_PATTERN, 0); + + for (int i = 4; i <= 7; i++) { + int digit = Integer.parseInt(contents.substring(i, i + 1)); + pos += appendPattern(result, pos, UPCEANReader.L_PATTERNS[digit], 1); + } + pos += appendPattern(result, pos, UPCEANReader.START_END_PATTERN, 1); + + return result; + } + +} diff --git a/src/com/google/zxing/oned/EANManufacturerOrgSupport.java b/src/com/google/zxing/oned/EANManufacturerOrgSupport.java new file mode 100644 index 0000000..c477ca8 --- /dev/null +++ b/src/com/google/zxing/oned/EANManufacturerOrgSupport.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2010 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 java.util.Vector; + +/** + * Records EAN prefix to GS1 Member Organization, where the member organization + * correlates strongly with a country. This is an imperfect means of identifying + * a country of origin by EAN-13 barcode value. See + * + * http://en.wikipedia.org/wiki/List_of_GS1_country_codes. + * + * @author Sean Owen + */ +final class EANManufacturerOrgSupport { + + private final Vector ranges = new Vector(); + private final Vector countryIdentifiers = new Vector(); + + String lookupCountryIdentifier(String productCode) { + initIfNeeded(); + int prefix = Integer.parseInt(productCode.substring(0, 3)); + int max = ranges.size(); + for (int i = 0; i < max; i++) { + int[] range = (int[]) ranges.elementAt(i); + int start = range[0]; + if (prefix < start) { + return null; + } + int end = range.length == 1 ? start : range[1]; + if (prefix <= end) { + return (String) countryIdentifiers.elementAt(i); + } + } + return null; + } + + private void add(int[] range, String id) { + ranges.addElement(range); + countryIdentifiers.addElement(id); + } + + private synchronized void initIfNeeded() { + if (!ranges.isEmpty()) { + return; + } + add(new int[] {0,19}, "US/CA"); + add(new int[] {30,39}, "US"); + add(new int[] {60,139}, "US/CA"); + add(new int[] {300,379}, "FR"); + add(new int[] {380}, "BG"); + add(new int[] {383}, "SI"); + add(new int[] {385}, "HR"); + add(new int[] {387}, "BA"); + add(new int[] {400,440}, "DE"); + add(new int[] {450,459}, "JP"); + add(new int[] {460,469}, "RU"); + add(new int[] {471}, "TW"); + add(new int[] {474}, "EE"); + add(new int[] {475}, "LV"); + add(new int[] {476}, "AZ"); + add(new int[] {477}, "LT"); + add(new int[] {478}, "UZ"); + add(new int[] {479}, "LK"); + add(new int[] {480}, "PH"); + add(new int[] {481}, "BY"); + add(new int[] {482}, "UA"); + add(new int[] {484}, "MD"); + add(new int[] {485}, "AM"); + add(new int[] {486}, "GE"); + add(new int[] {487}, "KZ"); + add(new int[] {489}, "HK"); + add(new int[] {490,499}, "JP"); + add(new int[] {500,509}, "GB"); + add(new int[] {520}, "GR"); + add(new int[] {528}, "LB"); + add(new int[] {529}, "CY"); + add(new int[] {531}, "MK"); + add(new int[] {535}, "MT"); + add(new int[] {539}, "IE"); + add(new int[] {540,549}, "BE/LU"); + add(new int[] {560}, "PT"); + add(new int[] {569}, "IS"); + add(new int[] {570,579}, "DK"); + add(new int[] {590}, "PL"); + add(new int[] {594}, "RO"); + add(new int[] {599}, "HU"); + add(new int[] {600,601}, "ZA"); + add(new int[] {603}, "GH"); + add(new int[] {608}, "BH"); + add(new int[] {609}, "MU"); + add(new int[] {611}, "MA"); + add(new int[] {613}, "DZ"); + add(new int[] {616}, "KE"); + add(new int[] {618}, "CI"); + add(new int[] {619}, "TN"); + add(new int[] {621}, "SY"); + add(new int[] {622}, "EG"); + add(new int[] {624}, "LY"); + add(new int[] {625}, "JO"); + add(new int[] {626}, "IR"); + add(new int[] {627}, "KW"); + add(new int[] {628}, "SA"); + add(new int[] {629}, "AE"); + add(new int[] {640,649}, "FI"); + add(new int[] {690,695}, "CN"); + add(new int[] {700,709}, "NO"); + add(new int[] {729}, "IL"); + add(new int[] {730,739}, "SE"); + add(new int[] {740}, "GT"); + add(new int[] {741}, "SV"); + add(new int[] {742}, "HN"); + add(new int[] {743}, "NI"); + add(new int[] {744}, "CR"); + add(new int[] {745}, "PA"); + add(new int[] {746}, "DO"); + add(new int[] {750}, "MX"); + add(new int[] {754,755}, "CA"); + add(new int[] {759}, "VE"); + add(new int[] {760,769}, "CH"); + add(new int[] {770}, "CO"); + add(new int[] {773}, "UY"); + add(new int[] {775}, "PE"); + add(new int[] {777}, "BO"); + add(new int[] {779}, "AR"); + add(new int[] {780}, "CL"); + add(new int[] {784}, "PY"); + add(new int[] {785}, "PE"); + add(new int[] {786}, "EC"); + add(new int[] {789,790}, "BR"); + add(new int[] {800,839}, "IT"); + add(new int[] {840,849}, "ES"); + add(new int[] {850}, "CU"); + add(new int[] {858}, "SK"); + add(new int[] {859}, "CZ"); + add(new int[] {860}, "YU"); + add(new int[] {865}, "MN"); + add(new int[] {867}, "KP"); + add(new int[] {868,869}, "TR"); + add(new int[] {870,879}, "NL"); + add(new int[] {880}, "KR"); + add(new int[] {885}, "TH"); + add(new int[] {888}, "SG"); + add(new int[] {890}, "IN"); + add(new int[] {893}, "VN"); + add(new int[] {896}, "PK"); + add(new int[] {899}, "ID"); + add(new int[] {900,919}, "AT"); + add(new int[] {930,939}, "AU"); + add(new int[] {940,949}, "AZ"); + add(new int[] {955}, "MY"); + add(new int[] {958}, "MO"); + } + +} diff --git a/src/com/google/zxing/oned/ITFReader.java b/src/com/google/zxing/oned/ITFReader.java new file mode 100644 index 0000000..7aef5ae --- /dev/null +++ b/src/com/google/zxing/oned/ITFReader.java @@ -0,0 +1,348 @@ +/* + * 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.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Hashtable; + +/** + *Implements decoding of the ITF format.
+ * + *"ITF" stands for Interleaved Two of Five. This Reader will scan ITF barcode with 6, 10 or 14 + * digits. The checksum is optional and is not applied by this Reader. The consumer of the decoded + * value will have to apply a checksum if required.
+ * + *http://en.wikipedia.org/wiki/Interleaved_2_of_5 + * is a great reference for Interleaved 2 of 5 information.
+ * + * @author kevin.osullivan@sita.aero, SITA Lab. + */ +public final class ITFReader extends OneDReader { + + 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.8f); + + 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 static final int[] DEFAULT_ALLOWED_LENGTHS = { 6, 10, 12, 14, 44 }; + + // Stores the actual narrow line width of the image being decoded. + private int narrowLineWidth = -1; + + /** + * 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 Result decodeRow(int rowNumber, BitArray row, Hashtable hints) throws FormatException, NotFoundException { + + // Find out where the Middle section (payload) starts & ends + int[] startRange = decodeStart(row); + int[] endRange = decodeEnd(row); + + StringBuffer result = new StringBuffer(20); + decodeMiddle(row, startRange[1], endRange[0], result); + String resultString = result.toString(); + + int[] allowedLengths = null; + if (hints != null) { + allowedLengths = (int[]) hints.get(DecodeHintType.ALLOWED_LENGTHS); + + } + if (allowedLengths == null) { + allowedLengths = DEFAULT_ALLOWED_LENGTHS; + } + + // To avoid false positives with 2D barcodes (and other patterns), make + // an assumption that the decoded string must be 6, 10 or 14 digits. + int length = resultString.length(); + boolean lengthOK = false; + for (int i = 0; i < allowedLengths.length; i++) { + if (length == allowedLengths[i]) { + lengthOK = true; + break; + } + + } + if (!lengthOK) { + throw FormatException.getFormatInstance(); + } + + return new Result( + resultString, + null, // no natural byte representation for these barcodes + new ResultPoint[] { new ResultPoint(startRange[1], (float) rowNumber), + new ResultPoint(endRange[0], (float) rowNumber)}, + BarcodeFormat.ITF); + } + + /** + * @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 NotFoundException if decoding could not complete successfully + */ + private static void decodeMiddle(BitArray row, int payloadStart, int payloadEnd, + StringBuffer resultString) throws NotFoundException { + + // 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]; + + while (payloadStart < payloadEnd) { + + // Get 10 runs of black/white. + recordPattern(row, payloadStart, counterDigitPair); + // Split them into each array + for (int k = 0; k < 5; k++) { + int twoK = k << 1; + counterBlack[k] = counterDigitPair[twoK]; + counterWhite[k] = counterDigitPair[twoK + 1]; + } + + int bestMatch = decodeDigit(counterBlack); + resultString.append((char) ('0' + bestMatch)); + bestMatch = decodeDigit(counterWhite); + resultString.append((char) ('0' + bestMatch)); + + 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 NotFoundException + */ + int[] decodeStart(BitArray row) throws NotFoundException { + int endStart = skipWhiteSpace(row); + int[] startPattern = findGuardPattern(row, endStart, START_PATTERN); + + // Determine the width of a narrow line in pixels. We can do this by + // getting the width of the start pattern and dividing by 4 because its + // made up of 4 narrow lines. + this.narrowLineWidth = (startPattern[1] - startPattern[0]) >> 2; + + validateQuietZone(row, startPattern[0]); + + return startPattern; + } + + /** + * The start & end patterns must be pre/post fixed by a quiet zone. This + * zone must be at least 10 times the width of a narrow line. Scan back until + * we either get to the start of the barcode or match the necessary number of + * quiet zone pixels. + * + * Note: Its assumed the row is reversed when using this method to find + * quiet zone after the end pattern. + * + * ref: http://www.barcode-1.net/i25code.html + * + * @param row bit array representing the scanned barcode. + * @param startPattern index into row of the start or end pattern. + * @throws NotFoundException if the quiet zone cannot be found, a ReaderException is thrown. + */ + private void validateQuietZone(BitArray row, int startPattern) throws NotFoundException { + + int quietCount = this.narrowLineWidth * 10; // expect to find this many pixels of quiet zone + + for (int i = startPattern - 1; quietCount > 0 && i >= 0; i--) { + if (row.get(i)) { + break; + } + quietCount--; + } + if (quietCount != 0) { + // Unable to find the necessary number of quiet zone pixels. + throw NotFoundException.getNotFoundInstance(); + } + } + + /** + * 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 NotFoundException Throws exception if no black lines are found in the row + */ + private static int skipWhiteSpace(BitArray row) throws NotFoundException { + int width = row.getSize(); + int endStart = 0; + while (endStart < width) { + if (row.get(endStart)) { + break; + } + endStart++; + } + if (endStart == width) { + throw NotFoundException.getNotFoundInstance(); + } + + return endStart; + } + + /** + * Identify where the end 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 NotFoundException + */ + + int[] decodeEnd(BitArray row) throws NotFoundException { + + // For convenience, reverse the row and then + // search from 'the start' for the end block + row.reverse(); + try { + int endStart = skipWhiteSpace(row); + int[] endPattern = findGuardPattern(row, endStart, END_PATTERN_REVERSED); + + // The start & end patterns must be pre/post fixed by a quiet zone. This + // zone must be at least 10 times the width of a narrow line. + // ref: http://www.barcode-1.net/i25code.html + validateQuietZone(row, endPattern[0]); + + // Now recalculate the indices of where the 'endblock' starts & stops to + // accommodate + // the reversed nature of the search + int temp = endPattern[0]; + endPattern[0] = row.getSize() - endPattern[1]; + endPattern[1] = row.getSize() - temp; + + return endPattern; + } finally { + // Put the row back the right way. + row.reverse(); + } + } + + /** + * @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 NotFoundException if pattern is not found + */ + private static int[] findGuardPattern(BitArray row, int rowOffset, int[] pattern) throws NotFoundException { + + // TODO: This is very similar to implementation in UPCEANReader. Consider if they can be + // merged to a single method. + 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) { + 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 NotFoundException.getNotFoundInstance(); + } + + /** + * Attempts to decode a sequence of ITF black/white lines into single + * digit. + * + * @param counters the counts of runs of observed black/white/black/... values + * @return The decoded digit + * @throws NotFoundException if digit cannot be decoded + */ + private static int decodeDigit(int[] counters) throws NotFoundException { + + 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 NotFoundException.getNotFoundInstance(); + } + } + +} diff --git a/src/com/google/zxing/oned/ITFWriter.java b/src/com/google/zxing/oned/ITFWriter.java new file mode 100644 index 0000000..16894a6 --- /dev/null +++ b/src/com/google/zxing/oned/ITFWriter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2010 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 java.util.Hashtable; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +/** + * This object renders a ITF code as a {@link BitMatrix}. + * + * @author erik.barbara@gmail.com (Erik Barbara) + */ +public final class ITFWriter extends UPCEANWriter { + + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Hashtable hints) throws WriterException { + if (format != BarcodeFormat.ITF) { + throw new IllegalArgumentException("Can only encode ITF, but got " + format); + } + + return super.encode(contents, format, width, height, hints); + } + + public byte[] encode(String contents) { + int length = contents.length(); + if (length > 80) { + throw new IllegalArgumentException( + "Requested contents should be less than 80 digits long, but got " + length); + } + byte[] result = new byte[9 + 9 * length]; + int[] start = {1, 1, 1, 1}; + int pos = appendPattern(result, 0, start, 1); + for (int i = 0; i < length; i += 2) { + int one = Character.digit(contents.charAt(i), 10); + int two = Character.digit(contents.charAt(i+1), 10); + int[] encoding = new int[18]; + for (int j = 0; j < 5; j++) { + encoding[(j << 1)] = ITFReader.PATTERNS[one][j]; + encoding[(j << 1) + 1] = ITFReader.PATTERNS[two][j]; + } + pos += appendPattern(result, pos, encoding, 1); + } + int[] end = {3, 1, 1}; + pos += appendPattern(result, pos, end, 1); + + return result; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/oned/MultiFormatOneDReader.java b/src/com/google/zxing/oned/MultiFormatOneDReader.java new file mode 100644 index 0000000..fa3de5d --- /dev/null +++ b/src/com/google/zxing/oned/MultiFormatOneDReader.java @@ -0,0 +1,109 @@ +/* + * 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.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.BitArray; +import com.google.zxing.oned.rss.RSS14Reader; +import com.google.zxing.oned.rss.expanded.RSSExpandedReader; + +import java.util.Hashtable; +import java.util.Vector; + +/** + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public final class MultiFormatOneDReader extends OneDReader { + + private final Vector readers; + + public MultiFormatOneDReader(Hashtable hints) { + Vector possibleFormats = hints == null ? null : + (Vector) hints.get(DecodeHintType.POSSIBLE_FORMATS); + boolean useCode39CheckDigit = hints != null && + hints.get(DecodeHintType.ASSUME_CODE_39_CHECK_DIGIT) != null; + readers = new Vector(); + if (possibleFormats != null) { + if (possibleFormats.contains(BarcodeFormat.EAN_13) || + possibleFormats.contains(BarcodeFormat.UPC_A) || + possibleFormats.contains(BarcodeFormat.EAN_8) || + possibleFormats.contains(BarcodeFormat.UPC_E)) { + readers.addElement(new MultiFormatUPCEANReader(hints)); + } + if (possibleFormats.contains(BarcodeFormat.CODE_39)) { + readers.addElement(new Code39Reader(useCode39CheckDigit)); + } + if (possibleFormats.contains(BarcodeFormat.CODE_93)) { + readers.addElement(new Code93Reader()); + } + if (possibleFormats.contains(BarcodeFormat.CODE_128)) { + readers.addElement(new Code128Reader()); + } + if (possibleFormats.contains(BarcodeFormat.ITF)) { + readers.addElement(new ITFReader()); + } + if (possibleFormats.contains(BarcodeFormat.CODABAR)) { + readers.addElement(new CodaBarReader()); + } + if (possibleFormats.contains(BarcodeFormat.RSS14)) { + readers.addElement(new RSS14Reader()); + } + if (possibleFormats.contains(BarcodeFormat.RSS_EXPANDED)){ + readers.addElement(new RSSExpandedReader()); + } + } + if (readers.isEmpty()) { + readers.addElement(new MultiFormatUPCEANReader(hints)); + readers.addElement(new Code39Reader()); + //readers.addElement(new CodaBarReader()); + readers.addElement(new Code93Reader()); + readers.addElement(new Code128Reader()); + readers.addElement(new ITFReader()); + readers.addElement(new RSS14Reader()); + readers.addElement(new RSSExpandedReader()); + } + } + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) throws NotFoundException { + int size = readers.size(); + for (int i = 0; i < size; i++) { + OneDReader reader = (OneDReader) readers.elementAt(i); + try { + return reader.decodeRow(rowNumber, row, hints); + } catch (ReaderException re) { + // continue + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + public void reset() { + int size = readers.size(); + for (int i = 0; i < size; i++) { + Reader reader = (Reader) readers.elementAt(i); + reader.reset(); + } + } + +} diff --git a/src/com/google/zxing/oned/MultiFormatUPCEANReader.java b/src/com/google/zxing/oned/MultiFormatUPCEANReader.java new file mode 100644 index 0000000..e561f59 --- /dev/null +++ b/src/com/google/zxing/oned/MultiFormatUPCEANReader.java @@ -0,0 +1,113 @@ +/* + * 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.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.BitArray; + +import java.util.Hashtable; +import java.util.Vector; + +/** + *A reader that can read all available UPC/EAN formats. If a caller wants to try to + * read all such formats, it is most efficient to use this implementation rather than invoke + * individual readers.
+ * + * @author Sean Owen + */ +public final class MultiFormatUPCEANReader extends OneDReader { + + private final Vector readers; + + public MultiFormatUPCEANReader(Hashtable hints) { + Vector possibleFormats = hints == null ? null : + (Vector) hints.get(DecodeHintType.POSSIBLE_FORMATS); + readers = new Vector(); + if (possibleFormats != null) { + if (possibleFormats.contains(BarcodeFormat.EAN_13)) { + readers.addElement(new EAN13Reader()); + } else if (possibleFormats.contains(BarcodeFormat.UPC_A)) { + readers.addElement(new UPCAReader()); + } + if (possibleFormats.contains(BarcodeFormat.EAN_8)) { + readers.addElement(new EAN8Reader()); + } + if (possibleFormats.contains(BarcodeFormat.UPC_E)) { + readers.addElement(new UPCEReader()); + } + } + if (readers.isEmpty()) { + readers.addElement(new EAN13Reader()); + // UPC-A is covered by EAN-13 + readers.addElement(new EAN8Reader()); + readers.addElement(new UPCEReader()); + } + } + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) throws NotFoundException { + // Compute this location once and reuse it on multiple implementations + int[] startGuardPattern = UPCEANReader.findStartGuardPattern(row); + int size = readers.size(); + for (int i = 0; i < size; i++) { + UPCEANReader reader = (UPCEANReader) readers.elementAt(i); + Result result; + try { + result = reader.decodeRow(rowNumber, row, startGuardPattern, hints); + } catch (ReaderException re) { + continue; + } + // Special case: a 12-digit code encoded in UPC-A is identical to a "0" + // followed by those 12 digits encoded as EAN-13. Each will recognize such a code, + // UPC-A as a 12-digit string and EAN-13 as a 13-digit string starting with "0". + // Individually these are correct and their readers will both read such a code + // and correctly call it EAN-13, or UPC-A, respectively. + // + // In this case, if we've been looking for both types, we'd like to call it + // a UPC-A code. But for efficiency we only run the EAN-13 decoder to also read + // UPC-A. So we special case it here, and convert an EAN-13 result to a UPC-A + // result if appropriate. + // + // But, don't return UPC-A if UPC-A was not a requested format! + boolean ean13MayBeUPCA = + BarcodeFormat.EAN_13.equals(result.getBarcodeFormat()) && + result.getText().charAt(0) == '0'; + Vector possibleFormats = hints == null ? null : (Vector) hints.get(DecodeHintType.POSSIBLE_FORMATS); + boolean canReturnUPCA = possibleFormats == null || possibleFormats.contains(BarcodeFormat.UPC_A); + + if (ean13MayBeUPCA && canReturnUPCA) { + return new Result(result.getText().substring(1), null, result.getResultPoints(), BarcodeFormat.UPC_A); + } + return result; + } + + throw NotFoundException.getNotFoundInstance(); + } + + public void reset() { + int size = readers.size(); + for (int i = 0; i < size; i++) { + Reader reader = (Reader) readers.elementAt(i); + reader.reset(); + } + } + +} diff --git a/src/com/google/zxing/oned/OneDReader.java b/src/com/google/zxing/oned/OneDReader.java new file mode 100644 index 0000000..102483c --- /dev/null +++ b/src/com/google/zxing/oned/OneDReader.java @@ -0,0 +1,297 @@ +/* + * 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.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Enumeration; +import java.util.Hashtable; + +/** + * Encapsulates functionality and implementation that is common to all families + * of one-dimensional barcodes. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public abstract class OneDReader implements Reader { + + protected static final int INTEGER_MATH_SHIFT = 8; + protected static final int PATTERN_MATCH_RESULT_SCALE_FACTOR = 1 << INTEGER_MATH_SHIFT; + + public Result decode(BinaryBitmap image) throws NotFoundException, FormatException { + return decode(image, null); + } + + // Note that we don't try rotation without the try harder flag, even if rotation was supported. + public Result decode(BinaryBitmap image, Hashtable hints) throws NotFoundException, FormatException { + try { + return doDecode(image, hints); + } catch (NotFoundException nfe) { + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + if (tryHarder && image.isRotateSupported()) { + BinaryBitmap rotatedImage = image.rotateCounterClockwise(); + Result result = doDecode(rotatedImage, hints); + // Record that we found it rotated 90 degrees CCW / 270 degrees CW + Hashtable metadata = result.getResultMetadata(); + int orientation = 270; + if (metadata != null && metadata.containsKey(ResultMetadataType.ORIENTATION)) { + // But if we found it reversed in doDecode(), add in that result here: + orientation = (orientation + + ((Integer) metadata.get(ResultMetadataType.ORIENTATION)).intValue()) % 360; + } + result.putMetadata(ResultMetadataType.ORIENTATION, new Integer(orientation)); + // Update result points + ResultPoint[] points = result.getResultPoints(); + int height = rotatedImage.getHeight(); + for (int i = 0; i < points.length; i++) { + points[i] = new ResultPoint(height - points[i].getY() - 1, points[i].getX()); + } + return result; + } else { + throw nfe; + } + } + } + + public void reset() { + // do nothing + } + + /** + * We're going to examine rows from the middle outward, searching alternately above and below the + * middle, and farther out each time. rowStep is the number of rows between each successive + * attempt above and below the middle. So we'd scan row middle, then middle - rowStep, then + * middle + rowStep, then middle - (2 * rowStep), etc. + * rowStep is bigger as the image is taller, but is always at least 1. We've somewhat arbitrarily + * decided that moving up and down by about 1/16 of the image is pretty good; we try more of the + * image if "trying harder". + * + * @param image The image to decode + * @param hints Any hints that were requested + * @return The contents of the decoded barcode + * @throws NotFoundException Any spontaneous errors which occur + */ + private Result doDecode(BinaryBitmap image, Hashtable hints) throws NotFoundException { + int width = image.getWidth(); + int height = image.getHeight(); + BitArray row = new BitArray(width); + + int middle = height >> 1; + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + int rowStep = Math.max(1, height >> (tryHarder ? 8 : 5)); + int maxLines; + if (tryHarder) { + maxLines = height; // Look at the whole image, not just the center + } else { + maxLines = 15; // 15 rows spaced 1/32 apart is roughly the middle half of the image + } + + for (int x = 0; x < maxLines; x++) { + + // Scanning from the middle out. Determine which row we're looking at next: + int rowStepsAboveOrBelow = (x + 1) >> 1; + boolean isAbove = (x & 0x01) == 0; // i.e. is x even? + int rowNumber = middle + rowStep * (isAbove ? rowStepsAboveOrBelow : -rowStepsAboveOrBelow); + if (rowNumber < 0 || rowNumber >= height) { + // Oops, if we run off the top or bottom, stop + break; + } + + // Estimate black point for this row and load it: + try { + row = image.getBlackRow(rowNumber, row); + } catch (NotFoundException nfe) { + continue; + } + + // While we have the image data in a BitArray, it's fairly cheap to reverse it in place to + // handle decoding upside down barcodes. + for (int attempt = 0; attempt < 2; attempt++) { + if (attempt == 1) { // trying again? + row.reverse(); // reverse the row and continue + // This means we will only ever draw result points *once* in the life of this method + // since we want to avoid drawing the wrong points after flipping the row, and, + // don't want to clutter with noise from every single row scan -- just the scans + // that start on the center line. + if (hints != null && hints.containsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)) { + Hashtable newHints = new Hashtable(); // Can't use clone() in J2ME + Enumeration hintEnum = hints.keys(); + while (hintEnum.hasMoreElements()) { + Object key = hintEnum.nextElement(); + if (!key.equals(DecodeHintType.NEED_RESULT_POINT_CALLBACK)) { + newHints.put(key, hints.get(key)); + } + } + hints = newHints; + } + } + try { + // Look for a barcode + Result result = decodeRow(rowNumber, row, hints); + // We found our barcode + if (attempt == 1) { + // But it was upside down, so note that + result.putMetadata(ResultMetadataType.ORIENTATION, new Integer(180)); + // And remember to flip the result points horizontally. + ResultPoint[] points = result.getResultPoints(); + points[0] = new ResultPoint(width - points[0].getX() - 1, points[0].getY()); + points[1] = new ResultPoint(width - points[1].getX() - 1, points[1].getY()); + } + return result; + } catch (ReaderException re) { + // continue -- just couldn't decode this row + } + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Records the size of successive runs of white and black pixels in a row, starting at a given point. + * The values are recorded in the given array, and the number of runs recorded is equal to the size + * of the array. If the row starts on a white pixel at the given start point, then the first count + * recorded is the run of white pixels starting from that point; likewise it is the count of a run + * of black pixels if the row begin on a black pixels at that point. + * + * @param row row to count from + * @param start offset into row to start at + * @param counters array into which to record counts + * @throws NotFoundException if counters cannot be filled entirely from row before running out + * of pixels + */ + protected static void recordPattern(BitArray row, int start, int[] counters) throws NotFoundException { + int numCounters = counters.length; + for (int i = 0; i < numCounters; i++) { + counters[i] = 0; + } + int end = row.getSize(); + if (start >= end) { + throw NotFoundException.getNotFoundInstance(); + } + boolean isWhite = !row.get(start); + int counterPosition = 0; + int i = start; + while (i < end) { + boolean pixel = row.get(i); + if (pixel ^ isWhite) { // that is, exactly one is true + counters[counterPosition]++; + } else { + counterPosition++; + if (counterPosition == numCounters) { + break; + } else { + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + i++; + } + // If we read fully the last section of pixels and filled up our counters -- or filled + // the last counter but ran off the side of the image, OK. Otherwise, a problem. + if (!(counterPosition == numCounters || (counterPosition == numCounters - 1 && i == end))) { + throw NotFoundException.getNotFoundInstance(); + } + } + + protected static void recordPatternInReverse(BitArray row, int start, int[] counters) + throws NotFoundException { + // This could be more efficient I guess + int numTransitionsLeft = counters.length; + boolean last = row.get(start); + while (start > 0 && numTransitionsLeft >= 0) { + if (row.get(--start) != last) { + numTransitionsLeft--; + last = !last; + } + } + if (numTransitionsLeft >= 0) { + throw NotFoundException.getNotFoundInstance(); + } + recordPattern(row, start + 1, counters); + } + + /** + * Determines how closely a set of observed counts of runs of black/white values matches a given + * target pattern. This is reported as the ratio of the total variance from the expected pattern + * proportions across all pattern elements, to the length of the pattern. + * + * @param counters observed counters + * @param pattern expected pattern + * @param maxIndividualVariance The most any counter can differ before we give up + * @return ratio of total variance between counters and pattern compared to total pattern size, + * where the ratio has been multiplied by 256. So, 0 means no variance (perfect match); 256 means + * the total variance between counters and patterns equals the pattern length, higher values mean + * even more variance + */ + protected static int patternMatchVariance(int[] counters, int[] pattern, int maxIndividualVariance) { + int numCounters = counters.length; + int total = 0; + int patternLength = 0; + for (int i = 0; i < numCounters; i++) { + total += counters[i]; + patternLength += pattern[i]; + } + if (total < patternLength) { + // If we don't even have one pixel per unit of bar width, assume this is too small + // to reliably match, so fail: + return Integer.MAX_VALUE; + } + // We're going to fake floating-point math in integers. We just need to use more bits. + // Scale up patternLength so that intermediate values below like scaledCounter will have + // more "significant digits" + int unitBarWidth = (total << INTEGER_MATH_SHIFT) / patternLength; + maxIndividualVariance = (maxIndividualVariance * unitBarWidth) >> INTEGER_MATH_SHIFT; + + int totalVariance = 0; + for (int x = 0; x < numCounters; x++) { + int counter = counters[x] << INTEGER_MATH_SHIFT; + int scaledPattern = pattern[x] * unitBarWidth; + int variance = counter > scaledPattern ? counter - scaledPattern : scaledPattern - counter; + if (variance > maxIndividualVariance) { + return Integer.MAX_VALUE; + } + totalVariance += variance; + } + return totalVariance / total; + } + + /** + *Attempts to decode a one-dimensional barcode format given a single row of + * an image.
+ * + * @param rowNumber row number from top of the row + * @param row the black/white pixel data of the row + * @param hints decode hints + * @return {@link Result} containing encoded string and start/end of barcode + * @throws NotFoundException if an error occurs or barcode cannot be found + */ + public abstract Result decodeRow(int rowNumber, BitArray row, Hashtable hints) + throws NotFoundException, ChecksumException, FormatException; + +} diff --git a/src/com/google/zxing/oned/UPCAReader.java b/src/com/google/zxing/oned/UPCAReader.java new file mode 100644 index 0000000..b90c1b8 --- /dev/null +++ b/src/com/google/zxing/oned/UPCAReader.java @@ -0,0 +1,75 @@ +/* + * 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.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.common.BitArray; + +import java.util.Hashtable; + +/** + *Implements decoding of the UPC-A format.
+ * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public final class UPCAReader extends UPCEANReader { + + private final UPCEANReader ean13Reader = new EAN13Reader(); + + public Result decodeRow(int rowNumber, BitArray row, int[] startGuardRange, Hashtable hints) + throws NotFoundException, FormatException, ChecksumException { + return maybeReturnResult(ean13Reader.decodeRow(rowNumber, row, startGuardRange, hints)); + } + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) + throws NotFoundException, FormatException, ChecksumException { + return maybeReturnResult(ean13Reader.decodeRow(rowNumber, row, hints)); + } + + public Result decode(BinaryBitmap image) throws NotFoundException, FormatException { + return maybeReturnResult(ean13Reader.decode(image)); + } + + public Result decode(BinaryBitmap image, Hashtable hints) throws NotFoundException, FormatException { + return maybeReturnResult(ean13Reader.decode(image, hints)); + } + + BarcodeFormat getBarcodeFormat() { + return BarcodeFormat.UPC_A; + } + + protected int decodeMiddle(BitArray row, int[] startRange, StringBuffer resultString) + throws NotFoundException { + return ean13Reader.decodeMiddle(row, startRange, resultString); + } + + private static Result maybeReturnResult(Result result) throws FormatException { + String text = result.getText(); + if (text.charAt(0) == '0') { + return new Result(text.substring(1), null, result.getResultPoints(), BarcodeFormat.UPC_A); + } else { + throw FormatException.getFormatInstance(); + } + } + +} diff --git a/src/com/google/zxing/oned/UPCEANExtensionSupport.java b/src/com/google/zxing/oned/UPCEANExtensionSupport.java new file mode 100644 index 0000000..dea116d --- /dev/null +++ b/src/com/google/zxing/oned/UPCEANExtensionSupport.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2010 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 java.util.Hashtable; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +final class UPCEANExtensionSupport { + + private static final int[] EXTENSION_START_PATTERN = {1,1,2}; + private static final int[] CHECK_DIGIT_ENCODINGS = { + 0x18, 0x14, 0x12, 0x11, 0x0C, 0x06, 0x03, 0x0A, 0x09, 0x05 + }; + + private final int[] decodeMiddleCounters = new int[4]; + private final StringBuffer decodeRowStringBuffer = new StringBuffer(); + + Result decodeRow(int rowNumber, BitArray row, int rowOffset) throws NotFoundException { + + int[] extensionStartRange = UPCEANReader.findGuardPattern(row, rowOffset, false, EXTENSION_START_PATTERN); + + StringBuffer result = decodeRowStringBuffer; + result.setLength(0); + int end = decodeMiddle(row, extensionStartRange, result); + + String resultString = result.toString(); + Hashtable extensionData = parseExtensionString(resultString); + + Result extensionResult = + new Result(resultString, + null, + new ResultPoint[] { + new ResultPoint((extensionStartRange[0] + extensionStartRange[1]) / 2.0f, (float) rowNumber), + new ResultPoint((float) end, (float) rowNumber), + }, + BarcodeFormat.UPC_EAN_EXTENSION); + if (extensionData != null) { + extensionResult.putAllMetadata(extensionData); + } + return extensionResult; + } + + int decodeMiddle(BitArray row, int[] startRange, StringBuffer resultString) throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + int lgPatternFound = 0; + + for (int x = 0; x < 5 && rowOffset < end; x++) { + int bestMatch = UPCEANReader.decodeDigit(row, counters, rowOffset, UPCEANReader.L_AND_G_PATTERNS); + resultString.append((char) ('0' + bestMatch % 10)); + for (int i = 0; i < counters.length; i++) { + rowOffset += counters[i]; + } + if (bestMatch >= 10) { + lgPatternFound |= 1 << (4 - x); + } + if (x != 4) { + // Read off separator if not last + while (rowOffset < end && !row.get(rowOffset)) { + rowOffset++; + } + while (rowOffset < end && row.get(rowOffset)) { + rowOffset++; + } + } + } + + if (resultString.length() != 5) { + throw NotFoundException.getNotFoundInstance(); + } + + int checkDigit = determineCheckDigit(lgPatternFound); + if (extensionChecksum(resultString.toString()) != checkDigit) { + throw NotFoundException.getNotFoundInstance(); + } + + return rowOffset; + } + + private static int extensionChecksum(String s) { + int length = s.length(); + int sum = 0; + for (int i = length - 2; i >= 0; i -= 2) { + sum += (int) s.charAt(i) - (int) '0'; + } + sum *= 3; + for (int i = length - 1; i >= 0; i -= 2) { + sum += (int) s.charAt(i) - (int) '0'; + } + sum *= 3; + return sum % 10; + } + + private static int determineCheckDigit(int lgPatternFound) + throws NotFoundException { + for (int d = 0; d < 10; d++) { + if (lgPatternFound == CHECK_DIGIT_ENCODINGS[d]) { + return d; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * @param raw raw content of extension + * @return formatted interpretation of raw content as a {@link Hashtable} mapping + * one {@link ResultMetadataType} to appropriate value, ornull
if not known
+ */
+ private static Hashtable parseExtensionString(String raw) {
+ ResultMetadataType type;
+ Object value;
+ switch (raw.length()) {
+ case 2:
+ type = ResultMetadataType.ISSUE_NUMBER;
+ value = parseExtension2String(raw);
+ break;
+ case 5:
+ type = ResultMetadataType.SUGGESTED_PRICE;
+ value = parseExtension5String(raw);
+ break;
+ default:
+ return null;
+ }
+ if (value == null) {
+ return null;
+ }
+ Hashtable result = new Hashtable(1);
+ result.put(type, value);
+ return result;
+ }
+
+ private static Integer parseExtension2String(String raw) {
+ return Integer.valueOf(raw);
+ }
+
+ private static String parseExtension5String(String raw) {
+ String currency = null;
+ switch (raw.charAt(0)) {
+ case '0':
+ currency = "£";
+ break;
+ case '5':
+ currency = "$";
+ break;
+ case '9':
+ if ("99991".equals(raw)) {
+ return "0.00";
+ } else if ("99990".equals(raw)) {
+ return "Used";
+ }
+ break;
+ default:
+ currency = "";
+ break;
+ }
+ int rawAmount = Integer.parseInt(raw.substring(1));
+ return currency + (rawAmount / 100) + '.' + (rawAmount % 100);
+ }
+
+}
diff --git a/src/com/google/zxing/oned/UPCEANReader.java b/src/com/google/zxing/oned/UPCEANReader.java
new file mode 100644
index 0000000..6de318e
--- /dev/null
+++ b/src/com/google/zxing/oned/UPCEANReader.java
@@ -0,0 +1,355 @@
+/*
+ * 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.ChecksumException;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.FormatException;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.ReaderException;
+import com.google.zxing.Result;
+import com.google.zxing.ResultMetadataType;
+import com.google.zxing.ResultPoint;
+import com.google.zxing.ResultPointCallback;
+import com.google.zxing.common.BitArray;
+
+import java.util.Hashtable;
+
+/**
+ * Encapsulates functionality and implementation that is common to UPC and EAN families + * of one-dimensional barcodes.
+ * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + * @author alasdair@google.com (Alasdair Mackintosh) + */ +public abstract class UPCEANReader extends OneDReader { + + // These two values are critical for determining how permissive the decoding will be. + // We've arrived at these values through a lot of trial and error. Setting them any higher + // lets false positives creep in quickly. + 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); + + /** + * Start/end guard pattern. + */ + static final int[] START_END_PATTERN = {1, 1, 1,}; + + /** + * Pattern marking the middle of a UPC/EAN pattern, separating the two halves. + */ + static final int[] MIDDLE_PATTERN = {1, 1, 1, 1, 1}; + + /** + * "Odd", or "L" patterns used to encode UPC/EAN digits. + */ + static final int[][] L_PATTERNS = { + {3, 2, 1, 1}, // 0 + {2, 2, 2, 1}, // 1 + {2, 1, 2, 2}, // 2 + {1, 4, 1, 1}, // 3 + {1, 1, 3, 2}, // 4 + {1, 2, 3, 1}, // 5 + {1, 1, 1, 4}, // 6 + {1, 3, 1, 2}, // 7 + {1, 2, 1, 3}, // 8 + {3, 1, 1, 2} // 9 + }; + + /** + * As above but also including the "even", or "G" patterns used to encode UPC/EAN digits. + */ + static final int[][] L_AND_G_PATTERNS; + + static { + L_AND_G_PATTERNS = new int[20][]; + for (int i = 0; i < 10; i++) { + L_AND_G_PATTERNS[i] = L_PATTERNS[i]; + } + for (int i = 10; i < 20; i++) { + int[] widths = L_PATTERNS[i - 10]; + int[] reversedWidths = new int[widths.length]; + for (int j = 0; j < widths.length; j++) { + reversedWidths[j] = widths[widths.length - j - 1]; + } + L_AND_G_PATTERNS[i] = reversedWidths; + } + } + + private final StringBuffer decodeRowStringBuffer; + private final UPCEANExtensionSupport extensionReader; + private final EANManufacturerOrgSupport eanManSupport; + + protected UPCEANReader() { + decodeRowStringBuffer = new StringBuffer(20); + extensionReader = new UPCEANExtensionSupport(); + eanManSupport = new EANManufacturerOrgSupport(); + } + + static int[] findStartGuardPattern(BitArray row) throws NotFoundException { + boolean foundStart = false; + int[] startRange = null; + int nextStart = 0; + while (!foundStart) { + startRange = findGuardPattern(row, nextStart, false, START_END_PATTERN); + int start = startRange[0]; + nextStart = startRange[1]; + // Make sure there is a quiet zone at least as big as the start pattern before the barcode. + // If this check would run off the left edge of the image, do not accept this barcode, + // as it is very likely to be a false positive. + int quietStart = start - (nextStart - start); + if (quietStart >= 0) { + foundStart = row.isRange(quietStart, start, false); + } + } + return startRange; + } + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) + throws NotFoundException, ChecksumException, FormatException { + return decodeRow(rowNumber, row, findStartGuardPattern(row), hints); + } + + /** + *Like {@link #decodeRow(int, BitArray, java.util.Hashtable)}, but + * allows caller to inform method about where the UPC/EAN start pattern is + * found. This allows this to be computed once and reused across many implementations.
+ */ + public Result decodeRow(int rowNumber, BitArray row, int[] startGuardRange, Hashtable hints) + throws NotFoundException, ChecksumException, FormatException { + + ResultPointCallback resultPointCallback = hints == null ? null : + (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(new ResultPoint( + (startGuardRange[0] + startGuardRange[1]) / 2.0f, rowNumber + )); + } + + StringBuffer result = decodeRowStringBuffer; + result.setLength(0); + int endStart = decodeMiddle(row, startGuardRange, result); + + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(new ResultPoint( + endStart, rowNumber + )); + } + + int[] endRange = decodeEnd(row, endStart); + + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(new ResultPoint( + (endRange[0] + endRange[1]) / 2.0f, rowNumber + )); + } + + + // Make sure there is a quiet zone at least as big as the end pattern after the barcode. The + // spec might want more whitespace, but in practice this is the maximum we can count on. + int end = endRange[1]; + int quietEnd = end + (end - endRange[0]); + if (quietEnd >= row.getSize() || !row.isRange(end, quietEnd, false)) { + throw NotFoundException.getNotFoundInstance(); + } + + String resultString = result.toString(); + if (!checkChecksum(resultString)) { + throw ChecksumException.getChecksumInstance(); + } + + float left = (float) (startGuardRange[1] + startGuardRange[0]) / 2.0f; + float right = (float) (endRange[1] + endRange[0]) / 2.0f; + BarcodeFormat format = getBarcodeFormat(); + Result decodeResult = new Result(resultString, + null, // no natural byte representation for these barcodes + new ResultPoint[]{ + new ResultPoint(left, (float) rowNumber), + new ResultPoint(right, (float) rowNumber)}, + format); + + try { + Result extensionResult = extensionReader.decodeRow(rowNumber, row, endRange[1]); + decodeResult.putAllMetadata(extensionResult.getResultMetadata()); + decodeResult.addResultPoints(extensionResult.getResultPoints()); + } catch (ReaderException re) { + // continue + } + + if (BarcodeFormat.EAN_13.equals(format) || BarcodeFormat.UPC_A.equals(format)) { + String countryID = eanManSupport.lookupCountryIdentifier(resultString); + if (countryID != null) { + decodeResult.putMetadata(ResultMetadataType.POSSIBLE_COUNTRY, countryID); + } + } + + return decodeResult; + } + + /** + * @return {@link #checkStandardUPCEANChecksum(String)} + */ + boolean checkChecksum(String s) throws ChecksumException, FormatException { + return checkStandardUPCEANChecksum(s); + } + + /** + * Computes the UPC/EAN checksum on a string of digits, and reports + * whether the checksum is correct or not. + * + * @param s string of digits to check + * @return true iff string of digits passes the UPC/EAN checksum algorithm + * @throws FormatException if the string does not contain only digits + */ + private static boolean checkStandardUPCEANChecksum(String s) throws FormatException { + int length = s.length(); + if (length == 0) { + return false; + } + + int sum = 0; + for (int i = length - 2; i >= 0; i -= 2) { + int digit = (int) s.charAt(i) - (int) '0'; + if (digit < 0 || digit > 9) { + throw FormatException.getFormatInstance(); + } + sum += digit; + } + sum *= 3; + for (int i = length - 1; i >= 0; i -= 2) { + int digit = (int) s.charAt(i) - (int) '0'; + if (digit < 0 || digit > 9) { + throw FormatException.getFormatInstance(); + } + sum += digit; + } + return sum % 10 == 0; + } + + int[] decodeEnd(BitArray row, int endStart) throws NotFoundException { + return findGuardPattern(row, endStart, false, START_END_PATTERN); + } + + /** + * @param row row of black/white values to search + * @param rowOffset position to start search + * @param whiteFirst if true, indicates that the pattern specifies white/black/white/... + * pixel counts, otherwise, it is interpreted as black/white/black/... + * @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 NotFoundException if pattern is not found + */ + static int[] findGuardPattern(BitArray row, int rowOffset, boolean whiteFirst, int[] pattern) + throws NotFoundException { + int patternLength = pattern.length; + int[] counters = new int[patternLength]; + int width = row.getSize(); + boolean isWhite = false; + while (rowOffset < width) { + isWhite = !row.get(rowOffset); + if (whiteFirst == isWhite) { + break; + } + rowOffset++; + } + + int counterPosition = 0; + int patternStart = rowOffset; + for (int x = rowOffset; x < width; x++) { + boolean pixel = row.get(x); + if (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 NotFoundException.getNotFoundInstance(); + } + + /** + * Attempts to decode a single UPC/EAN-encoded digit. + * + * @param row row of black/white values to decode + * @param counters the counts of runs of observed black/white/black/... values + * @param rowOffset horizontal offset to start decoding from + * @param patterns the set of patterns to use to decode -- sometimes different encodings + * for the digits 0-9 are used, and this indicates the encodings for 0 to 9 that should + * be used + * @return horizontal offset of first pixel beyond the decoded digit + * @throws NotFoundException if digit cannot be decoded + */ + static int decodeDigit(BitArray row, int[] counters, int rowOffset, int[][] patterns) + throws NotFoundException { + recordPattern(row, rowOffset, counters); + 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 NotFoundException.getNotFoundInstance(); + } + } + + /** + * Get the format of this decoder. + * + * @return The 1D format. + */ + abstract BarcodeFormat getBarcodeFormat(); + + /** + * Subclasses override this to decode the portion of a barcode between the start + * and end guard patterns. + * + * @param row row of black/white values to search + * @param startRange start/end offset of start guard pattern + * @param resultString {@link StringBuffer} to append decoded chars to + * @return horizontal offset of first pixel after the "middle" that was decoded + * @throws NotFoundException if decoding could not complete successfully + */ + protected abstract int decodeMiddle(BitArray row, int[] startRange, StringBuffer resultString) + throws NotFoundException; + +} diff --git a/src/com/google/zxing/oned/UPCEANWriter.java b/src/com/google/zxing/oned/UPCEANWriter.java new file mode 100644 index 0000000..68fa4a0 --- /dev/null +++ b/src/com/google/zxing/oned/UPCEANWriter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2009 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.Writer; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Hashtable; + +/** + *Encapsulates functionality and implementation that is common to UPC and EAN families + * of one-dimensional barcodes.
+ * + * @author aripollak@gmail.com (Ari Pollak) + */ +public abstract class UPCEANWriter implements Writer { + + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) + throws WriterException { + return encode(contents, format, width, height, null); + } + + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height, + Hashtable hints) throws WriterException { + if (contents == null || contents.length() == 0) { + throw new IllegalArgumentException("Found empty contents"); + } + + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Requested dimensions are too small: " + + width + 'x' + height); + } + + byte[] code = encode(contents); + return renderResult(code, width, height); + } + + /** @return a byte array of horizontal pixels (0 = white, 1 = black) */ + private static BitMatrix renderResult(byte[] code, int width, int height) { + int inputWidth = code.length; + // Add quiet zone on both sides + int fullWidth = inputWidth + (UPCEANReader.START_END_PATTERN.length << 1); + int outputWidth = Math.max(width, fullWidth); + int outputHeight = Math.max(1, height); + + int multiple = outputWidth / fullWidth; + int leftPadding = (outputWidth - (inputWidth * multiple)) / 2; + + BitMatrix output = new BitMatrix(outputWidth, outputHeight); + for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) { + if (code[inputX] == 1) { + output.setRegion(outputX, 0, multiple, outputHeight); + } + } + return output; + } + + + /** + * Appends the given pattern to the target array starting at pos. + * + * @param startColor + * starting color - 0 for white, 1 for black + * @return the number of elements added to target. + */ + protected static int appendPattern(byte[] target, int pos, int[] pattern, int startColor) { + if (startColor != 0 && startColor != 1) { + throw new IllegalArgumentException( + "startColor must be either 0 or 1, but got: " + startColor); + } + + byte color = (byte) startColor; + int numAdded = 0; + for (int i = 0; i < pattern.length; i++) { + for (int j = 0; j < pattern[i]; j++) { + target[pos] = color; + pos += 1; + numAdded += 1; + } + color ^= 1; // flip color after each segment + } + return numAdded; + } + + /** @return a byte array of horizontal pixels (0 = white, 1 = black) */ + public abstract byte[] encode(String contents); + +} diff --git a/src/com/google/zxing/oned/UPCEReader.java b/src/com/google/zxing/oned/UPCEReader.java new file mode 100644 index 0000000..ef74b97 --- /dev/null +++ b/src/com/google/zxing/oned/UPCEReader.java @@ -0,0 +1,153 @@ +/* + * 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.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + *Implements decoding of the UPC-E format.
+ * + *This is a great reference for + * UPC-E information.
+ * + * @author Sean Owen + */ +public final class UPCEReader extends UPCEANReader { + + /** + * The pattern that marks the middle, and end, of a UPC-E pattern. + * There is no "second half" to a UPC-E barcode. + */ + private static final int[] MIDDLE_END_PATTERN = {1, 1, 1, 1, 1, 1}; + + /** + * See {@link #L_AND_G_PATTERNS}; these values similarly represent patterns of + * even-odd parity encodings of digits that imply both the number system (0 or 1) + * used, and the check digit. + */ + private static final int[][] NUMSYS_AND_CHECK_DIGIT_PATTERNS = { + {0x38, 0x34, 0x32, 0x31, 0x2C, 0x26, 0x23, 0x2A, 0x29, 0x25}, + {0x07, 0x0B, 0x0D, 0x0E, 0x13, 0x19, 0x1C, 0x15, 0x16, 0x1A} + }; + + private final int[] decodeMiddleCounters; + + public UPCEReader() { + decodeMiddleCounters = new int[4]; + } + + protected int decodeMiddle(BitArray row, int[] startRange, StringBuffer result) + throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + int lgPatternFound = 0; + + for (int x = 0; x < 6 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_AND_G_PATTERNS); + result.append((char) ('0' + bestMatch % 10)); + for (int i = 0; i < counters.length; i++) { + rowOffset += counters[i]; + } + if (bestMatch >= 10) { + lgPatternFound |= 1 << (5 - x); + } + } + + determineNumSysAndCheckDigit(result, lgPatternFound); + + return rowOffset; + } + + protected int[] decodeEnd(BitArray row, int endStart) throws NotFoundException { + return findGuardPattern(row, endStart, true, MIDDLE_END_PATTERN); + } + + protected boolean checkChecksum(String s) throws FormatException, ChecksumException { + return super.checkChecksum(convertUPCEtoUPCA(s)); + } + + private static void determineNumSysAndCheckDigit(StringBuffer resultString, int lgPatternFound) + throws NotFoundException { + + for (int numSys = 0; numSys <= 1; numSys++) { + for (int d = 0; d < 10; d++) { + if (lgPatternFound == NUMSYS_AND_CHECK_DIGIT_PATTERNS[numSys][d]) { + resultString.insert(0, (char) ('0' + numSys)); + resultString.append((char) ('0' + d)); + return; + } + } + } + throw NotFoundException.getNotFoundInstance(); + } + + BarcodeFormat getBarcodeFormat() { + return BarcodeFormat.UPC_E; + } + + /** + * Expands a UPC-E value back into its full, equivalent UPC-A code value. + * + * @param upce UPC-E code as string of digits + * @return equivalent UPC-A code as string of digits + */ + public static String convertUPCEtoUPCA(String upce) { + char[] upceChars = new char[6]; + upce.getChars(1, 7, upceChars, 0); + StringBuffer result = new StringBuffer(12); + result.append(upce.charAt(0)); + char lastChar = upceChars[5]; + switch (lastChar) { + case '0': + case '1': + case '2': + result.append(upceChars, 0, 2); + result.append(lastChar); + result.append("0000"); + result.append(upceChars, 2, 3); + break; + case '3': + result.append(upceChars, 0, 3); + result.append("00000"); + result.append(upceChars, 3, 2); + break; + case '4': + result.append(upceChars, 0, 4); + result.append("00000"); + result.append(upceChars[4]); + break; + default: + result.append(upceChars, 0, 5); + result.append("0000"); + result.append(lastChar); + break; + } + result.append(upce.charAt(7)); + return result.toString(); + } + +} diff --git a/src/com/google/zxing/oned/rss/AbstractRSSReader.java b/src/com/google/zxing/oned/rss/AbstractRSSReader.java new file mode 100644 index 0000000..fce9e83 --- /dev/null +++ b/src/com/google/zxing/oned/rss/AbstractRSSReader.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2010 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.rss; + +import com.google.zxing.NotFoundException; +import com.google.zxing.oned.OneDReader; + +public abstract class AbstractRSSReader extends OneDReader { + + private static final int MAX_AVG_VARIANCE = (int) (PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.2f); + private static final int MAX_INDIVIDUAL_VARIANCE = (int) (PATTERN_MATCH_RESULT_SCALE_FACTOR * 0.4f); + + private static final float MIN_FINDER_PATTERN_RATIO = 9.5f / 12.0f; + private static final float MAX_FINDER_PATTERN_RATIO = 12.5f / 14.0f; + + protected final int[] decodeFinderCounters; + protected final int[] dataCharacterCounters; + protected final float[] oddRoundingErrors; + protected final float[] evenRoundingErrors; + protected final int[] oddCounts; + protected final int[] evenCounts; + + protected AbstractRSSReader(){ + decodeFinderCounters = new int[4]; + dataCharacterCounters = new int[8]; + oddRoundingErrors = new float[4]; + evenRoundingErrors = new float[4]; + oddCounts = new int[dataCharacterCounters.length / 2]; + evenCounts = new int[dataCharacterCounters.length / 2]; + } + + + protected static int parseFinderValue(int[] counters, int [][] finderPatterns) throws NotFoundException { + for (int value = 0; value < finderPatterns.length; value++) { + if (patternMatchVariance(counters, finderPatterns[value], MAX_INDIVIDUAL_VARIANCE) < + MAX_AVG_VARIANCE) { + return value; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + protected static int count(int[] array) { + int count = 0; + for (int i = 0; i < array.length; i++) { + count += array[i]; + } + return count; + } + + protected static void increment(int[] array, float[] errors) { + int index = 0; + float biggestError = errors[0]; + for (int i = 1; i < array.length; i++) { + if (errors[i] > biggestError) { + biggestError = errors[i]; + index = i; + } + } + array[index]++; + } + + protected static void decrement(int[] array, float[] errors) { + int index = 0; + float biggestError = errors[0]; + for (int i = 1; i < array.length; i++) { + if (errors[i] < biggestError) { + biggestError = errors[i]; + index = i; + } + } + array[index]--; + } + + protected static boolean isFinderPattern(int[] counters) { + int firstTwoSum = counters[0] + counters[1]; + int sum = firstTwoSum + counters[2] + counters[3]; + float ratio = (float) firstTwoSum / (float) sum; + if (ratio >= MIN_FINDER_PATTERN_RATIO && ratio <= MAX_FINDER_PATTERN_RATIO) { + // passes ratio test in spec, but see if the counts are unreasonable + int minCounter = Integer.MAX_VALUE; + int maxCounter = Integer.MIN_VALUE; + for (int i = 0; i < counters.length; i++) { + int counter = counters[i]; + if (counter > maxCounter) { + maxCounter = counter; + } + if (counter < minCounter) { + minCounter = counter; + } + } + return maxCounter < 10 * minCounter; + } + return false; + } +} diff --git a/src/com/google/zxing/oned/rss/DataCharacter.java b/src/com/google/zxing/oned/rss/DataCharacter.java new file mode 100644 index 0000000..9b5e311 --- /dev/null +++ b/src/com/google/zxing/oned/rss/DataCharacter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2009 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.rss; + +public class DataCharacter { + + private final int value; + private final int checksumPortion; + + public DataCharacter(int value, int checksumPortion) { + this.value = value; + this.checksumPortion = checksumPortion; + } + + public int getValue() { + return value; + } + + public int getChecksumPortion() { + return checksumPortion; + } + +} diff --git a/src/com/google/zxing/oned/rss/FinderPattern.java b/src/com/google/zxing/oned/rss/FinderPattern.java new file mode 100644 index 0000000..afc1a13 --- /dev/null +++ b/src/com/google/zxing/oned/rss/FinderPattern.java @@ -0,0 +1,48 @@ +/* + * Copyright 2009 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.rss; + +import com.google.zxing.ResultPoint; + +public final class FinderPattern { + + private final int value; + private final int[] startEnd; + private final ResultPoint[] resultPoints; + + public FinderPattern(int value, int[] startEnd, int start, int end, int rowNumber) { + this.value = value; + this.startEnd = startEnd; + this.resultPoints = new ResultPoint[] { + new ResultPoint((float) start, (float) rowNumber), + new ResultPoint((float) end, (float) rowNumber), + }; + } + + public int getValue() { + return value; + } + + public int[] getStartEnd() { + return startEnd; + } + + public ResultPoint[] getResultPoints() { + return resultPoints; + } + +} diff --git a/src/com/google/zxing/oned/rss/Pair.java b/src/com/google/zxing/oned/rss/Pair.java new file mode 100644 index 0000000..e2371d2 --- /dev/null +++ b/src/com/google/zxing/oned/rss/Pair.java @@ -0,0 +1,41 @@ +/* + * Copyright 2009 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.rss; + +final class Pair extends DataCharacter { + + private final FinderPattern finderPattern; + private int count; + + Pair(int value, int checksumPortion, FinderPattern finderPattern) { + super(value, checksumPortion); + this.finderPattern = finderPattern; + } + + FinderPattern getFinderPattern() { + return finderPattern; + } + + int getCount() { + return count; + } + + void incrementCount() { + count++; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/oned/rss/RSS14Reader.java b/src/com/google/zxing/oned/rss/RSS14Reader.java new file mode 100644 index 0000000..1c99ac1 --- /dev/null +++ b/src/com/google/zxing/oned/rss/RSS14Reader.java @@ -0,0 +1,494 @@ +/* + * Copyright 2009 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.rss; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitArray; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +/** + * Decodes RSS-14, including truncated and stacked variants. See ISO/IEC 24724:2006. + */ +public final class RSS14Reader extends AbstractRSSReader { + + private static final int[] OUTSIDE_EVEN_TOTAL_SUBSET = {1,10,34,70,126}; + private static final int[] INSIDE_ODD_TOTAL_SUBSET = {4,20,48,81}; + private static final int[] OUTSIDE_GSUM = {0,161,961,2015,2715}; + private static final int[] INSIDE_GSUM = {0,336,1036,1516}; + private static final int[] OUTSIDE_ODD_WIDEST = {8,6,4,3,1}; + private static final int[] INSIDE_ODD_WIDEST = {2,4,6,8}; + + private static final int[][] FINDER_PATTERNS = { + {3,8,2,1}, + {3,5,5,1}, + {3,3,7,1}, + {3,1,9,1}, + {2,7,4,1}, + {2,5,6,1}, + {2,3,8,1}, + {1,5,7,1}, + {1,3,9,1}, + }; + + private final Vector possibleLeftPairs; + private final Vector possibleRightPairs; + + public RSS14Reader() { + possibleLeftPairs = new Vector(); + possibleRightPairs = new Vector(); + } + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) throws NotFoundException { + Pair leftPair = decodePair(row, false, rowNumber, hints); + addOrTally(possibleLeftPairs, leftPair); + row.reverse(); + Pair rightPair = decodePair(row, true, rowNumber, hints); + addOrTally(possibleRightPairs, rightPair); + row.reverse(); + int numLeftPairs = possibleLeftPairs.size(); + int numRightPairs = possibleRightPairs.size(); + for (int l = 0; l < numLeftPairs; l++) { + Pair left = (Pair) possibleLeftPairs.elementAt(l); + if (left.getCount() > 1) { + for (int r = 0; r < numRightPairs; r++) { + Pair right = (Pair) possibleRightPairs.elementAt(r); + if (right.getCount() > 1) { + if (checkChecksum(left, right)) { + return constructResult(left, right); + } + } + } + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static void addOrTally(Vector possiblePairs, Pair pair) { + if (pair == null) { + return; + } + Enumeration e = possiblePairs.elements(); + boolean found = false; + while (e.hasMoreElements()) { + Pair other = (Pair) e.nextElement(); + if (other.getValue() == pair.getValue()) { + other.incrementCount(); + found = true; + break; + } + } + if (!found) { + possiblePairs.addElement(pair); + } + } + + public void reset() { + possibleLeftPairs.setSize(0); + possibleRightPairs.setSize(0); + } + + private static Result constructResult(Pair leftPair, Pair rightPair) { + long symbolValue = 4537077L * leftPair.getValue() + rightPair.getValue(); + String text = String.valueOf(symbolValue); + + StringBuffer buffer = new StringBuffer(14); + for (int i = 13 - text.length(); i > 0; i--) { + buffer.append('0'); + } + buffer.append(text); + + int checkDigit = 0; + for (int i = 0; i < 13; i++) { + int digit = buffer.charAt(i) - '0'; + checkDigit += (((i & 0x01) == 0) ? 3 * digit : digit); + } + checkDigit = 10 - (checkDigit % 10); + if (checkDigit == 10) { + checkDigit = 0; + } + buffer.append(checkDigit); + + ResultPoint[] leftPoints = leftPair.getFinderPattern().getResultPoints(); + ResultPoint[] rightPoints = rightPair.getFinderPattern().getResultPoints(); + return new Result( + String.valueOf(buffer.toString()), + null, + new ResultPoint[] { leftPoints[0], leftPoints[1], rightPoints[0], rightPoints[1], }, + BarcodeFormat.RSS14); + } + + private static boolean checkChecksum(Pair leftPair, Pair rightPair) { + int leftFPValue = leftPair.getFinderPattern().getValue(); + int rightFPValue = rightPair.getFinderPattern().getValue(); + if ((leftFPValue == 0 && rightFPValue == 8) || + (leftFPValue == 8 && rightFPValue == 0)) { + } + int checkValue = (leftPair.getChecksumPortion() + 16 * rightPair.getChecksumPortion()) % 79; + int targetCheckValue = + 9 * leftPair.getFinderPattern().getValue() + rightPair.getFinderPattern().getValue(); + if (targetCheckValue > 72) { + targetCheckValue--; + } + if (targetCheckValue > 8) { + targetCheckValue--; + } + return checkValue == targetCheckValue; + } + + private Pair decodePair(BitArray row, boolean right, int rowNumber, Hashtable hints) { + try { + int[] startEnd = findFinderPattern(row, 0, right); + FinderPattern pattern = parseFoundFinderPattern(row, rowNumber, right, startEnd); + + ResultPointCallback resultPointCallback = hints == null ? null : + (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + + if (resultPointCallback != null) { + float center = (startEnd[0] + startEnd[1]) / 2.0f; + if (right) { + // row is actually reversed + center = row.getSize() - 1 - center; + } + resultPointCallback.foundPossibleResultPoint(new ResultPoint(center, rowNumber)); + } + + DataCharacter outside = decodeDataCharacter(row, pattern, true); + DataCharacter inside = decodeDataCharacter(row, pattern, false); + return new Pair(1597 * outside.getValue() + inside.getValue(), + outside.getChecksumPortion() + 4 * inside.getChecksumPortion(), + pattern); + } catch (NotFoundException re) { + return null; + } + } + + private DataCharacter decodeDataCharacter(BitArray row, FinderPattern pattern, boolean outsideChar) + throws NotFoundException { + + int[] counters = dataCharacterCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + counters[4] = 0; + counters[5] = 0; + counters[6] = 0; + counters[7] = 0; + + if (outsideChar) { + recordPatternInReverse(row, pattern.getStartEnd()[0], counters); + } else { + recordPattern(row, pattern.getStartEnd()[1] + 1, counters); + // reverse it + for (int i = 0, j = counters.length - 1; i < j; i++, j--) { + int temp = counters[i]; + counters[i] = counters[j]; + counters[j] = temp; + } + } + + int numModules = outsideChar ? 16 : 15; + float elementWidth = (float) count(counters) / (float) numModules; + + int[] oddCounts = this.oddCounts; + int[] evenCounts = this.evenCounts; + float[] oddRoundingErrors = this.oddRoundingErrors; + float[] evenRoundingErrors = this.evenRoundingErrors; + + for (int i = 0; i < counters.length; i++) { + float value = (float) counters[i] / elementWidth; + int count = (int) (value + 0.5f); // Round + if (count < 1) { + count = 1; + } else if (count > 8) { + count = 8; + } + int offset = i >> 1; + if ((i & 0x01) == 0) { + oddCounts[offset] = count; + oddRoundingErrors[offset] = value - count; + } else { + evenCounts[offset] = count; + evenRoundingErrors[offset] = value - count; + } + } + + adjustOddEvenCounts(outsideChar, numModules); + + int oddSum = 0; + int oddChecksumPortion = 0; + for (int i = oddCounts.length - 1; i >= 0; i--) { + oddChecksumPortion *= 9; + oddChecksumPortion += oddCounts[i]; + oddSum += oddCounts[i]; + } + int evenChecksumPortion = 0; + int evenSum = 0; + for (int i = evenCounts.length - 1; i >= 0; i--) { + evenChecksumPortion *= 9; + evenChecksumPortion += evenCounts[i]; + evenSum += evenCounts[i]; + } + int checksumPortion = oddChecksumPortion + 3*evenChecksumPortion; + + if (outsideChar) { + if ((oddSum & 0x01) != 0 || oddSum > 12 || oddSum < 4) { + throw NotFoundException.getNotFoundInstance(); + } + int group = (12 - oddSum) / 2; + int oddWidest = OUTSIDE_ODD_WIDEST[group]; + int evenWidest = 9 - oddWidest; + int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, false); + int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, true); + int tEven = OUTSIDE_EVEN_TOTAL_SUBSET[group]; + int gSum = OUTSIDE_GSUM[group]; + return new DataCharacter(vOdd * tEven + vEven + gSum, checksumPortion); + } else { + if ((evenSum & 0x01) != 0 || evenSum > 10 || evenSum < 4) { + throw NotFoundException.getNotFoundInstance(); + } + int group = (10 - evenSum) / 2; + int oddWidest = INSIDE_ODD_WIDEST[group]; + int evenWidest = 9 - oddWidest; + int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, true); + int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, false); + int tOdd = INSIDE_ODD_TOTAL_SUBSET[group]; + int gSum = INSIDE_GSUM[group]; + return new DataCharacter(vEven * tOdd + vOdd + gSum, checksumPortion); + } + + } + + private int[] findFinderPattern(BitArray row, int rowOffset, boolean rightFinderPattern) + throws NotFoundException { + + int[] counters = decodeFinderCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + + int width = row.getSize(); + boolean isWhite = false; + while (rowOffset < width) { + isWhite = !row.get(rowOffset); + if (rightFinderPattern == isWhite) { + // Will encounter white first when searching for right finder pattern + break; + } + rowOffset++; + } + + int counterPosition = 0; + int patternStart = rowOffset; + for (int x = rowOffset; x < width; x++) { + boolean pixel = row.get(x); + if (pixel ^ isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == 3) { + if (isFinderPattern(counters)) { + return new int[]{patternStart, x}; + } + patternStart += counters[0] + counters[1]; + counters[0] = counters[2]; + counters[1] = counters[3]; + counters[2] = 0; + counters[3] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + + } + + private FinderPattern parseFoundFinderPattern(BitArray row, int rowNumber, boolean right, int[] startEnd) + throws NotFoundException { + // Actually we found elements 2-5 + boolean firstIsBlack = row.get(startEnd[0]); + int firstElementStart = startEnd[0] - 1; + // Locate element 1 + while (firstElementStart >= 0 && firstIsBlack ^ row.get(firstElementStart)) { + firstElementStart--; + } + firstElementStart++; + int firstCounter = startEnd[0] - firstElementStart; + // Make 'counters' hold 1-4 + int[] counters = decodeFinderCounters; + for (int i = counters.length - 1; i > 0; i--) { + counters[i] = counters[i-1]; + } + counters[0] = firstCounter; + int value = parseFinderValue(counters, FINDER_PATTERNS); + int start = firstElementStart; + int end = startEnd[1]; + if (right) { + // row is actually reversed + start = row.getSize() - 1 - start; + end = row.getSize() - 1 - end; + } + return new FinderPattern(value, new int[] {firstElementStart, startEnd[1]}, start, end, rowNumber); + } + + /* + private static int[] normalizeE2SEValues(int[] counters) { + int p = 0; + for (int i = 0; i < counters.length; i++) { + p += counters[i]; + } + int[] normalized = new int[counters.length - 2]; + for (int i = 0; i < normalized.length; i++) { + int e = counters[i] + counters[i+1]; + float eRatio = (float) e / (float) p; + float E = ((eRatio * 32.0f) + 1.0f) / 2.0f; + normalized[i] = (int) E; + } + return normalized; + } + */ + + private void adjustOddEvenCounts(boolean outsideChar, int numModules) throws NotFoundException { + + int oddSum = count(oddCounts); + int evenSum = count(evenCounts); + int mismatch = oddSum + evenSum - numModules; + boolean oddParityBad = (oddSum & 0x01) == (outsideChar ? 1 : 0); + boolean evenParityBad = (evenSum & 0x01) == 1; + + boolean incrementOdd = false; + boolean decrementOdd = false; + boolean incrementEven = false; + boolean decrementEven = false; + + if (outsideChar) { + if (oddSum > 12) { + decrementOdd = true; + } else if (oddSum < 4) { + incrementOdd = true; + } + if (evenSum > 12) { + decrementEven = true; + } else if (evenSum < 4) { + incrementEven = true; + } + } else { + if (oddSum > 11) { + decrementOdd = true; + } else if (oddSum < 5) { + incrementOdd = true; + } + if (evenSum > 10) { + decrementEven = true; + } else if (evenSum < 4) { + incrementEven = true; + } + } + + /*if (mismatch == 2) { + if (!(oddParityBad && evenParityBad)) { + throw ReaderException.getInstance(); + } + decrementOdd = true; + decrementEven = true; + } else if (mismatch == -2) { + if (!(oddParityBad && evenParityBad)) { + throw ReaderException.getInstance(); + } + incrementOdd = true; + incrementEven = true; + } else */if (mismatch == 1) { + if (oddParityBad) { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + decrementOdd = true; + } else { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + decrementEven = true; + } + } else if (mismatch == -1) { + if (oddParityBad) { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + incrementOdd = true; + } else { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + incrementEven = true; + } + } else if (mismatch == 0) { + if (oddParityBad) { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + // Both bad + if (oddSum < evenSum) { + incrementOdd = true; + decrementEven = true; + } else { + decrementOdd = true; + incrementEven = true; + } + } else { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + // Nothing to do! + } + } else { + throw NotFoundException.getNotFoundInstance(); + } + + if (incrementOdd) { + if (decrementOdd) { + throw NotFoundException.getNotFoundInstance(); + } + increment(oddCounts, oddRoundingErrors); + } + if (decrementOdd) { + decrement(oddCounts, oddRoundingErrors); + } + if (incrementEven) { + if (decrementEven) { + throw NotFoundException.getNotFoundInstance(); + } + increment(evenCounts, oddRoundingErrors); + } + if (decrementEven) { + decrement(evenCounts, evenRoundingErrors); + } + + } + +} diff --git a/src/com/google/zxing/oned/rss/RSSUtils.java b/src/com/google/zxing/oned/rss/RSSUtils.java new file mode 100644 index 0000000..6977fc2 --- /dev/null +++ b/src/com/google/zxing/oned/rss/RSSUtils.java @@ -0,0 +1,155 @@ +/* + * Copyright 2009 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.rss; + +/** Adapted from listings in ISO/IEC 24724 Appendix B and Appendix G. */ +public final class RSSUtils { + + private RSSUtils() {} + + static int[] getRSSwidths(int val, int n, int elements, int maxWidth, boolean noNarrow) { + int[] widths = new int[elements]; + int bar; + int narrowMask = 0; + for (bar = 0; bar < elements - 1; bar++) { + narrowMask |= (1 << bar); + int elmWidth = 1; + int subVal; + while (true) { + subVal = combins(n - elmWidth - 1, elements - bar - 2); + if (noNarrow && (narrowMask == 0) && + (n - elmWidth - (elements - bar - 1) >= elements - bar - 1)) { + subVal -= combins(n - elmWidth - (elements - bar), elements - bar - 2); + } + if (elements - bar - 1 > 1) { + int lessVal = 0; + for (int mxwElement = n - elmWidth - (elements - bar - 2); + mxwElement > maxWidth; + mxwElement--) { + lessVal += combins(n - elmWidth - mxwElement - 1, elements - bar - 3); + } + subVal -= lessVal * (elements - 1 - bar); + } else if (n - elmWidth > maxWidth) { + subVal--; + } + val -= subVal; + if (val < 0) { + break; + } + elmWidth++; + narrowMask &= ~(1 << bar); + } + val += subVal; + n -= elmWidth; + widths[bar] = elmWidth; + } + widths[bar] = n; + return widths; + } + + public static int getRSSvalue(int[] widths, int maxWidth, boolean noNarrow) { + int elements = widths.length; + int n = 0; + for (int i = 0; i < elements; i++) { + n += widths[i]; + } + int val = 0; + int narrowMask = 0; + for (int bar = 0; bar < elements - 1; bar++) { + int elmWidth; + for (elmWidth = 1, narrowMask |= (1 << bar); + elmWidth < widths[bar]; + elmWidth++, narrowMask &= ~(1 << bar)) { + int subVal = combins(n - elmWidth - 1, elements - bar - 2); + if (noNarrow && (narrowMask == 0) && + (n - elmWidth - (elements - bar - 1) >= elements - bar - 1)) { + subVal -= combins(n - elmWidth - (elements - bar), + elements - bar - 2); + } + if (elements - bar - 1 > 1) { + int lessVal = 0; + for (int mxwElement = n - elmWidth - (elements - bar - 2); + mxwElement > maxWidth; mxwElement--) { + lessVal += combins(n - elmWidth - mxwElement - 1, + elements - bar - 3); + } + subVal -= lessVal * (elements - 1 - bar); + } else if (n - elmWidth > maxWidth) { + subVal--; + } + val += subVal; + } + n -= elmWidth; + } + return (val); + } + + static int combins(int n, int r) { + int maxDenom, minDenom; + if (n - r > r) { + minDenom = r; + maxDenom = n - r; + } else { + minDenom = n - r; + maxDenom = r; + } + int val = 1; + int j = 1; + for (int i = n; i > maxDenom; i--) { + val *= i; + if (j <= minDenom) { + val /= j; + j++; + } + } + while (j <= minDenom) { + val /= j; + j++; + } + return (val); + } + + static int[] elements(int[] eDist, int N, int K) { + int[] widths = new int[eDist.length + 2]; + int twoK = K << 1; + widths[0] = 1; + int i; + int minEven = 10; + int barSum = 1; + for (i = 1; i < twoK - 2; i += 2) { + widths[i] = eDist[i - 1] - widths[i - 1]; + widths[i + 1] = eDist[i] - widths[i]; + barSum += widths[i] + widths[i + 1]; + if (widths[i] < minEven) { + minEven = widths[i]; + } + } + widths[twoK - 1] = N - barSum; + if (widths[twoK - 1] < minEven) { + minEven = widths[twoK - 1]; + } + if (minEven > 1) { + for (i = 0; i < twoK; i += 2) { + widths[i] += minEven - 1; + widths[i + 1] -= minEven - 1; + } + } + return widths; + } + + +} diff --git a/src/com/google/zxing/oned/rss/expanded/BitArrayBuilder.java b/src/com/google/zxing/oned/rss/expanded/BitArrayBuilder.java new file mode 100644 index 0000000..336046c --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/BitArrayBuilder.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded; + +import java.util.Vector; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class BitArrayBuilder { + + private BitArrayBuilder() { + } + + static BitArray buildBitArray(Vector pairs) { + int charNumber = (pairs.size() << 1) - 1; + if ((((ExpandedPair)pairs.lastElement()).getRightChar()) == null) { + charNumber -= 1; + } + + int size = 12 * charNumber; + + BitArray binary = new BitArray(size); + int accPos = 0; + + ExpandedPair firstPair = (ExpandedPair) pairs.elementAt(0); + int firstValue = firstPair.getRightChar().getValue(); + for(int i = 11; i >= 0; --i){ + if ((firstValue & (1 << i)) != 0) { + binary.set(accPos); + } + accPos++; + } + + for(int i = 1; i < pairs.size(); ++i){ + ExpandedPair currentPair = (ExpandedPair) pairs.elementAt(i); + + int leftValue = currentPair.getLeftChar().getValue(); + for(int j = 11; j >= 0; --j){ + if ((leftValue & (1 << j)) != 0) { + binary.set(accPos); + } + accPos++; + } + + if(currentPair.getRightChar() != null){ + int rightValue = currentPair.getRightChar().getValue(); + for(int j = 11; j >= 0; --j){ + if ((rightValue & (1 << j)) != 0) { + binary.set(accPos); + } + accPos++; + } + } + } + return binary; + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/ExpandedPair.java b/src/com/google/zxing/oned/rss/expanded/ExpandedPair.java new file mode 100644 index 0000000..cde1d02 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/ExpandedPair.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded; + +import com.google.zxing.oned.rss.DataCharacter; +import com.google.zxing.oned.rss.FinderPattern; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class ExpandedPair { + + private final boolean mayBeLast; + private final DataCharacter leftChar; + private final DataCharacter rightChar; + private final FinderPattern finderPattern; + + ExpandedPair(DataCharacter leftChar, DataCharacter rightChar, FinderPattern finderPattern, boolean mayBeLast) { + this.leftChar = leftChar; + this.rightChar = rightChar; + this.finderPattern = finderPattern; + this.mayBeLast = mayBeLast; + } + + boolean mayBeLast(){ + return this.mayBeLast; + } + + DataCharacter getLeftChar() { + return this.leftChar; + } + + DataCharacter getRightChar() { + return this.rightChar; + } + + FinderPattern getFinderPattern() { + return this.finderPattern; + } + + public boolean mustBeLast() { + return this.rightChar == null; + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/RSSExpandedReader.java b/src/com/google/zxing/oned/rss/expanded/RSSExpandedReader.java new file mode 100644 index 0000000..6e6ddc6 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/RSSExpandedReader.java @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded; + +import java.util.Hashtable; +import java.util.Vector; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; +import com.google.zxing.oned.rss.AbstractRSSReader; +import com.google.zxing.oned.rss.DataCharacter; +import com.google.zxing.oned.rss.FinderPattern; +import com.google.zxing.oned.rss.RSSUtils; +import com.google.zxing.oned.rss.expanded.decoders.AbstractExpandedDecoder; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +public final class RSSExpandedReader extends AbstractRSSReader{ + + private static final int[] SYMBOL_WIDEST = {7, 5, 4, 3, 1}; + private static final int[] EVEN_TOTAL_SUBSET = {4, 20, 52, 104, 204}; + private static final int[] GSUM = {0, 348, 1388, 2948, 3988}; + + private static final int[][] FINDER_PATTERNS = { + {1,8,4,1}, // A + {3,6,4,1}, // B + {3,4,6,1}, // C + {3,2,8,1}, // D + {2,6,5,1}, // E + {2,2,9,1} // F + }; + + private static final int[][] WEIGHTS = { + { 1, 3, 9, 27, 81, 32, 96, 77}, + { 20, 60, 180, 118, 143, 7, 21, 63}, + {189, 145, 13, 39, 117, 140, 209, 205}, + {193, 157, 49, 147, 19, 57, 171, 91}, + { 62, 186, 136, 197, 169, 85, 44, 132}, + {185, 133, 188, 142, 4, 12, 36, 108}, + {113, 128, 173, 97, 80, 29, 87, 50}, + {150, 28, 84, 41, 123, 158, 52, 156}, + { 46, 138, 203, 187, 139, 206, 196, 166}, + { 76, 17, 51, 153, 37, 111, 122, 155}, + { 43, 129, 176, 106, 107, 110, 119, 146}, + { 16, 48, 144, 10, 30, 90, 59, 177}, + {109, 116, 137, 200, 178, 112, 125, 164}, + { 70, 210, 208, 202, 184, 130, 179, 115}, + {134, 191, 151, 31, 93, 68, 204, 190}, + {148, 22, 66, 198, 172, 94, 71, 2}, + { 6, 18, 54, 162, 64, 192,154, 40}, + {120, 149, 25, 75, 14, 42,126, 167}, + { 79, 26, 78, 23, 69, 207,199, 175}, + {103, 98, 83, 38, 114, 131, 182, 124}, + {161, 61, 183, 127, 170, 88, 53, 159}, + { 55, 165, 73, 8, 24, 72, 5, 15}, + { 45, 135, 194, 160, 58, 174, 100, 89} + }; + + private static final int FINDER_PAT_A = 0; + private static final int FINDER_PAT_B = 1; + private static final int FINDER_PAT_C = 2; + private static final int FINDER_PAT_D = 3; + private static final int FINDER_PAT_E = 4; + private static final int FINDER_PAT_F = 5; + + private static final int [][] FINDER_PATTERN_SEQUENCES = { + { FINDER_PAT_A, FINDER_PAT_A }, + { FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B }, + { FINDER_PAT_A, FINDER_PAT_C, FINDER_PAT_B, FINDER_PAT_D }, + { FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_C }, + { FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_D, FINDER_PAT_F }, + { FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F }, + { FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_D }, + { FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_E }, + { FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F }, + { FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F }, + }; + + private static final int LONGEST_SEQUENCE_SIZE = FINDER_PATTERN_SEQUENCES[FINDER_PATTERN_SEQUENCES.length - 1].length; + + private static final int MAX_PAIRS = 11; + private final Vector pairs = new Vector(MAX_PAIRS); + private final int [] startEnd = new int[2]; + private final int [] currentSequence = new int[LONGEST_SEQUENCE_SIZE]; + + public Result decodeRow(int rowNumber, BitArray row, Hashtable hints) throws NotFoundException { + this.reset(); + decodeRow2pairs(rowNumber, row); + return constructResult(this.pairs); + } + + public void reset() { + this.pairs.setSize(0); + } + + // Not private for testing + Vector decodeRow2pairs(int rowNumber, BitArray row) throws NotFoundException { + while(true){ + ExpandedPair nextPair = retrieveNextPair(row, this.pairs, rowNumber); + this.pairs.addElement(nextPair); + + if(nextPair.mayBeLast()){ + if(checkChecksum()) { + return this.pairs; + } + if(nextPair.mustBeLast()) { + throw NotFoundException.getNotFoundInstance(); + } + } + } + } + + private static Result constructResult(Vector pairs) throws NotFoundException{ + BitArray binary = BitArrayBuilder.buildBitArray(pairs); + + AbstractExpandedDecoder decoder = AbstractExpandedDecoder.createDecoder(binary); + String resultingString = decoder.parseInformation(); + + ResultPoint [] firstPoints = ((ExpandedPair)pairs.elementAt(0)).getFinderPattern().getResultPoints(); + ResultPoint [] lastPoints = ((ExpandedPair)pairs.lastElement()).getFinderPattern().getResultPoints(); + + return new Result( + resultingString, + null, + new ResultPoint[]{firstPoints[0], firstPoints[1], lastPoints[0], lastPoints[1]}, + BarcodeFormat.RSS_EXPANDED + ); + } + + private boolean checkChecksum(){ + ExpandedPair firstPair = (ExpandedPair)this.pairs.elementAt(0); + DataCharacter checkCharacter = firstPair.getLeftChar(); + DataCharacter firstCharacter = firstPair.getRightChar(); + + int checksum = firstCharacter.getChecksumPortion(); + int S = 2; + + for(int i = 1; i < this.pairs.size(); ++i){ + ExpandedPair currentPair = (ExpandedPair)this.pairs.elementAt(i); + checksum += currentPair.getLeftChar().getChecksumPortion(); + S++; + if(currentPair.getRightChar() != null){ + checksum += currentPair.getRightChar().getChecksumPortion(); + S++; + } + } + + checksum %= 211; + + int checkCharacterValue = 211 * (S - 4) + checksum; + + return checkCharacterValue == checkCharacter.getValue(); + } + + private static int getNextSecondBar(BitArray row, int initialPos){ + int currentPos = initialPos; + boolean current = row.get(currentPos); + + while(currentPos < row.size && row.get(currentPos) == current) { + currentPos++; + } + + current = !current; + while(currentPos < row.size && row.get(currentPos) == current) { + currentPos++; + } + + return currentPos; + } + + // not private for testing + ExpandedPair retrieveNextPair(BitArray row, Vector previousPairs, int rowNumber) throws NotFoundException{ + boolean isOddPattern = previousPairs.size() % 2 == 0; + + FinderPattern pattern; + + boolean keepFinding = true; + int forcedOffset = -1; + do{ + this.findNextPair(row, previousPairs, forcedOffset); + pattern = parseFoundFinderPattern(row, rowNumber, isOddPattern); + if (pattern == null){ + forcedOffset = getNextSecondBar(row, this.startEnd[0]); + } else { + keepFinding = false; + } + }while(keepFinding); + + boolean mayBeLast = checkPairSequence(previousPairs, pattern); + + DataCharacter leftChar = this.decodeDataCharacter(row, pattern, isOddPattern, true); + DataCharacter rightChar; + try{ + rightChar = this.decodeDataCharacter(row, pattern, isOddPattern, false); + }catch(NotFoundException nfe){ + if(mayBeLast) { + rightChar = null; + } else { + throw nfe; + } + } + + return new ExpandedPair(leftChar, rightChar, pattern, mayBeLast); + } + + private boolean checkPairSequence(Vector previousPairs, FinderPattern pattern) throws NotFoundException{ + int currentSequenceLength = previousPairs.size() + 1; + if(currentSequenceLength > this.currentSequence.length) { + throw NotFoundException.getNotFoundInstance(); + } + + for(int pos = 0; pos < previousPairs.size(); ++pos) { + this.currentSequence[pos] = ((ExpandedPair) previousPairs.elementAt(pos)).getFinderPattern().getValue(); + } + + this.currentSequence[currentSequenceLength - 1] = pattern.getValue(); + + for(int i = 0; i < FINDER_PATTERN_SEQUENCES.length; ++i){ + int [] validSequence = FINDER_PATTERN_SEQUENCES[i]; + if(validSequence.length >= currentSequenceLength){ + boolean valid = true; + for(int pos = 0; pos < currentSequenceLength; ++pos) { + if (this.currentSequence[pos] != validSequence[pos]) { + valid = false; + break; + } + } + + if(valid) { + return currentSequenceLength == validSequence.length; + } + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + private void findNextPair(BitArray row, Vector previousPairs, int forcedOffset) throws NotFoundException{ + int[] counters = this.decodeFinderCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + + int width = row.getSize(); + + int rowOffset; + if (forcedOffset >= 0) { + rowOffset = forcedOffset; + } else if (previousPairs.isEmpty()) { + rowOffset = 0; + } else{ + ExpandedPair lastPair = ((ExpandedPair)previousPairs.lastElement()); + rowOffset = lastPair.getFinderPattern().getStartEnd()[1]; + } + boolean searchingEvenPair = previousPairs.size() % 2 != 0; + + boolean isWhite = false; + while (rowOffset < width) { + isWhite = !row.get(rowOffset); + if (!isWhite) { + break; + } + rowOffset++; + } + + int counterPosition = 0; + int patternStart = rowOffset; + for (int x = rowOffset; x < width; x++) { + boolean pixel = row.get(x); + if (pixel ^ isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == 3) { + if (searchingEvenPair) { + reverseCounters(counters); + } + + if (isFinderPattern(counters)){ + this.startEnd[0] = patternStart; + this.startEnd[1] = x; + return; + } + + if (searchingEvenPair) { + reverseCounters(counters); + } + + patternStart += counters[0] + counters[1]; + counters[0] = counters[2]; + counters[1] = counters[3]; + counters[2] = 0; + counters[3] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static void reverseCounters(int [] counters){ + int length = counters.length; + for(int i = 0; i < length / 2; ++i){ + int tmp = counters[i]; + counters[i] = counters[length - i - 1]; + counters[length - i - 1] = tmp; + } + } + + private FinderPattern parseFoundFinderPattern(BitArray row, int rowNumber, boolean oddPattern) { + // Actually we found elements 2-5. + int firstCounter; + int start; + int end; + + if(oddPattern){ + // If pattern number is odd, we need to locate element 1 *before* the current block. + + int firstElementStart = this.startEnd[0] - 1; + // Locate element 1 + while (firstElementStart >= 0 && !row.get(firstElementStart)) { + firstElementStart--; + } + + firstElementStart++; + firstCounter = this.startEnd[0] - firstElementStart; + start = firstElementStart; + end = this.startEnd[1]; + + }else{ + // If pattern number is even, the pattern is reversed, so we need to locate element 1 *after* the current block. + + start = this.startEnd[0]; + + int firstElementStart = this.startEnd[1] + 1; + while(row.get(firstElementStart) && firstElementStart < row.size) { + firstElementStart++; + } + + end = firstElementStart; + firstCounter = end - this.startEnd[1]; + } + + // Make 'counters' hold 1-4 + int [] counters = this.decodeFinderCounters; + for (int i = counters.length - 1; i > 0; i--) { + counters[i] = counters[i - 1]; + } + + counters[0] = firstCounter; + int value; + try { + value = parseFinderValue(counters, FINDER_PATTERNS); + } catch (NotFoundException nfe) { + return null; + } + return new FinderPattern(value, new int[] {start, end}, start, end, rowNumber); + } + + DataCharacter decodeDataCharacter(BitArray row, FinderPattern pattern, boolean isOddPattern, boolean leftChar) + throws NotFoundException { + int[] counters = this.dataCharacterCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + counters[4] = 0; + counters[5] = 0; + counters[6] = 0; + counters[7] = 0; + + if (leftChar) { + recordPatternInReverse(row, pattern.getStartEnd()[0], counters); + } else { + recordPattern(row, pattern.getStartEnd()[1] + 1, counters); + // reverse it + for (int i = 0, j = counters.length - 1; i < j; i++, j--) { + int temp = counters[i]; + counters[i] = counters[j]; + counters[j] = temp; + } + }//counters[] has the pixels of the module + + int numModules = 17; //left and right data characters have all the same length + float elementWidth = (float) count(counters) / (float) numModules; + + int[] oddCounts = this.oddCounts; + int[] evenCounts = this.evenCounts; + float[] oddRoundingErrors = this.oddRoundingErrors; + float[] evenRoundingErrors = this.evenRoundingErrors; + + for (int i = 0; i < counters.length; i++) { + float value = 1.0f * counters[i] / elementWidth; + int count = (int) (value + 0.5f); // Round + if (count < 1) { + count = 1; + } else if (count > 8) { + count = 8; + } + int offset = i >> 1; + if ((i & 0x01) == 0) { + oddCounts[offset] = count; + oddRoundingErrors[offset] = value - count; + } else { + evenCounts[offset] = count; + evenRoundingErrors[offset] = value - count; + } + } + + adjustOddEvenCounts(numModules); + + int weightRowNumber = 4 * pattern.getValue() + (isOddPattern?0:2) + (leftChar?0:1) - 1; + + int oddSum = 0; + int oddChecksumPortion = 0; + for (int i = oddCounts.length - 1; i >= 0; i--) { + if(isNotA1left(pattern, isOddPattern, leftChar)){ + int weight = WEIGHTS[weightRowNumber][2 * i]; + oddChecksumPortion += oddCounts[i] * weight; + } + oddSum += oddCounts[i]; + } + int evenChecksumPortion = 0; + int evenSum = 0; + for (int i = evenCounts.length - 1; i >= 0; i--) { + if(isNotA1left(pattern, isOddPattern, leftChar)){ + int weight = WEIGHTS[weightRowNumber][2 * i + 1]; + evenChecksumPortion += evenCounts[i] * weight; + } + evenSum += evenCounts[i]; + } + int checksumPortion = oddChecksumPortion + evenChecksumPortion; + + if ((oddSum & 0x01) != 0 || oddSum > 13 || oddSum < 4) { + throw NotFoundException.getNotFoundInstance(); + } + + int group = (13 - oddSum) / 2; + int oddWidest = SYMBOL_WIDEST[group]; + int evenWidest = 9 - oddWidest; + int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, true); + int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, false); + int tEven = EVEN_TOTAL_SUBSET[group]; + int gSum = GSUM[group]; + int value = vOdd * tEven + vEven + gSum; + + return new DataCharacter(value, checksumPortion); + } + + private static boolean isNotA1left(FinderPattern pattern, boolean isOddPattern, boolean leftChar) { + // A1: pattern.getValue is 0 (A), and it's an oddPattern, and it is a left char + return !(pattern.getValue() == 0 && isOddPattern && leftChar); + } + + private void adjustOddEvenCounts(int numModules) throws NotFoundException { + + int oddSum = count(this.oddCounts); + int evenSum = count(this.evenCounts); + int mismatch = oddSum + evenSum - numModules; + boolean oddParityBad = (oddSum & 0x01) == 1; + boolean evenParityBad = (evenSum & 0x01) == 0; + + boolean incrementOdd = false; + boolean decrementOdd = false; + + if (oddSum > 13) { + decrementOdd = true; + } else if (oddSum < 4) { + incrementOdd = true; + } + boolean incrementEven = false; + boolean decrementEven = false; + if (evenSum > 13) { + decrementEven = true; + } else if (evenSum < 4) { + incrementEven = true; + } + + if (mismatch == 1) { + if (oddParityBad) { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + decrementOdd = true; + } else { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + decrementEven = true; + } + } else if (mismatch == -1) { + if (oddParityBad) { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + incrementOdd = true; + } else { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + incrementEven = true; + } + } else if (mismatch == 0) { + if (oddParityBad) { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + // Both bad + if (oddSum < evenSum) { + incrementOdd = true; + decrementEven = true; + } else { + decrementOdd = true; + incrementEven = true; + } + } else { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + // Nothing to do! + } + } else { + throw NotFoundException.getNotFoundInstance(); + } + + if (incrementOdd) { + if (decrementOdd) { + throw NotFoundException.getNotFoundInstance(); + } + increment(this.oddCounts, this.oddRoundingErrors); + } + if (decrementOdd) { + decrement(this.oddCounts, this.oddRoundingErrors); + } + if (incrementEven) { + if (decrementEven) { + throw NotFoundException.getNotFoundInstance(); + } + increment(this.evenCounts, this.oddRoundingErrors); + } + if (decrementEven) { + decrement(this.evenCounts, this.evenRoundingErrors); + } + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI013103decoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI013103decoder.java new file mode 100644 index 0000000..5ef3fed --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI013103decoder.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class AI013103decoder extends AI013x0xDecoder { + + AI013103decoder(BitArray information) { + super(information); + } + + protected void addWeightCode(StringBuffer buf, int weight) { + buf.append("(3103)"); + } + + protected int checkWeight(int weight) { + return weight; + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI01320xDecoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI01320xDecoder.java new file mode 100644 index 0000000..1fd7c0e --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI01320xDecoder.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class AI01320xDecoder extends AI013x0xDecoder { + + AI01320xDecoder(BitArray information) { + super(information); + } + + protected void addWeightCode(StringBuffer buf, int weight) { + if (weight < 10000) { + buf.append("(3202)"); + } else { + buf.append("(3203)"); + } + } + + protected int checkWeight(int weight) { + if(weight < 10000) { + return weight; + } + return weight - 10000; + } + +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI01392xDecoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI01392xDecoder.java new file mode 100644 index 0000000..e618d81 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI01392xDecoder.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class AI01392xDecoder extends AI01decoder { + + private static final int headerSize = 5 + 1 + 2; + private static final int lastDigitSize = 2; + + AI01392xDecoder(BitArray information) { + super(information); + } + + public String parseInformation() throws NotFoundException { + if (this.information.size < headerSize + gtinSize) { + throw NotFoundException.getNotFoundInstance(); + } + + StringBuffer buf = new StringBuffer(); + + encodeCompressedGtin(buf, headerSize); + + int lastAIdigit = + this.generalDecoder.extractNumericValueFromBitArray(headerSize + gtinSize, lastDigitSize); + buf.append("(392"); + buf.append(lastAIdigit); + buf.append(')'); + + DecodedInformation decodedInformation = + this.generalDecoder.decodeGeneralPurposeField(headerSize + gtinSize + lastDigitSize, null); + buf.append(decodedInformation.getNewString()); + + return buf.toString(); + } + +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI01393xDecoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI01393xDecoder.java new file mode 100644 index 0000000..67db980 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI01393xDecoder.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class AI01393xDecoder extends AI01decoder { + + private static final int headerSize = 5 + 1 + 2; + private static final int lastDigitSize = 2; + private static final int firstThreeDigitsSize = 10; + + AI01393xDecoder(BitArray information) { + super(information); + } + + public String parseInformation() throws NotFoundException { + if(this.information.size < headerSize + gtinSize) { + throw NotFoundException.getNotFoundInstance(); + } + + StringBuffer buf = new StringBuffer(); + + encodeCompressedGtin(buf, headerSize); + + int lastAIdigit = + this.generalDecoder.extractNumericValueFromBitArray(headerSize + gtinSize, lastDigitSize); + + buf.append("(393"); + buf.append(lastAIdigit); + buf.append(')'); + + int firstThreeDigits = + this.generalDecoder.extractNumericValueFromBitArray(headerSize + gtinSize + lastDigitSize, firstThreeDigitsSize); + if(firstThreeDigits / 100 == 0) { + buf.append('0'); + } + if(firstThreeDigits / 10 == 0) { + buf.append('0'); + } + buf.append(firstThreeDigits); + + DecodedInformation generalInformation = + this.generalDecoder.decodeGeneralPurposeField(headerSize + gtinSize + lastDigitSize + firstThreeDigitsSize, null); + buf.append(generalInformation.getNewString()); + + return buf.toString(); + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI013x0x1xDecoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI013x0x1xDecoder.java new file mode 100644 index 0000000..0c373c3 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI013x0x1xDecoder.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class AI013x0x1xDecoder extends AI01weightDecoder { + + private static final int headerSize = 7 + 1; + private static final int weightSize = 20; + private static final int dateSize = 16; + + private final String dateCode; + private final String firstAIdigits; + + AI013x0x1xDecoder(BitArray information, String firstAIdigits, String dateCode) { + super(information); + this.dateCode = dateCode; + this.firstAIdigits = firstAIdigits; + } + + public String parseInformation() throws NotFoundException { + if (this.information.size != headerSize + gtinSize + weightSize + dateSize) { + throw NotFoundException.getNotFoundInstance(); + } + + StringBuffer buf = new StringBuffer(); + + encodeCompressedGtin(buf, headerSize); + encodeCompressedWeight(buf, headerSize + gtinSize, weightSize); + encodeCompressedDate(buf, headerSize + gtinSize + weightSize); + + return buf.toString(); + } + + private void encodeCompressedDate(StringBuffer buf, int currentPos) { + int numericDate = this.generalDecoder.extractNumericValueFromBitArray(currentPos, dateSize); + if(numericDate == 38400) { + return; + } + + buf.append('('); + buf.append(this.dateCode); + buf.append(')'); + + int day = numericDate % 32; + numericDate /= 32; + int month = numericDate % 12 + 1; + numericDate /= 12; + int year = numericDate; + + if (year / 10 == 0) { + buf.append('0'); + } + buf.append(year); + if (month / 10 == 0) { + buf.append('0'); + } + buf.append(month); + if (day / 10 == 0) { + buf.append('0'); + } + buf.append(day); + } + + protected void addWeightCode(StringBuffer buf, int weight) { + int lastAI = weight / 100000; + buf.append('('); + buf.append(this.firstAIdigits); + buf.append(lastAI); + buf.append(')'); + } + + protected int checkWeight(int weight) { + return weight % 100000; + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI013x0xDecoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI013x0xDecoder.java new file mode 100644 index 0000000..96bdaff --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI013x0xDecoder.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +abstract class AI013x0xDecoder extends AI01weightDecoder { + + private static final int headerSize = 4 + 1; + private static final int weightSize = 15; + + AI013x0xDecoder(BitArray information) { + super(information); + } + + public String parseInformation() throws NotFoundException { + if (this.information.size != headerSize + gtinSize + weightSize) { + throw NotFoundException.getNotFoundInstance(); + } + + StringBuffer buf = new StringBuffer(); + + encodeCompressedGtin(buf, headerSize); + encodeCompressedWeight(buf, headerSize + gtinSize, weightSize); + + return buf.toString(); + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI01AndOtherAIs.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI01AndOtherAIs.java new file mode 100644 index 0000000..49f109b --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI01AndOtherAIs.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class AI01AndOtherAIs extends AI01decoder { + + private static final int HEADER_SIZE = 1 + 1 + 2; //first bit encodes the linkage flag, + //the second one is the encodation method, and the other two are for the variable length + AI01AndOtherAIs(BitArray information) { + super(information); + } + + public String parseInformation() throws NotFoundException { + StringBuffer buff = new StringBuffer(); + + buff.append("(01)"); + int initialGtinPosition = buff.length(); + int firstGtinDigit = this.generalDecoder.extractNumericValueFromBitArray(HEADER_SIZE, 4); + buff.append(firstGtinDigit); + + this.encodeCompressedGtinWithoutAI(buff, HEADER_SIZE + 4, initialGtinPosition); + + return this.generalDecoder.decodeAllCodes(buff, HEADER_SIZE + 44); + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI01decoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI01decoder.java new file mode 100644 index 0000000..62f878b --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI01decoder.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +abstract class AI01decoder extends AbstractExpandedDecoder { + + protected static final int gtinSize = 40; + + AI01decoder(BitArray information) { + super(information); + } + + protected void encodeCompressedGtin(StringBuffer buf, int currentPos) { + buf.append("(01)"); + int initialPosition = buf.length(); + buf.append('9'); + + encodeCompressedGtinWithoutAI(buf, currentPos, initialPosition); + } + + protected void encodeCompressedGtinWithoutAI(StringBuffer buf, int currentPos, int initialBufferPosition) { + for(int i = 0; i < 4; ++i){ + int currentBlock = this.generalDecoder.extractNumericValueFromBitArray(currentPos + 10 * i, 10); + if (currentBlock / 100 == 0) { + buf.append('0'); + } + if (currentBlock / 10 == 0) { + buf.append('0'); + } + buf.append(currentBlock); + } + + appendCheckDigit(buf, initialBufferPosition); + } + + private static void appendCheckDigit(StringBuffer buf, int currentPos){ + int checkDigit = 0; + for (int i = 0; i < 13; i++) { + int digit = buf.charAt(i + currentPos) - '0'; + checkDigit += (((i & 0x01) == 0) ? 3 * digit : digit); + } + + checkDigit = 10 - (checkDigit % 10); + if (checkDigit == 10) { + checkDigit = 0; + } + + buf.append(checkDigit); + } + +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AI01weightDecoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AI01weightDecoder.java new file mode 100644 index 0000000..f120cc0 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AI01weightDecoder.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +abstract class AI01weightDecoder extends AI01decoder { + + AI01weightDecoder(BitArray information) { + super(information); + } + + protected void encodeCompressedWeight(StringBuffer buf, int currentPos, int weightSize) { + int originalWeightNumeric = this.generalDecoder.extractNumericValueFromBitArray(currentPos, weightSize); + addWeightCode(buf, originalWeightNumeric); + + int weightNumeric = checkWeight(originalWeightNumeric); + + int currentDivisor = 100000; + for(int i = 0; i < 5; ++i){ + if (weightNumeric / currentDivisor == 0) { + buf.append('0'); + } + currentDivisor /= 10; + } + buf.append(weightNumeric); + } + + protected abstract void addWeightCode(StringBuffer buf, int weight); + protected abstract int checkWeight(int weight); +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AbstractExpandedDecoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AbstractExpandedDecoder.java new file mode 100644 index 0000000..84fae37 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AbstractExpandedDecoder.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +public abstract class AbstractExpandedDecoder { + + protected final BitArray information; + protected final GeneralAppIdDecoder generalDecoder; + + AbstractExpandedDecoder(BitArray information){ + this.information = information; + this.generalDecoder = new GeneralAppIdDecoder(information); + } + + public abstract String parseInformation() throws NotFoundException; + + public static AbstractExpandedDecoder createDecoder(BitArray information){ + if (information.get(1)) { + return new AI01AndOtherAIs(information); + } else if (!information.get(2)) { + return new AnyAIDecoder(information); + } + + int fourBitEncodationMethod = GeneralAppIdDecoder.extractNumericValueFromBitArray(information, 1, 4); + + switch(fourBitEncodationMethod){ + case 4: return new AI013103decoder(information); + case 5: return new AI01320xDecoder(information); + } + + int fiveBitEncodationMethod = GeneralAppIdDecoder.extractNumericValueFromBitArray(information, 1, 5); + switch(fiveBitEncodationMethod){ + case 12: return new AI01392xDecoder(information); + case 13: return new AI01393xDecoder(information); + } + + int sevenBitEncodationMethod = GeneralAppIdDecoder.extractNumericValueFromBitArray(information, 1, 7); + switch(sevenBitEncodationMethod){ + case 56: return new AI013x0x1xDecoder(information, "310", "11"); + case 57: return new AI013x0x1xDecoder(information, "320", "11"); + case 58: return new AI013x0x1xDecoder(information, "310", "13"); + case 59: return new AI013x0x1xDecoder(information, "320", "13"); + case 60: return new AI013x0x1xDecoder(information, "310", "15"); + case 61: return new AI013x0x1xDecoder(information, "320", "15"); + case 62: return new AI013x0x1xDecoder(information, "310", "17"); + case 63: return new AI013x0x1xDecoder(information, "320", "17"); + } + + throw new IllegalStateException("unknown decoder: " + information); + } + + + +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/AnyAIDecoder.java b/src/com/google/zxing/oned/rss/expanded/decoders/AnyAIDecoder.java new file mode 100644 index 0000000..4fcac27 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/AnyAIDecoder.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class AnyAIDecoder extends AbstractExpandedDecoder { + + private static final int HEADER_SIZE = 2 + 1 + 2; + + AnyAIDecoder(BitArray information) { + super(information); + } + + public String parseInformation() throws NotFoundException { + StringBuffer buf = new StringBuffer(); + return this.generalDecoder.decodeAllCodes(buf, HEADER_SIZE); + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/BlockParsedResult.java b/src/com/google/zxing/oned/rss/expanded/decoders/BlockParsedResult.java new file mode 100644 index 0000000..132ba55 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/BlockParsedResult.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class BlockParsedResult { + + private final DecodedInformation decodedInformation; + private final boolean finished; + + BlockParsedResult() { + this.finished = true; + this.decodedInformation = null; + } + + BlockParsedResult(boolean finished) { + this.finished = finished; + this.decodedInformation = null; + } + + BlockParsedResult(DecodedInformation information, boolean finished) { + this.finished = finished; + this.decodedInformation = information; + } + + DecodedInformation getDecodedInformation() { + return this.decodedInformation; + } + + boolean isFinished() { + return this.finished; + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/CurrentParsingState.java b/src/com/google/zxing/oned/rss/expanded/decoders/CurrentParsingState.java new file mode 100644 index 0000000..cf7d687 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/CurrentParsingState.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class CurrentParsingState { + + int position; + private int encoding; + + private static final int NUMERIC = 1; + private static final int ALPHA = 2; + private static final int ISO_IEC_646 = 4; + + CurrentParsingState(){ + this.position = 0; + this.encoding = NUMERIC; + } + + boolean isAlpha(){ + return this.encoding == ALPHA; + } + + boolean isNumeric(){ + return this.encoding == NUMERIC; + } + + boolean isIsoIec646(){ + return this.encoding == ISO_IEC_646; + } + + void setNumeric(){ + this.encoding = NUMERIC; + } + + void setAlpha(){ + this.encoding = ALPHA; + } + + void setIsoIec646(){ + this.encoding = ISO_IEC_646; + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/DecodedChar.java b/src/com/google/zxing/oned/rss/expanded/decoders/DecodedChar.java new file mode 100644 index 0000000..790b684 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/DecodedChar.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class DecodedChar extends DecodedObject { + + private final char value; + + static final char FNC1 = '$'; // It's not in Alphanumeric neither in ISO/IEC 646 charset + + DecodedChar(int newPosition, char value) { + super(newPosition); + this.value = value; + } + + char getValue(){ + return this.value; + } + + boolean isFNC1(){ + return this.value == FNC1; + } + +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/DecodedInformation.java b/src/com/google/zxing/oned/rss/expanded/decoders/DecodedInformation.java new file mode 100644 index 0000000..13eec5c --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/DecodedInformation.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class DecodedInformation extends DecodedObject { + private final String newString; + private final int remainingValue; + private final boolean remaining; + + DecodedInformation(int newPosition, String newString){ + super(newPosition); + this.newString = newString; + this.remaining = false; + this.remainingValue = 0; + } + + DecodedInformation(int newPosition, String newString, int remainingValue){ + super(newPosition); + this.remaining = true; + this.remainingValue = remainingValue; + this.newString = newString; + } + + String getNewString(){ + return this.newString; + } + + boolean isRemaining(){ + return this.remaining; + } + + int getRemainingValue(){ + return this.remainingValue; + } +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/DecodedNumeric.java b/src/com/google/zxing/oned/rss/expanded/decoders/DecodedNumeric.java new file mode 100644 index 0000000..1b1be42 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/DecodedNumeric.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class DecodedNumeric extends DecodedObject { + + private final int firstDigit; + private final int secondDigit; + + static final int FNC1 = 10; + + DecodedNumeric(int newPosition, int firstDigit, int secondDigit){ + super(newPosition); + + this.firstDigit = firstDigit; + this.secondDigit = secondDigit; + + if (this.firstDigit < 0 || this.firstDigit > 10) { + throw new IllegalArgumentException("Invalid firstDigit: " + firstDigit); + } + + if (this.secondDigit < 0 || this.secondDigit > 10) { + throw new IllegalArgumentException("Invalid secondDigit: " + secondDigit); + } + } + + int getFirstDigit(){ + return this.firstDigit; + } + + int getSecondDigit(){ + return this.secondDigit; + } + + int getValue(){ + return this.firstDigit * 10 + this.secondDigit; + } + + boolean isFirstDigitFNC1(){ + return this.firstDigit == FNC1; + } + + boolean isSecondDigitFNC1(){ + return this.secondDigit == FNC1; + } + + boolean isAnyFNC1(){ + return this.firstDigit == FNC1 || this.secondDigit == FNC1; + } + +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/DecodedObject.java b/src/com/google/zxing/oned/rss/expanded/decoders/DecodedObject.java new file mode 100644 index 0000000..7547384 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/DecodedObject.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +abstract class DecodedObject { + + protected final int newPosition; + + DecodedObject(int newPosition){ + this.newPosition = newPosition; + } + + int getNewPosition() { + return this.newPosition; + } + +} diff --git a/src/com/google/zxing/oned/rss/expanded/decoders/FieldParser.java b/src/com/google/zxing/oned/rss/expanded/decoders/FieldParser.java new file mode 100644 index 0000000..91c1d49 --- /dev/null +++ b/src/com/google/zxing/oned/rss/expanded/decoders/FieldParser.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2010 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. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class FieldParser { + + private static final Object VARIABLE_LENGTH = new Object(); + + private static final Object [][] TWO_DIGIT_DATA_LENGTH = { + // "DIGITS", new Integer(LENGTH) + // or + // "DIGITS", VARIABLE_LENGTH, new Integer(MAX_SIZE) + + { "00", new Integer(18) }, + { "01", new Integer(14) }, + { "02", new Integer(14) }, + + { "10", VARIABLE_LENGTH, new Integer(20) }, + { "11", new Integer(6) }, + { "12", new Integer(6) }, + { "13", new Integer(6) }, + { "15", new Integer(6) }, + { "17", new Integer(6) }, + + { "20", new Integer(2) }, + { "21", VARIABLE_LENGTH, new Integer(20) }, + { "22", VARIABLE_LENGTH, new Integer(29) }, + + { "30", VARIABLE_LENGTH, new Integer( 8) }, + { "37", VARIABLE_LENGTH, new Integer( 8) }, + + //internal company codes + { "90", VARIABLE_LENGTH, new Integer(30) }, + { "91", VARIABLE_LENGTH, new Integer(30) }, + { "92", VARIABLE_LENGTH, new Integer(30) }, + { "93", VARIABLE_LENGTH, new Integer(30) }, + { "94", VARIABLE_LENGTH, new Integer(30) }, + { "95", VARIABLE_LENGTH, new Integer(30) }, + { "96", VARIABLE_LENGTH, new Integer(30) }, + { "97", VARIABLE_LENGTH, new Integer(30) }, + { "98", VARIABLE_LENGTH, new Integer(30) }, + { "99", VARIABLE_LENGTH, new Integer(30) }, + }; + + private static final Object [][] THREE_DIGIT_DATA_LENGTH = { + // Same format as above + + { "240", VARIABLE_LENGTH, new Integer(30) }, + { "241", VARIABLE_LENGTH, new Integer(30) }, + { "242", VARIABLE_LENGTH, new Integer( 6) }, + { "250", VARIABLE_LENGTH, new Integer(30) }, + { "251", VARIABLE_LENGTH, new Integer(30) }, + { "253", VARIABLE_LENGTH, new Integer(17) }, + { "254", VARIABLE_LENGTH, new Integer(20) }, + + { "400", VARIABLE_LENGTH, new Integer(30) }, + { "401", VARIABLE_LENGTH, new Integer(30) }, + { "402", new Integer(17) }, + { "403", VARIABLE_LENGTH, new Integer(30) }, + { "410", new Integer(13) }, + { "411", new Integer(13) }, + { "412", new Integer(13) }, + { "413", new Integer(13) }, + { "414", new Integer(13) }, + { "420", VARIABLE_LENGTH, new Integer(20) }, + { "421", VARIABLE_LENGTH, new Integer(15) }, + { "422", new Integer( 3) }, + { "423", VARIABLE_LENGTH, new Integer(15) }, + { "424", new Integer(3) }, + { "425", new Integer(3) }, + { "426", new Integer(3) }, + }; + + private static final Object [][] THREE_DIGIT_PLUS_DIGIT_DATA_LENGTH = { + // Same format as above + + { "310", new Integer(6) }, + { "311", new Integer(6) }, + { "312", new Integer(6) }, + { "313", new Integer(6) }, + { "314", new Integer(6) }, + { "315", new Integer(6) }, + { "316", new Integer(6) }, + { "320", new Integer(6) }, + { "321", new Integer(6) }, + { "322", new Integer(6) }, + { "323", new Integer(6) }, + { "324", new Integer(6) }, + { "325", new Integer(6) }, + { "326", new Integer(6) }, + { "327", new Integer(6) }, + { "328", new Integer(6) }, + { "329", new Integer(6) }, + { "330", new Integer(6) }, + { "331", new Integer(6) }, + { "332", new Integer(6) }, + { "333", new Integer(6) }, + { "334", new Integer(6) }, + { "335", new Integer(6) }, + { "336", new Integer(6) }, + { "340", new Integer(6) }, + { "341", new Integer(6) }, + { "342", new Integer(6) }, + { "343", new Integer(6) }, + { "344", new Integer(6) }, + { "345", new Integer(6) }, + { "346", new Integer(6) }, + { "347", new Integer(6) }, + { "348", new Integer(6) }, + { "349", new Integer(6) }, + { "350", new Integer(6) }, + { "351", new Integer(6) }, + { "352", new Integer(6) }, + { "353", new Integer(6) }, + { "354", new Integer(6) }, + { "355", new Integer(6) }, + { "356", new Integer(6) }, + { "357", new Integer(6) }, + { "360", new Integer(6) }, + { "361", new Integer(6) }, + { "362", new Integer(6) }, + { "363", new Integer(6) }, + { "364", new Integer(6) }, + { "365", new Integer(6) }, + { "366", new Integer(6) }, + { "367", new Integer(6) }, + { "368", new Integer(6) }, + { "369", new Integer(6) }, + { "390", VARIABLE_LENGTH, new Integer(15) }, + { "391", VARIABLE_LENGTH, new Integer(18) }, + { "392", VARIABLE_LENGTH, new Integer(15) }, + { "393", VARIABLE_LENGTH, new Integer(18) }, + { "703", VARIABLE_LENGTH, new Integer(30) } + }; + + private static final Object [][] FOUR_DIGIT_DATA_LENGTH = { + // Same format as above + + { "7001", new Integer(13) }, + { "7002", VARIABLE_LENGTH, new Integer(30) }, + { "7003", new Integer(10) }, + + { "8001", new Integer(14) }, + { "8002", VARIABLE_LENGTH, new Integer(20) }, + { "8003", VARIABLE_LENGTH, new Integer(30) }, + { "8004", VARIABLE_LENGTH, new Integer(30) }, + { "8005", new Integer(6) }, + { "8006", new Integer(18) }, + { "8007", VARIABLE_LENGTH, new Integer(30) }, + { "8008", VARIABLE_LENGTH, new Integer(12) }, + { "8018", new Integer(18) }, + { "8020", VARIABLE_LENGTH, new Integer(25) }, + { "8100", new Integer(6) }, + { "8101", new Integer(10) }, + { "8102", new Integer(2) }, + { "8110", VARIABLE_LENGTH, new Integer(30) }, + }; + + private FieldParser() { + } + + static String parseFieldsInGeneralPurpose(String rawInformation) throws NotFoundException{ + if(rawInformation.length() == 0) { + return ""; + } + + // Processing 2-digit AIs + + if(rawInformation.length() < 2) { + throw NotFoundException.getNotFoundInstance(); + } + + String firstTwoDigits = rawInformation.substring(0, 2); + + for (int i=0; i+ * This class parses the BitMatrix image into codewords. + *
+ * + * @author SITA Lab (kevin.osullivan@sita.aero) + */ +final class BitMatrixParser { + + private static final int MAX_ROW_DIFFERENCE = 6; + private static final int MAX_ROWS = 90; + //private static final int MAX_COLUMNS = 30; + // Maximum Codewords (Data + Error) + private static final int MAX_CW_CAPACITY = 929; + private static final int MODULES_IN_SYMBOL = 17; + + private final BitMatrix bitMatrix; + private int rows = 0; + //private int columns = 0; + + private int leftColumnECData = 0; + private int rightColumnECData = 0; + private int eraseCount = 0; + private int[] erasures = null; + private int ecLevel = -1; + + BitMatrixParser(BitMatrix bitMatrix) { + this.bitMatrix = bitMatrix; + } + + /** + * To ensure separability of rows, codewords of consecutive rows belong to + * different subsets of all possible codewords. This routine scans the + * symbols in the barcode. When it finds a number of consecutive rows which + * are the same, it assumes that this is a row of codewords and processes + * them into a codeword array. + * + * @return an array of codewords. + */ + int[] readCodewords() throws FormatException { + int width = bitMatrix.getWidth(); + // TODO should be a rectangular matrix + int height = width; + + erasures = new int[MAX_CW_CAPACITY]; + + // Get the number of pixels in a module across the X dimension + //float moduleWidth = bitMatrix.getModuleWidth(); + float moduleWidth = 1.0f; // Image has been sampled and reduced + + int[] rowCounters = new int[width]; + int[] codewords = new int[MAX_CW_CAPACITY]; + int next = 0; + int matchingConsecutiveScans = 0; + boolean rowInProgress = false; + int rowNumber = 0; + int rowHeight = 0; + for (int i = 1; i < height; i++) { + if (rowNumber >= MAX_ROWS) { + // Something is wrong, since we have exceeded + // the maximum rows in the specification. + // TODO Maybe return error code + return null; + } + int rowDifference = 0; + // Scan a line of modules and check the + // difference between this and the previous line + for (int j = 0; j < width; j++) { + // Accumulate differences between this line and the + // previous line. + if (bitMatrix.get(j, i) != bitMatrix.get(j, i - 1)) { + rowDifference++; + } + } + if (rowDifference <= moduleWidth * MAX_ROW_DIFFERENCE) { + for (int j = 0; j < width; j++) { + // Accumulate the black pixels on this line + if (bitMatrix.get(j, i)) { + rowCounters[j]++; + } + } + // Increment the number of consecutive rows of pixels + // that are more or less the same + matchingConsecutiveScans++; + // Height of a row is a multiple of the module size in pixels + // Usually at least 3 times the module size + if (matchingConsecutiveScans >= moduleWidth * 2) { // MGMG + // We have some previous matches as well as a match here + // Set processing a unique row. + rowInProgress = true; + } + } else { + if (rowInProgress) { + // Process Row + next = processRow(rowCounters, rowNumber, rowHeight, codewords, next); + if (next == -1) { + // Something is wrong, since we have exceeded + // the maximum columns in the specification. + // TODO Maybe return error code + return null; + } + // Reinitialize the row counters. + for (int j = 0; j < rowCounters.length; j++) { + rowCounters[j] = 0; + } + rowNumber++; + rowHeight = 0; + } + matchingConsecutiveScans = 0; + rowInProgress = false; + } + rowHeight++; + } + // Check for a row that was in progress before we exited above. + if (rowInProgress) { + // Process Row + if (rowNumber >= MAX_ROWS) { + // Something is wrong, since we have exceeded + // the maximum rows in the specification. + // TODO Maybe return error code + return null; + } + next = processRow(rowCounters, rowNumber, rowHeight, codewords, next); + rowNumber++; + rows = rowNumber; + } + erasures = trimArray(erasures, eraseCount); + return trimArray(codewords, next); + } + + /** + * Trim the array to the required size. + * + * @param array the array + * @param size the size to trim it to + * @return the new trimmed array + */ + private static int[] trimArray(int[] array, int size) { + if (size > 0) { + int[] a = new int[size]; + for (int i = 0; i < size; i++) { + a[i] = array[i]; + } + return a; + } else { + return null; + } + } + + /** + * Convert the symbols in the row to codewords. + * Each PDF417 symbol character consists of four bar elements and four space + * elements, each of which can be one to six modules wide. The four bar and + * four space elements shall measure 17 modules in total. + * + * @param rowCounters an array containing the counts of black pixels for each column + * in the row. + * @param rowNumber the current row number of codewords. + * @param rowHeight the height of this row in pixels. + * @param codewords the codeword array to save codewords into. + * @param next the next available index into the codewords array. + * @return the next available index into the codeword array after processing + * this row. + */ + int processRow(int[] rowCounters, int rowNumber, int rowHeight, int[] codewords, int next) + throws FormatException { + int width = bitMatrix.getWidth(); + int columnNumber = 0; + long symbol = 0; + for (int i = 0; i < width; i += MODULES_IN_SYMBOL) { + // This happens in real life and is almost surely a rare misdecode + if (i + MODULES_IN_SYMBOL > rowCounters.length) { + throw FormatException.getFormatInstance(); + } + for (int mask = MODULES_IN_SYMBOL - 1; mask >= 0; mask--) { + if (rowCounters[i + (MODULES_IN_SYMBOL - 1 - mask)] >= rowHeight >>> 1) { + symbol |= 1L << mask; + } + } + if (columnNumber > 0) { + int cw = getCodeword(symbol); + // if (debug) System.out.println(" " + Long.toBinaryString(symbol) + + // " cw=" +cw + " ColumnNumber=" +columnNumber + "i=" +i); + if (cw < 0 && i < width - MODULES_IN_SYMBOL) { + // Skip errors on the Right row indicator column + erasures[eraseCount] = next; + next++; + eraseCount++; + } else { + codewords[next++] = cw; + } + } else { + // Left row indicator column + int cw = getCodeword(symbol); + // if (debug) System.out.println(" " + Long.toBinaryString(symbol) + + // " cw=" +cw + " ColumnNumber=" +columnNumber + "i=" +i); + if (ecLevel < 0) { + switch (rowNumber % 3) { + case 0: + break; + case 1: + leftColumnECData = cw; + break; + case 2: + break; + } + } + } + symbol = 0; + //columns = columnNumber; + columnNumber++; + } + if (columnNumber > 1) { + // Right row indicator column is in codeword[next] + //columns--; + // Overwrite the last codeword i.e. Right Row Indicator + --next; + if (ecLevel < 0) { + switch (rowNumber % 3) { + case 0: + break; + case 1: + break; + case 2: + rightColumnECData = codewords[next]; + if (rightColumnECData == leftColumnECData + && leftColumnECData != 0) { + ecLevel = ((rightColumnECData % 30) - rows % 3) / 3; + } + break; + } + } + codewords[next] = 0; + } + return next; + } + + /** + * Build a symbol from the pixels. + * Each symbol character is defined by an 8-digit bar-space sequence which + * represents the module widths of the eight elements of that symbol + * character. + * + * @param counters array of pixel counter corresponding to each Bar/Space pattern. + * @return the symbol + */ + /* + private static long getSymbol(int[] counters, float moduleWidth) { + int pixelsInSymbol = 0; + for (int j = 0; j < counters.length; j++) { + pixelsInSymbol += counters[j]; + } + float avgModuleWidth = (pixelsInSymbol / 17.0f); + boolean toggle = true; + int shift = 0; + int symbol = 0; + for (int j = 0; j < counters.length; j++) { + if (counters[j] < moduleWidth && counters[j] > 0) { + // Give a very narrow bar/space a chance + counters[j] = (int) moduleWidth; + } + // Calculate number of modules in the symbol. + // int modules = (int)(counters[j]/moduleWidth); + // int modules = round(counters[j]/moduleWidth); + int modules = round(counters[j] / avgModuleWidth); + if (modules > 6) { + // Maximum size is 6 modules + modules = 6; + } else if (modules < 1) { + modules = 1; + } + if (toggle) { + for (int k = 0; k < modules; k++) { + symbol |= 1 << (16 - k - shift); + } + toggle = false; + } else { + toggle = true; + } + shift += modules; + } + return symbol; + } + */ + + /** + * Translate the symbol into a codeword. + * + * @param symbol + * @return the codeword corresponding to the symbol. + */ + private static int getCodeword(long symbol) { + long sym = symbol; + sym &= 0x3ffff; + int i = findCodewordIndex(sym); + if (i == -1) { + return -1; + } else { + long cw = CODEWORD_TABLE[i] - 1; + cw %= 929; + return (int) cw; + } + } + + /** + * Use a binary search to find the index of the codeword corresponding to + * this symbol. + * + * @param symbol the symbol from the barcode. + * @return the index into the codeword table. + */ + private static int findCodewordIndex(long symbol) { + int first = 0; + int upto = SYMBOL_TABLE.length; + while (first < upto) { + int mid = (first + upto) >>> 1; // Compute mid point. + if (symbol < SYMBOL_TABLE[mid]) { + upto = mid; // repeat search in bottom half. + } else if (symbol > SYMBOL_TABLE[mid]) { + first = mid + 1; // Repeat search in top half. + } else { + return mid; // Found it. return position + } + } + return -1; + // if (debug) System.out.println("Failed to find codeword for Symbol=" + + // symbol); + } + + /** + * Ends up being a bit faster than Math.round(). This merely rounds its + * argument to the nearest int, where x.5 rounds up. + */ + /* + private static int round(float d) { + return (int) (d + 0.5f); + } + */ + + /** + * Returns an array of locations representing the erasures. + */ + public int[] getErasures() { + return erasures; + } + + public int getECLevel() { + return ecLevel; + } + + /** + * Convert the symbols in the row to codewords. + * Each PDF417 symbol character consists of four bar elements and four space + * elements, each of which can be one to six modules wide. The four bar and + * four space elements shall measure 17 modules in total. + * + * @param rowCounters an array containing the counts of black pixels for each column + * in the row. + * @param rowNumber the current row number of codewords. + * @param rowHeight the height of this row in pixels. + * @param moduleWidth the size of a module in pixels. + * @param codewords the codeword array to save codewords into. + * @param next the next available index into the codewords array. + * @return the next available index into the codeword array after processing + * this row. + * @throws NotFoundException + */ + /* + int processRow1(int[] rowCounters, int rowNumber, int rowHeight, + float moduleWidth, int[] codewords, int next) { + int width = bitMatrix.getDimension(); + int firstBlack = 0; + + for (firstBlack = 0; firstBlack < width; firstBlack++) { + // Step forward until we find the first black pixels + if (rowCounters[firstBlack] >= rowHeight >>> 1) { + break; + } + } + + int[] counters = new int[8]; + int state = 0; // In black pixels, looking for white, first or second time + long symbol = 0; + int columnNumber = 0; + for (int i = firstBlack; i < width; i++) { + if (state == 1 || state == 3 || state == 5 || state == 7) { // In white + // pixels, + // looking + // for + // black + // If more than half the column is black + if (rowCounters[i] >= rowHeight >>> 1 || i == width - 1) { + if (i == width - 1) { + counters[state]++; + } + // In black pixels or the end of a row + state++; + if (state < 8) { + // Don't count the last one + counters[state]++; + } + } else { + counters[state]++; + } + } else { + if (rowCounters[i] < rowHeight >>> 1) { + // Found white pixels + state++; + if (state == 7 && i == width - 1) { + // Its found white pixels at the end of the row, + // give it a chance to exit gracefully + i--; + } else { + // Found white pixels + counters[state]++; + } + } else { + if (state < 8) { + // Still in black pixels + counters[state]++; + } + } + } + if (state == 8) { // Found black, white, black, white, black, white, + // black, white and stumbled back onto black; done + if (columnNumber >= MAX_COLUMNS) { + // Something is wrong, since we have exceeded + // the maximum columns in the specification. + // TODO Maybe return error code + return -1; + } + if (columnNumber > 0) { + symbol = getSymbol(counters, moduleWidth); + int cw = getCodeword(symbol); + // if (debug) System.out.println(" " + + // Long.toBinaryString(symbol) + " cw=" +cw + " ColumnNumber=" + // +columnNumber + "i=" +i); + if (cw < 0) { + erasures[eraseCount] = next; + next++; + eraseCount++; + } else { + codewords[next++] = cw; + } + } else { + // Left row indicator column + symbol = getSymbol(counters, moduleWidth); + int cw = getCodeword(symbol); + if (ecLevel < 0) { + switch (rowNumber % 3) { + case 0: + break; + case 1: + leftColumnECData = cw; + break; + case 2: + break; + } + } + } + // Step back so that this pixel can be examined during the next + // pass. + i--; + counters = new int[8]; + columns = columnNumber; + columnNumber++; + // Introduce some errors if (rowNumber == 0 && columnNumber == 4) + // { codewords[next-1] = 0; erasures[eraseCount] = next-1; + // eraseCount++; } if (rowNumber == 0 && columnNumber == 6) { + // codewords[next-1] = 10; erasures[eraseCount] = next-1; + // eraseCount++; } if (rowNumber == 0 && columnNumber == 8) { + // codewords[next-1] = 10; erasures[eraseCount] = next-1; + // eraseCount++; } + state = 0; + symbol = 0; + } + } + if (columnNumber > 1) { + // Right row indicator column is in codeword[next] + columns--; + // Overwrite the last codeword i.e. Right Row Indicator + --next; + if (ecLevel < 0) { + switch (rowNumber % 3) { + case 0: + break; + case 1: + break; + case 2: + rightColumnECData = codewords[next]; + if (rightColumnECData == leftColumnECData + && leftColumnECData != 0) { + ecLevel = ((rightColumnECData % 30) - rows % 3) / 3; + } + break; + } + } + codewords[next] = 0; + } + return next; + } + */ + + /** + * The sorted table of all possible symbols. Extracted from the PDF417 + * specification. The index of a symbol in this table corresponds to the + * index into the codeword table. + */ + private static final int[] SYMBOL_TABLE = {0x1025e, 0x1027a, 0x1029e, + 0x102bc, 0x102f2, 0x102f4, 0x1032e, 0x1034e, 0x1035c, 0x10396, + 0x103a6, 0x103ac, 0x10422, 0x10428, 0x10436, 0x10442, 0x10444, + 0x10448, 0x10450, 0x1045e, 0x10466, 0x1046c, 0x1047a, 0x10482, + 0x1049e, 0x104a0, 0x104bc, 0x104c6, 0x104d8, 0x104ee, 0x104f2, + 0x104f4, 0x10504, 0x10508, 0x10510, 0x1051e, 0x10520, 0x1053c, + 0x10540, 0x10578, 0x10586, 0x1058c, 0x10598, 0x105b0, 0x105be, + 0x105ce, 0x105dc, 0x105e2, 0x105e4, 0x105e8, 0x105f6, 0x1062e, + 0x1064e, 0x1065c, 0x1068e, 0x1069c, 0x106b8, 0x106de, 0x106fa, + 0x10716, 0x10726, 0x1072c, 0x10746, 0x1074c, 0x10758, 0x1076e, + 0x10792, 0x10794, 0x107a2, 0x107a4, 0x107a8, 0x107b6, 0x10822, + 0x10828, 0x10842, 0x10848, 0x10850, 0x1085e, 0x10866, 0x1086c, + 0x1087a, 0x10882, 0x10884, 0x10890, 0x1089e, 0x108a0, 0x108bc, + 0x108c6, 0x108cc, 0x108d8, 0x108ee, 0x108f2, 0x108f4, 0x10902, + 0x10908, 0x1091e, 0x10920, 0x1093c, 0x10940, 0x10978, 0x10986, + 0x10998, 0x109b0, 0x109be, 0x109ce, 0x109dc, 0x109e2, 0x109e4, + 0x109e8, 0x109f6, 0x10a08, 0x10a10, 0x10a1e, 0x10a20, 0x10a3c, + 0x10a40, 0x10a78, 0x10af0, 0x10b06, 0x10b0c, 0x10b18, 0x10b30, + 0x10b3e, 0x10b60, 0x10b7c, 0x10b8e, 0x10b9c, 0x10bb8, 0x10bc2, + 0x10bc4, 0x10bc8, 0x10bd0, 0x10bde, 0x10be6, 0x10bec, 0x10c2e, + 0x10c4e, 0x10c5c, 0x10c62, 0x10c64, 0x10c68, 0x10c76, 0x10c8e, + 0x10c9c, 0x10cb8, 0x10cc2, 0x10cc4, 0x10cc8, 0x10cd0, 0x10cde, + 0x10ce6, 0x10cec, 0x10cfa, 0x10d0e, 0x10d1c, 0x10d38, 0x10d70, + 0x10d7e, 0x10d82, 0x10d84, 0x10d88, 0x10d90, 0x10d9e, 0x10da0, + 0x10dbc, 0x10dc6, 0x10dcc, 0x10dd8, 0x10dee, 0x10df2, 0x10df4, + 0x10e16, 0x10e26, 0x10e2c, 0x10e46, 0x10e58, 0x10e6e, 0x10e86, + 0x10e8c, 0x10e98, 0x10eb0, 0x10ebe, 0x10ece, 0x10edc, 0x10f0a, + 0x10f12, 0x10f14, 0x10f22, 0x10f28, 0x10f36, 0x10f42, 0x10f44, + 0x10f48, 0x10f50, 0x10f5e, 0x10f66, 0x10f6c, 0x10fb2, 0x10fb4, + 0x11022, 0x11028, 0x11042, 0x11048, 0x11050, 0x1105e, 0x1107a, + 0x11082, 0x11084, 0x11090, 0x1109e, 0x110a0, 0x110bc, 0x110c6, + 0x110cc, 0x110d8, 0x110ee, 0x110f2, 0x110f4, 0x11102, 0x1111e, + 0x11120, 0x1113c, 0x11140, 0x11178, 0x11186, 0x11198, 0x111b0, + 0x111be, 0x111ce, 0x111dc, 0x111e2, 0x111e4, 0x111e8, 0x111f6, + 0x11208, 0x1121e, 0x11220, 0x11278, 0x112f0, 0x1130c, 0x11330, + 0x1133e, 0x11360, 0x1137c, 0x1138e, 0x1139c, 0x113b8, 0x113c2, + 0x113c8, 0x113d0, 0x113de, 0x113e6, 0x113ec, 0x11408, 0x11410, + 0x1141e, 0x11420, 0x1143c, 0x11440, 0x11478, 0x114f0, 0x115e0, + 0x1160c, 0x11618, 0x11630, 0x1163e, 0x11660, 0x1167c, 0x116c0, + 0x116f8, 0x1171c, 0x11738, 0x11770, 0x1177e, 0x11782, 0x11784, + 0x11788, 0x11790, 0x1179e, 0x117a0, 0x117bc, 0x117c6, 0x117cc, + 0x117d8, 0x117ee, 0x1182e, 0x11834, 0x1184e, 0x1185c, 0x11862, + 0x11864, 0x11868, 0x11876, 0x1188e, 0x1189c, 0x118b8, 0x118c2, + 0x118c8, 0x118d0, 0x118de, 0x118e6, 0x118ec, 0x118fa, 0x1190e, + 0x1191c, 0x11938, 0x11970, 0x1197e, 0x11982, 0x11984, 0x11990, + 0x1199e, 0x119a0, 0x119bc, 0x119c6, 0x119cc, 0x119d8, 0x119ee, + 0x119f2, 0x119f4, 0x11a0e, 0x11a1c, 0x11a38, 0x11a70, 0x11a7e, + 0x11ae0, 0x11afc, 0x11b08, 0x11b10, 0x11b1e, 0x11b20, 0x11b3c, + 0x11b40, 0x11b78, 0x11b8c, 0x11b98, 0x11bb0, 0x11bbe, 0x11bce, + 0x11bdc, 0x11be2, 0x11be4, 0x11be8, 0x11bf6, 0x11c16, 0x11c26, + 0x11c2c, 0x11c46, 0x11c4c, 0x11c58, 0x11c6e, 0x11c86, 0x11c98, + 0x11cb0, 0x11cbe, 0x11cce, 0x11cdc, 0x11ce2, 0x11ce4, 0x11ce8, + 0x11cf6, 0x11d06, 0x11d0c, 0x11d18, 0x11d30, 0x11d3e, 0x11d60, + 0x11d7c, 0x11d8e, 0x11d9c, 0x11db8, 0x11dc4, 0x11dc8, 0x11dd0, + 0x11dde, 0x11de6, 0x11dec, 0x11dfa, 0x11e0a, 0x11e12, 0x11e14, + 0x11e22, 0x11e24, 0x11e28, 0x11e36, 0x11e42, 0x11e44, 0x11e50, + 0x11e5e, 0x11e66, 0x11e6c, 0x11e82, 0x11e84, 0x11e88, 0x11e90, + 0x11e9e, 0x11ea0, 0x11ebc, 0x11ec6, 0x11ecc, 0x11ed8, 0x11eee, + 0x11f1a, 0x11f2e, 0x11f32, 0x11f34, 0x11f4e, 0x11f5c, 0x11f62, + 0x11f64, 0x11f68, 0x11f76, 0x12048, 0x1205e, 0x12082, 0x12084, + 0x12090, 0x1209e, 0x120a0, 0x120bc, 0x120d8, 0x120f2, 0x120f4, + 0x12108, 0x1211e, 0x12120, 0x1213c, 0x12140, 0x12178, 0x12186, + 0x12198, 0x121b0, 0x121be, 0x121e2, 0x121e4, 0x121e8, 0x121f6, + 0x12204, 0x12210, 0x1221e, 0x12220, 0x12278, 0x122f0, 0x12306, + 0x1230c, 0x12330, 0x1233e, 0x12360, 0x1237c, 0x1238e, 0x1239c, + 0x123b8, 0x123c2, 0x123c8, 0x123d0, 0x123e6, 0x123ec, 0x1241e, + 0x12420, 0x1243c, 0x124f0, 0x125e0, 0x12618, 0x1263e, 0x12660, + 0x1267c, 0x126c0, 0x126f8, 0x12738, 0x12770, 0x1277e, 0x12782, + 0x12784, 0x12790, 0x1279e, 0x127a0, 0x127bc, 0x127c6, 0x127cc, + 0x127d8, 0x127ee, 0x12820, 0x1283c, 0x12840, 0x12878, 0x128f0, + 0x129e0, 0x12bc0, 0x12c18, 0x12c30, 0x12c3e, 0x12c60, 0x12c7c, + 0x12cc0, 0x12cf8, 0x12df0, 0x12e1c, 0x12e38, 0x12e70, 0x12e7e, + 0x12ee0, 0x12efc, 0x12f04, 0x12f08, 0x12f10, 0x12f20, 0x12f3c, + 0x12f40, 0x12f78, 0x12f86, 0x12f8c, 0x12f98, 0x12fb0, 0x12fbe, + 0x12fce, 0x12fdc, 0x1302e, 0x1304e, 0x1305c, 0x13062, 0x13068, + 0x1308e, 0x1309c, 0x130b8, 0x130c2, 0x130c8, 0x130d0, 0x130de, + 0x130ec, 0x130fa, 0x1310e, 0x13138, 0x13170, 0x1317e, 0x13182, + 0x13184, 0x13190, 0x1319e, 0x131a0, 0x131bc, 0x131c6, 0x131cc, + 0x131d8, 0x131f2, 0x131f4, 0x1320e, 0x1321c, 0x13270, 0x1327e, + 0x132e0, 0x132fc, 0x13308, 0x1331e, 0x13320, 0x1333c, 0x13340, + 0x13378, 0x13386, 0x13398, 0x133b0, 0x133be, 0x133ce, 0x133dc, + 0x133e2, 0x133e4, 0x133e8, 0x133f6, 0x1340e, 0x1341c, 0x13438, + 0x13470, 0x1347e, 0x134e0, 0x134fc, 0x135c0, 0x135f8, 0x13608, + 0x13610, 0x1361e, 0x13620, 0x1363c, 0x13640, 0x13678, 0x136f0, + 0x1370c, 0x13718, 0x13730, 0x1373e, 0x13760, 0x1377c, 0x1379c, + 0x137b8, 0x137c2, 0x137c4, 0x137c8, 0x137d0, 0x137de, 0x137e6, + 0x137ec, 0x13816, 0x13826, 0x1382c, 0x13846, 0x1384c, 0x13858, + 0x1386e, 0x13874, 0x13886, 0x13898, 0x138b0, 0x138be, 0x138ce, + 0x138dc, 0x138e2, 0x138e4, 0x138e8, 0x13906, 0x1390c, 0x13930, + 0x1393e, 0x13960, 0x1397c, 0x1398e, 0x1399c, 0x139b8, 0x139c8, + 0x139d0, 0x139de, 0x139e6, 0x139ec, 0x139fa, 0x13a06, 0x13a0c, + 0x13a18, 0x13a30, 0x13a3e, 0x13a60, 0x13a7c, 0x13ac0, 0x13af8, + 0x13b0e, 0x13b1c, 0x13b38, 0x13b70, 0x13b7e, 0x13b88, 0x13b90, + 0x13b9e, 0x13ba0, 0x13bbc, 0x13bcc, 0x13bd8, 0x13bee, 0x13bf2, + 0x13bf4, 0x13c12, 0x13c14, 0x13c22, 0x13c24, 0x13c28, 0x13c36, + 0x13c42, 0x13c48, 0x13c50, 0x13c5e, 0x13c66, 0x13c6c, 0x13c82, + 0x13c84, 0x13c90, 0x13c9e, 0x13ca0, 0x13cbc, 0x13cc6, 0x13ccc, + 0x13cd8, 0x13cee, 0x13d02, 0x13d04, 0x13d08, 0x13d10, 0x13d1e, + 0x13d20, 0x13d3c, 0x13d40, 0x13d78, 0x13d86, 0x13d8c, 0x13d98, + 0x13db0, 0x13dbe, 0x13dce, 0x13ddc, 0x13de4, 0x13de8, 0x13df6, + 0x13e1a, 0x13e2e, 0x13e32, 0x13e34, 0x13e4e, 0x13e5c, 0x13e62, + 0x13e64, 0x13e68, 0x13e76, 0x13e8e, 0x13e9c, 0x13eb8, 0x13ec2, + 0x13ec4, 0x13ec8, 0x13ed0, 0x13ede, 0x13ee6, 0x13eec, 0x13f26, + 0x13f2c, 0x13f3a, 0x13f46, 0x13f4c, 0x13f58, 0x13f6e, 0x13f72, + 0x13f74, 0x14082, 0x1409e, 0x140a0, 0x140bc, 0x14104, 0x14108, + 0x14110, 0x1411e, 0x14120, 0x1413c, 0x14140, 0x14178, 0x1418c, + 0x14198, 0x141b0, 0x141be, 0x141e2, 0x141e4, 0x141e8, 0x14208, + 0x14210, 0x1421e, 0x14220, 0x1423c, 0x14240, 0x14278, 0x142f0, + 0x14306, 0x1430c, 0x14318, 0x14330, 0x1433e, 0x14360, 0x1437c, + 0x1438e, 0x143c2, 0x143c4, 0x143c8, 0x143d0, 0x143e6, 0x143ec, + 0x14408, 0x14410, 0x1441e, 0x14420, 0x1443c, 0x14440, 0x14478, + 0x144f0, 0x145e0, 0x1460c, 0x14618, 0x14630, 0x1463e, 0x14660, + 0x1467c, 0x146c0, 0x146f8, 0x1471c, 0x14738, 0x14770, 0x1477e, + 0x14782, 0x14784, 0x14788, 0x14790, 0x147a0, 0x147bc, 0x147c6, + 0x147cc, 0x147d8, 0x147ee, 0x14810, 0x14820, 0x1483c, 0x14840, + 0x14878, 0x148f0, 0x149e0, 0x14bc0, 0x14c30, 0x14c3e, 0x14c60, + 0x14c7c, 0x14cc0, 0x14cf8, 0x14df0, 0x14e38, 0x14e70, 0x14e7e, + 0x14ee0, 0x14efc, 0x14f04, 0x14f08, 0x14f10, 0x14f1e, 0x14f20, + 0x14f3c, 0x14f40, 0x14f78, 0x14f86, 0x14f8c, 0x14f98, 0x14fb0, + 0x14fce, 0x14fdc, 0x15020, 0x15040, 0x15078, 0x150f0, 0x151e0, + 0x153c0, 0x15860, 0x1587c, 0x158c0, 0x158f8, 0x159f0, 0x15be0, + 0x15c70, 0x15c7e, 0x15ce0, 0x15cfc, 0x15dc0, 0x15df8, 0x15e08, + 0x15e10, 0x15e20, 0x15e40, 0x15e78, 0x15ef0, 0x15f0c, 0x15f18, + 0x15f30, 0x15f60, 0x15f7c, 0x15f8e, 0x15f9c, 0x15fb8, 0x1604e, + 0x1605c, 0x1608e, 0x1609c, 0x160b8, 0x160c2, 0x160c4, 0x160c8, + 0x160de, 0x1610e, 0x1611c, 0x16138, 0x16170, 0x1617e, 0x16184, + 0x16188, 0x16190, 0x1619e, 0x161a0, 0x161bc, 0x161c6, 0x161cc, + 0x161d8, 0x161f2, 0x161f4, 0x1620e, 0x1621c, 0x16238, 0x16270, + 0x1627e, 0x162e0, 0x162fc, 0x16304, 0x16308, 0x16310, 0x1631e, + 0x16320, 0x1633c, 0x16340, 0x16378, 0x16386, 0x1638c, 0x16398, + 0x163b0, 0x163be, 0x163ce, 0x163dc, 0x163e2, 0x163e4, 0x163e8, + 0x163f6, 0x1640e, 0x1641c, 0x16438, 0x16470, 0x1647e, 0x164e0, + 0x164fc, 0x165c0, 0x165f8, 0x16610, 0x1661e, 0x16620, 0x1663c, + 0x16640, 0x16678, 0x166f0, 0x16718, 0x16730, 0x1673e, 0x16760, + 0x1677c, 0x1678e, 0x1679c, 0x167b8, 0x167c2, 0x167c4, 0x167c8, + 0x167d0, 0x167de, 0x167e6, 0x167ec, 0x1681c, 0x16838, 0x16870, + 0x168e0, 0x168fc, 0x169c0, 0x169f8, 0x16bf0, 0x16c10, 0x16c1e, + 0x16c20, 0x16c3c, 0x16c40, 0x16c78, 0x16cf0, 0x16de0, 0x16e18, + 0x16e30, 0x16e3e, 0x16e60, 0x16e7c, 0x16ec0, 0x16ef8, 0x16f1c, + 0x16f38, 0x16f70, 0x16f7e, 0x16f84, 0x16f88, 0x16f90, 0x16f9e, + 0x16fa0, 0x16fbc, 0x16fc6, 0x16fcc, 0x16fd8, 0x17026, 0x1702c, + 0x17046, 0x1704c, 0x17058, 0x1706e, 0x17086, 0x1708c, 0x17098, + 0x170b0, 0x170be, 0x170ce, 0x170dc, 0x170e8, 0x17106, 0x1710c, + 0x17118, 0x17130, 0x1713e, 0x17160, 0x1717c, 0x1718e, 0x1719c, + 0x171b8, 0x171c2, 0x171c4, 0x171c8, 0x171d0, 0x171de, 0x171e6, + 0x171ec, 0x171fa, 0x17206, 0x1720c, 0x17218, 0x17230, 0x1723e, + 0x17260, 0x1727c, 0x172c0, 0x172f8, 0x1730e, 0x1731c, 0x17338, + 0x17370, 0x1737e, 0x17388, 0x17390, 0x1739e, 0x173a0, 0x173bc, + 0x173cc, 0x173d8, 0x173ee, 0x173f2, 0x173f4, 0x1740c, 0x17418, + 0x17430, 0x1743e, 0x17460, 0x1747c, 0x174c0, 0x174f8, 0x175f0, + 0x1760e, 0x1761c, 0x17638, 0x17670, 0x1767e, 0x176e0, 0x176fc, + 0x17708, 0x17710, 0x1771e, 0x17720, 0x1773c, 0x17740, 0x17778, + 0x17798, 0x177b0, 0x177be, 0x177dc, 0x177e2, 0x177e4, 0x177e8, + 0x17822, 0x17824, 0x17828, 0x17836, 0x17842, 0x17844, 0x17848, + 0x17850, 0x1785e, 0x17866, 0x1786c, 0x17882, 0x17884, 0x17888, + 0x17890, 0x1789e, 0x178a0, 0x178bc, 0x178c6, 0x178cc, 0x178d8, + 0x178ee, 0x178f2, 0x178f4, 0x17902, 0x17904, 0x17908, 0x17910, + 0x1791e, 0x17920, 0x1793c, 0x17940, 0x17978, 0x17986, 0x1798c, + 0x17998, 0x179b0, 0x179be, 0x179ce, 0x179dc, 0x179e2, 0x179e4, + 0x179e8, 0x179f6, 0x17a04, 0x17a08, 0x17a10, 0x17a1e, 0x17a20, + 0x17a3c, 0x17a40, 0x17a78, 0x17af0, 0x17b06, 0x17b0c, 0x17b18, + 0x17b30, 0x17b3e, 0x17b60, 0x17b7c, 0x17b8e, 0x17b9c, 0x17bb8, + 0x17bc4, 0x17bc8, 0x17bd0, 0x17bde, 0x17be6, 0x17bec, 0x17c2e, + 0x17c32, 0x17c34, 0x17c4e, 0x17c5c, 0x17c62, 0x17c64, 0x17c68, + 0x17c76, 0x17c8e, 0x17c9c, 0x17cb8, 0x17cc2, 0x17cc4, 0x17cc8, + 0x17cd0, 0x17cde, 0x17ce6, 0x17cec, 0x17d0e, 0x17d1c, 0x17d38, + 0x17d70, 0x17d82, 0x17d84, 0x17d88, 0x17d90, 0x17d9e, 0x17da0, + 0x17dbc, 0x17dc6, 0x17dcc, 0x17dd8, 0x17dee, 0x17e26, 0x17e2c, + 0x17e3a, 0x17e46, 0x17e4c, 0x17e58, 0x17e6e, 0x17e72, 0x17e74, + 0x17e86, 0x17e8c, 0x17e98, 0x17eb0, 0x17ece, 0x17edc, 0x17ee2, + 0x17ee4, 0x17ee8, 0x17ef6, 0x1813a, 0x18172, 0x18174, 0x18216, + 0x18226, 0x1823a, 0x1824c, 0x18258, 0x1826e, 0x18272, 0x18274, + 0x18298, 0x182be, 0x182e2, 0x182e4, 0x182e8, 0x182f6, 0x1835e, + 0x1837a, 0x183ae, 0x183d6, 0x18416, 0x18426, 0x1842c, 0x1843a, + 0x18446, 0x18458, 0x1846e, 0x18472, 0x18474, 0x18486, 0x184b0, + 0x184be, 0x184ce, 0x184dc, 0x184e2, 0x184e4, 0x184e8, 0x184f6, + 0x18506, 0x1850c, 0x18518, 0x18530, 0x1853e, 0x18560, 0x1857c, + 0x1858e, 0x1859c, 0x185b8, 0x185c2, 0x185c4, 0x185c8, 0x185d0, + 0x185de, 0x185e6, 0x185ec, 0x185fa, 0x18612, 0x18614, 0x18622, + 0x18628, 0x18636, 0x18642, 0x18650, 0x1865e, 0x1867a, 0x18682, + 0x18684, 0x18688, 0x18690, 0x1869e, 0x186a0, 0x186bc, 0x186c6, + 0x186cc, 0x186d8, 0x186ee, 0x186f2, 0x186f4, 0x1872e, 0x1874e, + 0x1875c, 0x18796, 0x187a6, 0x187ac, 0x187d2, 0x187d4, 0x18826, + 0x1882c, 0x1883a, 0x18846, 0x1884c, 0x18858, 0x1886e, 0x18872, + 0x18874, 0x18886, 0x18898, 0x188b0, 0x188be, 0x188ce, 0x188dc, + 0x188e2, 0x188e4, 0x188e8, 0x188f6, 0x1890c, 0x18930, 0x1893e, + 0x18960, 0x1897c, 0x1898e, 0x189b8, 0x189c2, 0x189c8, 0x189d0, + 0x189de, 0x189e6, 0x189ec, 0x189fa, 0x18a18, 0x18a30, 0x18a3e, + 0x18a60, 0x18a7c, 0x18ac0, 0x18af8, 0x18b1c, 0x18b38, 0x18b70, + 0x18b7e, 0x18b82, 0x18b84, 0x18b88, 0x18b90, 0x18b9e, 0x18ba0, + 0x18bbc, 0x18bc6, 0x18bcc, 0x18bd8, 0x18bee, 0x18bf2, 0x18bf4, + 0x18c22, 0x18c24, 0x18c28, 0x18c36, 0x18c42, 0x18c48, 0x18c50, + 0x18c5e, 0x18c66, 0x18c7a, 0x18c82, 0x18c84, 0x18c90, 0x18c9e, + 0x18ca0, 0x18cbc, 0x18ccc, 0x18cf2, 0x18cf4, 0x18d04, 0x18d08, + 0x18d10, 0x18d1e, 0x18d20, 0x18d3c, 0x18d40, 0x18d78, 0x18d86, + 0x18d98, 0x18dce, 0x18de2, 0x18de4, 0x18de8, 0x18e2e, 0x18e32, + 0x18e34, 0x18e4e, 0x18e5c, 0x18e62, 0x18e64, 0x18e68, 0x18e8e, + 0x18e9c, 0x18eb8, 0x18ec2, 0x18ec4, 0x18ec8, 0x18ed0, 0x18efa, + 0x18f16, 0x18f26, 0x18f2c, 0x18f46, 0x18f4c, 0x18f58, 0x18f6e, + 0x18f8a, 0x18f92, 0x18f94, 0x18fa2, 0x18fa4, 0x18fa8, 0x18fb6, + 0x1902c, 0x1903a, 0x19046, 0x1904c, 0x19058, 0x19072, 0x19074, + 0x19086, 0x19098, 0x190b0, 0x190be, 0x190ce, 0x190dc, 0x190e2, + 0x190e8, 0x190f6, 0x19106, 0x1910c, 0x19130, 0x1913e, 0x19160, + 0x1917c, 0x1918e, 0x1919c, 0x191b8, 0x191c2, 0x191c8, 0x191d0, + 0x191de, 0x191e6, 0x191ec, 0x191fa, 0x19218, 0x1923e, 0x19260, + 0x1927c, 0x192c0, 0x192f8, 0x19338, 0x19370, 0x1937e, 0x19382, + 0x19384, 0x19390, 0x1939e, 0x193a0, 0x193bc, 0x193c6, 0x193cc, + 0x193d8, 0x193ee, 0x193f2, 0x193f4, 0x19430, 0x1943e, 0x19460, + 0x1947c, 0x194c0, 0x194f8, 0x195f0, 0x19638, 0x19670, 0x1967e, + 0x196e0, 0x196fc, 0x19702, 0x19704, 0x19708, 0x19710, 0x19720, + 0x1973c, 0x19740, 0x19778, 0x19786, 0x1978c, 0x19798, 0x197b0, + 0x197be, 0x197ce, 0x197dc, 0x197e2, 0x197e4, 0x197e8, 0x19822, + 0x19824, 0x19842, 0x19848, 0x19850, 0x1985e, 0x19866, 0x1987a, + 0x19882, 0x19884, 0x19890, 0x1989e, 0x198a0, 0x198bc, 0x198cc, + 0x198f2, 0x198f4, 0x19902, 0x19908, 0x1991e, 0x19920, 0x1993c, + 0x19940, 0x19978, 0x19986, 0x19998, 0x199ce, 0x199e2, 0x199e4, + 0x199e8, 0x19a08, 0x19a10, 0x19a1e, 0x19a20, 0x19a3c, 0x19a40, + 0x19a78, 0x19af0, 0x19b18, 0x19b3e, 0x19b60, 0x19b9c, 0x19bc2, + 0x19bc4, 0x19bc8, 0x19bd0, 0x19be6, 0x19c2e, 0x19c34, 0x19c4e, + 0x19c5c, 0x19c62, 0x19c64, 0x19c68, 0x19c8e, 0x19c9c, 0x19cb8, + 0x19cc2, 0x19cc8, 0x19cd0, 0x19ce6, 0x19cfa, 0x19d0e, 0x19d1c, + 0x19d38, 0x19d70, 0x19d7e, 0x19d82, 0x19d84, 0x19d88, 0x19d90, + 0x19da0, 0x19dcc, 0x19df2, 0x19df4, 0x19e16, 0x19e26, 0x19e2c, + 0x19e46, 0x19e4c, 0x19e58, 0x19e74, 0x19e86, 0x19e8c, 0x19e98, + 0x19eb0, 0x19ebe, 0x19ece, 0x19ee2, 0x19ee4, 0x19ee8, 0x19f0a, + 0x19f12, 0x19f14, 0x19f22, 0x19f24, 0x19f28, 0x19f42, 0x19f44, + 0x19f48, 0x19f50, 0x19f5e, 0x19f6c, 0x19f9a, 0x19fae, 0x19fb2, + 0x19fb4, 0x1a046, 0x1a04c, 0x1a072, 0x1a074, 0x1a086, 0x1a08c, + 0x1a098, 0x1a0b0, 0x1a0be, 0x1a0e2, 0x1a0e4, 0x1a0e8, 0x1a0f6, + 0x1a106, 0x1a10c, 0x1a118, 0x1a130, 0x1a13e, 0x1a160, 0x1a17c, + 0x1a18e, 0x1a19c, 0x1a1b8, 0x1a1c2, 0x1a1c4, 0x1a1c8, 0x1a1d0, + 0x1a1de, 0x1a1e6, 0x1a1ec, 0x1a218, 0x1a230, 0x1a23e, 0x1a260, + 0x1a27c, 0x1a2c0, 0x1a2f8, 0x1a31c, 0x1a338, 0x1a370, 0x1a37e, + 0x1a382, 0x1a384, 0x1a388, 0x1a390, 0x1a39e, 0x1a3a0, 0x1a3bc, + 0x1a3c6, 0x1a3cc, 0x1a3d8, 0x1a3ee, 0x1a3f2, 0x1a3f4, 0x1a418, + 0x1a430, 0x1a43e, 0x1a460, 0x1a47c, 0x1a4c0, 0x1a4f8, 0x1a5f0, + 0x1a61c, 0x1a638, 0x1a670, 0x1a67e, 0x1a6e0, 0x1a6fc, 0x1a702, + 0x1a704, 0x1a708, 0x1a710, 0x1a71e, 0x1a720, 0x1a73c, 0x1a740, + 0x1a778, 0x1a786, 0x1a78c, 0x1a798, 0x1a7b0, 0x1a7be, 0x1a7ce, + 0x1a7dc, 0x1a7e2, 0x1a7e4, 0x1a7e8, 0x1a830, 0x1a860, 0x1a87c, + 0x1a8c0, 0x1a8f8, 0x1a9f0, 0x1abe0, 0x1ac70, 0x1ac7e, 0x1ace0, + 0x1acfc, 0x1adc0, 0x1adf8, 0x1ae04, 0x1ae08, 0x1ae10, 0x1ae20, + 0x1ae3c, 0x1ae40, 0x1ae78, 0x1aef0, 0x1af06, 0x1af0c, 0x1af18, + 0x1af30, 0x1af3e, 0x1af60, 0x1af7c, 0x1af8e, 0x1af9c, 0x1afb8, + 0x1afc4, 0x1afc8, 0x1afd0, 0x1afde, 0x1b042, 0x1b05e, 0x1b07a, + 0x1b082, 0x1b084, 0x1b088, 0x1b090, 0x1b09e, 0x1b0a0, 0x1b0bc, + 0x1b0cc, 0x1b0f2, 0x1b0f4, 0x1b102, 0x1b104, 0x1b108, 0x1b110, + 0x1b11e, 0x1b120, 0x1b13c, 0x1b140, 0x1b178, 0x1b186, 0x1b198, + 0x1b1ce, 0x1b1e2, 0x1b1e4, 0x1b1e8, 0x1b204, 0x1b208, 0x1b210, + 0x1b21e, 0x1b220, 0x1b23c, 0x1b240, 0x1b278, 0x1b2f0, 0x1b30c, + 0x1b33e, 0x1b360, 0x1b39c, 0x1b3c2, 0x1b3c4, 0x1b3c8, 0x1b3d0, + 0x1b3e6, 0x1b410, 0x1b41e, 0x1b420, 0x1b43c, 0x1b440, 0x1b478, + 0x1b4f0, 0x1b5e0, 0x1b618, 0x1b660, 0x1b67c, 0x1b6c0, 0x1b738, + 0x1b782, 0x1b784, 0x1b788, 0x1b790, 0x1b79e, 0x1b7a0, 0x1b7cc, + 0x1b82e, 0x1b84e, 0x1b85c, 0x1b88e, 0x1b89c, 0x1b8b8, 0x1b8c2, + 0x1b8c4, 0x1b8c8, 0x1b8d0, 0x1b8e6, 0x1b8fa, 0x1b90e, 0x1b91c, + 0x1b938, 0x1b970, 0x1b97e, 0x1b982, 0x1b984, 0x1b988, 0x1b990, + 0x1b99e, 0x1b9a0, 0x1b9cc, 0x1b9f2, 0x1b9f4, 0x1ba0e, 0x1ba1c, + 0x1ba38, 0x1ba70, 0x1ba7e, 0x1bae0, 0x1bafc, 0x1bb08, 0x1bb10, + 0x1bb20, 0x1bb3c, 0x1bb40, 0x1bb98, 0x1bbce, 0x1bbe2, 0x1bbe4, + 0x1bbe8, 0x1bc16, 0x1bc26, 0x1bc2c, 0x1bc46, 0x1bc4c, 0x1bc58, + 0x1bc72, 0x1bc74, 0x1bc86, 0x1bc8c, 0x1bc98, 0x1bcb0, 0x1bcbe, + 0x1bcce, 0x1bce2, 0x1bce4, 0x1bce8, 0x1bd06, 0x1bd0c, 0x1bd18, + 0x1bd30, 0x1bd3e, 0x1bd60, 0x1bd7c, 0x1bd9c, 0x1bdc2, 0x1bdc4, + 0x1bdc8, 0x1bdd0, 0x1bde6, 0x1bdfa, 0x1be12, 0x1be14, 0x1be22, + 0x1be24, 0x1be28, 0x1be42, 0x1be44, 0x1be48, 0x1be50, 0x1be5e, + 0x1be66, 0x1be82, 0x1be84, 0x1be88, 0x1be90, 0x1be9e, 0x1bea0, + 0x1bebc, 0x1becc, 0x1bef4, 0x1bf1a, 0x1bf2e, 0x1bf32, 0x1bf34, + 0x1bf4e, 0x1bf5c, 0x1bf62, 0x1bf64, 0x1bf68, 0x1c09a, 0x1c0b2, + 0x1c0b4, 0x1c11a, 0x1c132, 0x1c134, 0x1c162, 0x1c164, 0x1c168, + 0x1c176, 0x1c1ba, 0x1c21a, 0x1c232, 0x1c234, 0x1c24e, 0x1c25c, + 0x1c262, 0x1c264, 0x1c268, 0x1c276, 0x1c28e, 0x1c2c2, 0x1c2c4, + 0x1c2c8, 0x1c2d0, 0x1c2de, 0x1c2e6, 0x1c2ec, 0x1c2fa, 0x1c316, + 0x1c326, 0x1c33a, 0x1c346, 0x1c34c, 0x1c372, 0x1c374, 0x1c41a, + 0x1c42e, 0x1c432, 0x1c434, 0x1c44e, 0x1c45c, 0x1c462, 0x1c464, + 0x1c468, 0x1c476, 0x1c48e, 0x1c49c, 0x1c4b8, 0x1c4c2, 0x1c4c8, + 0x1c4d0, 0x1c4de, 0x1c4e6, 0x1c4ec, 0x1c4fa, 0x1c51c, 0x1c538, + 0x1c570, 0x1c57e, 0x1c582, 0x1c584, 0x1c588, 0x1c590, 0x1c59e, + 0x1c5a0, 0x1c5bc, 0x1c5c6, 0x1c5cc, 0x1c5d8, 0x1c5ee, 0x1c5f2, + 0x1c5f4, 0x1c616, 0x1c626, 0x1c62c, 0x1c63a, 0x1c646, 0x1c64c, + 0x1c658, 0x1c66e, 0x1c672, 0x1c674, 0x1c686, 0x1c68c, 0x1c698, + 0x1c6b0, 0x1c6be, 0x1c6ce, 0x1c6dc, 0x1c6e2, 0x1c6e4, 0x1c6e8, + 0x1c712, 0x1c714, 0x1c722, 0x1c728, 0x1c736, 0x1c742, 0x1c744, + 0x1c748, 0x1c750, 0x1c75e, 0x1c766, 0x1c76c, 0x1c77a, 0x1c7ae, + 0x1c7d6, 0x1c7ea, 0x1c81a, 0x1c82e, 0x1c832, 0x1c834, 0x1c84e, + 0x1c85c, 0x1c862, 0x1c864, 0x1c868, 0x1c876, 0x1c88e, 0x1c89c, + 0x1c8b8, 0x1c8c2, 0x1c8c8, 0x1c8d0, 0x1c8de, 0x1c8e6, 0x1c8ec, + 0x1c8fa, 0x1c90e, 0x1c938, 0x1c970, 0x1c97e, 0x1c982, 0x1c984, + 0x1c990, 0x1c99e, 0x1c9a0, 0x1c9bc, 0x1c9c6, 0x1c9cc, 0x1c9d8, + 0x1c9ee, 0x1c9f2, 0x1c9f4, 0x1ca38, 0x1ca70, 0x1ca7e, 0x1cae0, + 0x1cafc, 0x1cb02, 0x1cb04, 0x1cb08, 0x1cb10, 0x1cb20, 0x1cb3c, + 0x1cb40, 0x1cb78, 0x1cb86, 0x1cb8c, 0x1cb98, 0x1cbb0, 0x1cbbe, + 0x1cbce, 0x1cbdc, 0x1cbe2, 0x1cbe4, 0x1cbe8, 0x1cbf6, 0x1cc16, + 0x1cc26, 0x1cc2c, 0x1cc3a, 0x1cc46, 0x1cc58, 0x1cc72, 0x1cc74, + 0x1cc86, 0x1ccb0, 0x1ccbe, 0x1ccce, 0x1cce2, 0x1cce4, 0x1cce8, + 0x1cd06, 0x1cd0c, 0x1cd18, 0x1cd30, 0x1cd3e, 0x1cd60, 0x1cd7c, + 0x1cd9c, 0x1cdc2, 0x1cdc4, 0x1cdc8, 0x1cdd0, 0x1cdde, 0x1cde6, + 0x1cdfa, 0x1ce22, 0x1ce28, 0x1ce42, 0x1ce50, 0x1ce5e, 0x1ce66, + 0x1ce7a, 0x1ce82, 0x1ce84, 0x1ce88, 0x1ce90, 0x1ce9e, 0x1cea0, + 0x1cebc, 0x1cecc, 0x1cef2, 0x1cef4, 0x1cf2e, 0x1cf32, 0x1cf34, + 0x1cf4e, 0x1cf5c, 0x1cf62, 0x1cf64, 0x1cf68, 0x1cf96, 0x1cfa6, + 0x1cfac, 0x1cfca, 0x1cfd2, 0x1cfd4, 0x1d02e, 0x1d032, 0x1d034, + 0x1d04e, 0x1d05c, 0x1d062, 0x1d064, 0x1d068, 0x1d076, 0x1d08e, + 0x1d09c, 0x1d0b8, 0x1d0c2, 0x1d0c4, 0x1d0c8, 0x1d0d0, 0x1d0de, + 0x1d0e6, 0x1d0ec, 0x1d0fa, 0x1d11c, 0x1d138, 0x1d170, 0x1d17e, + 0x1d182, 0x1d184, 0x1d188, 0x1d190, 0x1d19e, 0x1d1a0, 0x1d1bc, + 0x1d1c6, 0x1d1cc, 0x1d1d8, 0x1d1ee, 0x1d1f2, 0x1d1f4, 0x1d21c, + 0x1d238, 0x1d270, 0x1d27e, 0x1d2e0, 0x1d2fc, 0x1d302, 0x1d304, + 0x1d308, 0x1d310, 0x1d31e, 0x1d320, 0x1d33c, 0x1d340, 0x1d378, + 0x1d386, 0x1d38c, 0x1d398, 0x1d3b0, 0x1d3be, 0x1d3ce, 0x1d3dc, + 0x1d3e2, 0x1d3e4, 0x1d3e8, 0x1d3f6, 0x1d470, 0x1d47e, 0x1d4e0, + 0x1d4fc, 0x1d5c0, 0x1d5f8, 0x1d604, 0x1d608, 0x1d610, 0x1d620, + 0x1d640, 0x1d678, 0x1d6f0, 0x1d706, 0x1d70c, 0x1d718, 0x1d730, + 0x1d73e, 0x1d760, 0x1d77c, 0x1d78e, 0x1d79c, 0x1d7b8, 0x1d7c2, + 0x1d7c4, 0x1d7c8, 0x1d7d0, 0x1d7de, 0x1d7e6, 0x1d7ec, 0x1d826, + 0x1d82c, 0x1d83a, 0x1d846, 0x1d84c, 0x1d858, 0x1d872, 0x1d874, + 0x1d886, 0x1d88c, 0x1d898, 0x1d8b0, 0x1d8be, 0x1d8ce, 0x1d8e2, + 0x1d8e4, 0x1d8e8, 0x1d8f6, 0x1d90c, 0x1d918, 0x1d930, 0x1d93e, + 0x1d960, 0x1d97c, 0x1d99c, 0x1d9c2, 0x1d9c4, 0x1d9c8, 0x1d9d0, + 0x1d9e6, 0x1d9fa, 0x1da0c, 0x1da18, 0x1da30, 0x1da3e, 0x1da60, + 0x1da7c, 0x1dac0, 0x1daf8, 0x1db38, 0x1db82, 0x1db84, 0x1db88, + 0x1db90, 0x1db9e, 0x1dba0, 0x1dbcc, 0x1dbf2, 0x1dbf4, 0x1dc22, + 0x1dc42, 0x1dc44, 0x1dc48, 0x1dc50, 0x1dc5e, 0x1dc66, 0x1dc7a, + 0x1dc82, 0x1dc84, 0x1dc88, 0x1dc90, 0x1dc9e, 0x1dca0, 0x1dcbc, + 0x1dccc, 0x1dcf2, 0x1dcf4, 0x1dd04, 0x1dd08, 0x1dd10, 0x1dd1e, + 0x1dd20, 0x1dd3c, 0x1dd40, 0x1dd78, 0x1dd86, 0x1dd98, 0x1ddce, + 0x1dde2, 0x1dde4, 0x1dde8, 0x1de2e, 0x1de32, 0x1de34, 0x1de4e, + 0x1de5c, 0x1de62, 0x1de64, 0x1de68, 0x1de8e, 0x1de9c, 0x1deb8, + 0x1dec2, 0x1dec4, 0x1dec8, 0x1ded0, 0x1dee6, 0x1defa, 0x1df16, + 0x1df26, 0x1df2c, 0x1df46, 0x1df4c, 0x1df58, 0x1df72, 0x1df74, + 0x1df8a, 0x1df92, 0x1df94, 0x1dfa2, 0x1dfa4, 0x1dfa8, 0x1e08a, + 0x1e092, 0x1e094, 0x1e0a2, 0x1e0a4, 0x1e0a8, 0x1e0b6, 0x1e0da, + 0x1e10a, 0x1e112, 0x1e114, 0x1e122, 0x1e124, 0x1e128, 0x1e136, + 0x1e142, 0x1e144, 0x1e148, 0x1e150, 0x1e166, 0x1e16c, 0x1e17a, + 0x1e19a, 0x1e1b2, 0x1e1b4, 0x1e20a, 0x1e212, 0x1e214, 0x1e222, + 0x1e224, 0x1e228, 0x1e236, 0x1e242, 0x1e248, 0x1e250, 0x1e25e, + 0x1e266, 0x1e26c, 0x1e27a, 0x1e282, 0x1e284, 0x1e288, 0x1e290, + 0x1e2a0, 0x1e2bc, 0x1e2c6, 0x1e2cc, 0x1e2d8, 0x1e2ee, 0x1e2f2, + 0x1e2f4, 0x1e31a, 0x1e332, 0x1e334, 0x1e35c, 0x1e362, 0x1e364, + 0x1e368, 0x1e3ba, 0x1e40a, 0x1e412, 0x1e414, 0x1e422, 0x1e428, + 0x1e436, 0x1e442, 0x1e448, 0x1e450, 0x1e45e, 0x1e466, 0x1e46c, + 0x1e47a, 0x1e482, 0x1e484, 0x1e490, 0x1e49e, 0x1e4a0, 0x1e4bc, + 0x1e4c6, 0x1e4cc, 0x1e4d8, 0x1e4ee, 0x1e4f2, 0x1e4f4, 0x1e502, + 0x1e504, 0x1e508, 0x1e510, 0x1e51e, 0x1e520, 0x1e53c, 0x1e540, + 0x1e578, 0x1e586, 0x1e58c, 0x1e598, 0x1e5b0, 0x1e5be, 0x1e5ce, + 0x1e5dc, 0x1e5e2, 0x1e5e4, 0x1e5e8, 0x1e5f6, 0x1e61a, 0x1e62e, + 0x1e632, 0x1e634, 0x1e64e, 0x1e65c, 0x1e662, 0x1e668, 0x1e68e, + 0x1e69c, 0x1e6b8, 0x1e6c2, 0x1e6c4, 0x1e6c8, 0x1e6d0, 0x1e6e6, + 0x1e6fa, 0x1e716, 0x1e726, 0x1e72c, 0x1e73a, 0x1e746, 0x1e74c, + 0x1e758, 0x1e772, 0x1e774, 0x1e792, 0x1e794, 0x1e7a2, 0x1e7a4, + 0x1e7a8, 0x1e7b6, 0x1e812, 0x1e814, 0x1e822, 0x1e824, 0x1e828, + 0x1e836, 0x1e842, 0x1e844, 0x1e848, 0x1e850, 0x1e85e, 0x1e866, + 0x1e86c, 0x1e87a, 0x1e882, 0x1e884, 0x1e888, 0x1e890, 0x1e89e, + 0x1e8a0, 0x1e8bc, 0x1e8c6, 0x1e8cc, 0x1e8d8, 0x1e8ee, 0x1e8f2, + 0x1e8f4, 0x1e902, 0x1e904, 0x1e908, 0x1e910, 0x1e920, 0x1e93c, + 0x1e940, 0x1e978, 0x1e986, 0x1e98c, 0x1e998, 0x1e9b0, 0x1e9be, + 0x1e9ce, 0x1e9dc, 0x1e9e2, 0x1e9e4, 0x1e9e8, 0x1e9f6, 0x1ea04, + 0x1ea08, 0x1ea10, 0x1ea20, 0x1ea40, 0x1ea78, 0x1eaf0, 0x1eb06, + 0x1eb0c, 0x1eb18, 0x1eb30, 0x1eb3e, 0x1eb60, 0x1eb7c, 0x1eb8e, + 0x1eb9c, 0x1ebb8, 0x1ebc2, 0x1ebc4, 0x1ebc8, 0x1ebd0, 0x1ebde, + 0x1ebe6, 0x1ebec, 0x1ec1a, 0x1ec2e, 0x1ec32, 0x1ec34, 0x1ec4e, + 0x1ec5c, 0x1ec62, 0x1ec64, 0x1ec68, 0x1ec8e, 0x1ec9c, 0x1ecb8, + 0x1ecc2, 0x1ecc4, 0x1ecc8, 0x1ecd0, 0x1ece6, 0x1ecfa, 0x1ed0e, + 0x1ed1c, 0x1ed38, 0x1ed70, 0x1ed7e, 0x1ed82, 0x1ed84, 0x1ed88, + 0x1ed90, 0x1ed9e, 0x1eda0, 0x1edcc, 0x1edf2, 0x1edf4, 0x1ee16, + 0x1ee26, 0x1ee2c, 0x1ee3a, 0x1ee46, 0x1ee4c, 0x1ee58, 0x1ee6e, + 0x1ee72, 0x1ee74, 0x1ee86, 0x1ee8c, 0x1ee98, 0x1eeb0, 0x1eebe, + 0x1eece, 0x1eedc, 0x1eee2, 0x1eee4, 0x1eee8, 0x1ef12, 0x1ef22, + 0x1ef24, 0x1ef28, 0x1ef36, 0x1ef42, 0x1ef44, 0x1ef48, 0x1ef50, + 0x1ef5e, 0x1ef66, 0x1ef6c, 0x1ef7a, 0x1efae, 0x1efb2, 0x1efb4, + 0x1efd6, 0x1f096, 0x1f0a6, 0x1f0ac, 0x1f0ba, 0x1f0ca, 0x1f0d2, + 0x1f0d4, 0x1f116, 0x1f126, 0x1f12c, 0x1f13a, 0x1f146, 0x1f14c, + 0x1f158, 0x1f16e, 0x1f172, 0x1f174, 0x1f18a, 0x1f192, 0x1f194, + 0x1f1a2, 0x1f1a4, 0x1f1a8, 0x1f1da, 0x1f216, 0x1f226, 0x1f22c, + 0x1f23a, 0x1f246, 0x1f258, 0x1f26e, 0x1f272, 0x1f274, 0x1f286, + 0x1f28c, 0x1f298, 0x1f2b0, 0x1f2be, 0x1f2ce, 0x1f2dc, 0x1f2e2, + 0x1f2e4, 0x1f2e8, 0x1f2f6, 0x1f30a, 0x1f312, 0x1f314, 0x1f322, + 0x1f328, 0x1f342, 0x1f344, 0x1f348, 0x1f350, 0x1f35e, 0x1f366, + 0x1f37a, 0x1f39a, 0x1f3ae, 0x1f3b2, 0x1f3b4, 0x1f416, 0x1f426, + 0x1f42c, 0x1f43a, 0x1f446, 0x1f44c, 0x1f458, 0x1f46e, 0x1f472, + 0x1f474, 0x1f486, 0x1f48c, 0x1f498, 0x1f4b0, 0x1f4be, 0x1f4ce, + 0x1f4dc, 0x1f4e2, 0x1f4e4, 0x1f4e8, 0x1f4f6, 0x1f506, 0x1f50c, + 0x1f518, 0x1f530, 0x1f53e, 0x1f560, 0x1f57c, 0x1f58e, 0x1f59c, + 0x1f5b8, 0x1f5c2, 0x1f5c4, 0x1f5c8, 0x1f5d0, 0x1f5de, 0x1f5e6, + 0x1f5ec, 0x1f5fa, 0x1f60a, 0x1f612, 0x1f614, 0x1f622, 0x1f624, + 0x1f628, 0x1f636, 0x1f642, 0x1f644, 0x1f648, 0x1f650, 0x1f65e, + 0x1f666, 0x1f67a, 0x1f682, 0x1f684, 0x1f688, 0x1f690, 0x1f69e, + 0x1f6a0, 0x1f6bc, 0x1f6cc, 0x1f6f2, 0x1f6f4, 0x1f71a, 0x1f72e, + 0x1f732, 0x1f734, 0x1f74e, 0x1f75c, 0x1f762, 0x1f764, 0x1f768, + 0x1f776, 0x1f796, 0x1f7a6, 0x1f7ac, 0x1f7ba, 0x1f7d2, 0x1f7d4, + 0x1f89a, 0x1f8ae, 0x1f8b2, 0x1f8b4, 0x1f8d6, 0x1f8ea, 0x1f91a, + 0x1f92e, 0x1f932, 0x1f934, 0x1f94e, 0x1f95c, 0x1f962, 0x1f964, + 0x1f968, 0x1f976, 0x1f996, 0x1f9a6, 0x1f9ac, 0x1f9ba, 0x1f9ca, + 0x1f9d2, 0x1f9d4, 0x1fa1a, 0x1fa2e, 0x1fa32, 0x1fa34, 0x1fa4e, + 0x1fa5c, 0x1fa62, 0x1fa64, 0x1fa68, 0x1fa76, 0x1fa8e, 0x1fa9c, + 0x1fab8, 0x1fac2, 0x1fac4, 0x1fac8, 0x1fad0, 0x1fade, 0x1fae6, + 0x1faec, 0x1fb16, 0x1fb26, 0x1fb2c, 0x1fb3a, 0x1fb46, 0x1fb4c, + 0x1fb58, 0x1fb6e, 0x1fb72, 0x1fb74, 0x1fb8a, 0x1fb92, 0x1fb94, + 0x1fba2, 0x1fba4, 0x1fba8, 0x1fbb6, 0x1fbda}; + + /** + * This table contains to codewords for all symbols. + */ + private static final int[] CODEWORD_TABLE = {2627, 1819, 2622, 2621, 1813, + 1812, 2729, 2724, 2723, 2779, 2774, 2773, 902, 896, 908, 868, 865, + 861, 859, 2511, 873, 871, 1780, 835, 2493, 825, 2491, 842, 837, 844, + 1764, 1762, 811, 810, 809, 2483, 807, 2482, 806, 2480, 815, 814, 813, + 812, 2484, 817, 816, 1745, 1744, 1742, 1746, 2655, 2637, 2635, 2626, + 2625, 2623, 2628, 1820, 2752, 2739, 2737, 2728, 2727, 2725, 2730, + 2785, 2783, 2778, 2777, 2775, 2780, 787, 781, 747, 739, 736, 2413, + 754, 752, 1719, 692, 689, 681, 2371, 678, 2369, 700, 697, 694, 703, + 1688, 1686, 642, 638, 2343, 631, 2341, 627, 2338, 651, 646, 643, 2345, + 654, 652, 1652, 1650, 1647, 1654, 601, 599, 2322, 596, 2321, 594, + 2319, 2317, 611, 610, 608, 606, 2324, 603, 2323, 615, 614, 612, 1617, + 1616, 1614, 1612, 616, 1619, 1618, 2575, 2538, 2536, 905, 901, 898, + 909, 2509, 2507, 2504, 870, 867, 864, 860, 2512, 875, 872, 1781, 2490, + 2489, 2487, 2485, 1748, 836, 834, 832, 830, 2494, 827, 2492, 843, 841, + 839, 845, 1765, 1763, 2701, 2676, 2674, 2653, 2648, 2656, 2634, 2633, + 2631, 2629, 1821, 2638, 2636, 2770, 2763, 2761, 2750, 2745, 2753, + 2736, 2735, 2733, 2731, 1848, 2740, 2738, 2786, 2784, 591, 588, 576, + 569, 566, 2296, 1590, 537, 534, 526, 2276, 522, 2274, 545, 542, 539, + 548, 1572, 1570, 481, 2245, 466, 2242, 462, 2239, 492, 485, 482, 2249, + 496, 494, 1534, 1531, 1528, 1538, 413, 2196, 406, 2191, 2188, 425, + 419, 2202, 415, 2199, 432, 430, 427, 1472, 1467, 1464, 433, 1476, + 1474, 368, 367, 2160, 365, 2159, 362, 2157, 2155, 2152, 378, 377, 375, + 2166, 372, 2165, 369, 2162, 383, 381, 379, 2168, 1419, 1418, 1416, + 1414, 385, 1411, 384, 1423, 1422, 1420, 1424, 2461, 802, 2441, 2439, + 790, 786, 783, 794, 2409, 2406, 2403, 750, 742, 738, 2414, 756, 753, + 1720, 2367, 2365, 2362, 2359, 1663, 693, 691, 684, 2373, 680, 2370, + 702, 699, 696, 704, 1690, 1687, 2337, 2336, 2334, 2332, 1624, 2329, + 1622, 640, 637, 2344, 634, 2342, 630, 2340, 650, 648, 645, 2346, 655, + 653, 1653, 1651, 1649, 1655, 2612, 2597, 2595, 2571, 2568, 2565, 2576, + 2534, 2529, 2526, 1787, 2540, 2537, 907, 904, 900, 910, 2503, 2502, + 2500, 2498, 1768, 2495, 1767, 2510, 2508, 2506, 869, 866, 863, 2513, + 876, 874, 1782, 2720, 2713, 2711, 2697, 2694, 2691, 2702, 2672, 2670, + 2664, 1828, 2678, 2675, 2647, 2646, 2644, 2642, 1823, 2639, 1822, + 2654, 2652, 2650, 2657, 2771, 1855, 2765, 2762, 1850, 1849, 2751, + 2749, 2747, 2754, 353, 2148, 344, 342, 336, 2142, 332, 2140, 345, + 1375, 1373, 306, 2130, 299, 2128, 295, 2125, 319, 314, 311, 2132, + 1354, 1352, 1349, 1356, 262, 257, 2101, 253, 2096, 2093, 274, 273, + 267, 2107, 263, 2104, 280, 278, 275, 1316, 1311, 1308, 1320, 1318, + 2052, 202, 2050, 2044, 2040, 219, 2063, 212, 2060, 208, 2055, 224, + 221, 2066, 1260, 1258, 1252, 231, 1248, 229, 1266, 1264, 1261, 1268, + 155, 1998, 153, 1996, 1994, 1991, 1988, 165, 164, 2007, 162, 2006, + 159, 2003, 2000, 172, 171, 169, 2012, 166, 2010, 1186, 1184, 1182, + 1179, 175, 1176, 173, 1192, 1191, 1189, 1187, 176, 1194, 1193, 2313, + 2307, 2305, 592, 589, 2294, 2292, 2289, 578, 572, 568, 2297, 580, + 1591, 2272, 2267, 2264, 1547, 538, 536, 529, 2278, 525, 2275, 547, + 544, 541, 1574, 1571, 2237, 2235, 2229, 1493, 2225, 1489, 478, 2247, + 470, 2244, 465, 2241, 493, 488, 484, 2250, 498, 495, 1536, 1533, 1530, + 1539, 2187, 2186, 2184, 2182, 1432, 2179, 1430, 2176, 1427, 414, 412, + 2197, 409, 2195, 405, 2193, 2190, 426, 424, 421, 2203, 418, 2201, 431, + 429, 1473, 1471, 1469, 1466, 434, 1477, 1475, 2478, 2472, 2470, 2459, + 2457, 2454, 2462, 803, 2437, 2432, 2429, 1726, 2443, 2440, 792, 789, + 785, 2401, 2399, 2393, 1702, 2389, 1699, 2411, 2408, 2405, 745, 741, + 2415, 758, 755, 1721, 2358, 2357, 2355, 2353, 1661, 2350, 1660, 2347, + 1657, 2368, 2366, 2364, 2361, 1666, 690, 687, 2374, 683, 2372, 701, + 698, 705, 1691, 1689, 2619, 2617, 2610, 2608, 2605, 2613, 2593, 2588, + 2585, 1803, 2599, 2596, 2563, 2561, 2555, 1797, 2551, 1795, 2573, + 2570, 2567, 2577, 2525, 2524, 2522, 2520, 1786, 2517, 1785, 2514, + 1783, 2535, 2533, 2531, 2528, 1788, 2541, 2539, 906, 903, 911, 2721, + 1844, 2715, 2712, 1838, 1836, 2699, 2696, 2693, 2703, 1827, 1826, + 1824, 2673, 2671, 2669, 2666, 1829, 2679, 2677, 1858, 1857, 2772, + 1854, 1853, 1851, 1856, 2766, 2764, 143, 1987, 139, 1986, 135, 133, + 131, 1984, 128, 1983, 125, 1981, 138, 137, 136, 1985, 1133, 1132, + 1130, 112, 110, 1974, 107, 1973, 104, 1971, 1969, 122, 121, 119, 117, + 1977, 114, 1976, 124, 1115, 1114, 1112, 1110, 1117, 1116, 84, 83, + 1953, 81, 1952, 78, 1950, 1948, 1945, 94, 93, 91, 1959, 88, 1958, 85, + 1955, 99, 97, 95, 1961, 1086, 1085, 1083, 1081, 1078, 100, 1090, 1089, + 1087, 1091, 49, 47, 1917, 44, 1915, 1913, 1910, 1907, 59, 1926, 56, + 1925, 53, 1922, 1919, 66, 64, 1931, 61, 1929, 1042, 1040, 1038, 71, + 1035, 70, 1032, 68, 1048, 1047, 1045, 1043, 1050, 1049, 12, 10, 1869, + 1867, 1864, 1861, 21, 1880, 19, 1877, 1874, 1871, 28, 1888, 25, 1886, + 22, 1883, 982, 980, 977, 974, 32, 30, 991, 989, 987, 984, 34, 995, + 994, 992, 2151, 2150, 2147, 2146, 2144, 356, 355, 354, 2149, 2139, + 2138, 2136, 2134, 1359, 343, 341, 338, 2143, 335, 2141, 348, 347, 346, + 1376, 1374, 2124, 2123, 2121, 2119, 1326, 2116, 1324, 310, 308, 305, + 2131, 302, 2129, 298, 2127, 320, 318, 316, 313, 2133, 322, 321, 1355, + 1353, 1351, 1357, 2092, 2091, 2089, 2087, 1276, 2084, 1274, 2081, + 1271, 259, 2102, 256, 2100, 252, 2098, 2095, 272, 269, 2108, 266, + 2106, 281, 279, 277, 1317, 1315, 1313, 1310, 282, 1321, 1319, 2039, + 2037, 2035, 2032, 1203, 2029, 1200, 1197, 207, 2053, 205, 2051, 201, + 2049, 2046, 2043, 220, 218, 2064, 215, 2062, 211, 2059, 228, 226, 223, + 2069, 1259, 1257, 1254, 232, 1251, 230, 1267, 1265, 1263, 2316, 2315, + 2312, 2311, 2309, 2314, 2304, 2303, 2301, 2299, 1593, 2308, 2306, 590, + 2288, 2287, 2285, 2283, 1578, 2280, 1577, 2295, 2293, 2291, 579, 577, + 574, 571, 2298, 582, 581, 1592, 2263, 2262, 2260, 2258, 1545, 2255, + 1544, 2252, 1541, 2273, 2271, 2269, 2266, 1550, 535, 532, 2279, 528, + 2277, 546, 543, 549, 1575, 1573, 2224, 2222, 2220, 1486, 2217, 1485, + 2214, 1482, 1479, 2238, 2236, 2234, 2231, 1496, 2228, 1492, 480, 477, + 2248, 473, 2246, 469, 2243, 490, 487, 2251, 497, 1537, 1535, 1532, + 2477, 2476, 2474, 2479, 2469, 2468, 2466, 2464, 1730, 2473, 2471, + 2453, 2452, 2450, 2448, 1729, 2445, 1728, 2460, 2458, 2456, 2463, 805, + 804, 2428, 2427, 2425, 2423, 1725, 2420, 1724, 2417, 1722, 2438, 2436, + 2434, 2431, 1727, 2444, 2442, 793, 791, 788, 795, 2388, 2386, 2384, + 1697, 2381, 1696, 2378, 1694, 1692, 2402, 2400, 2398, 2395, 1703, + 2392, 1701, 2412, 2410, 2407, 751, 748, 744, 2416, 759, 757, 1807, + 2620, 2618, 1806, 1805, 2611, 2609, 2607, 2614, 1802, 1801, 1799, + 2594, 2592, 2590, 2587, 1804, 2600, 2598, 1794, 1793, 1791, 1789, + 2564, 2562, 2560, 2557, 1798, 2554, 1796, 2574, 2572, 2569, 2578, + 1847, 1846, 2722, 1843, 1842, 1840, 1845, 2716, 2714, 1835, 1834, + 1832, 1830, 1839, 1837, 2700, 2698, 2695, 2704, 1817, 1811, 1810, 897, + 862, 1777, 829, 826, 838, 1760, 1758, 808, 2481, 1741, 1740, 1738, + 1743, 2624, 1818, 2726, 2776, 782, 740, 737, 1715, 686, 679, 695, + 1682, 1680, 639, 628, 2339, 647, 644, 1645, 1643, 1640, 1648, 602, + 600, 597, 595, 2320, 593, 2318, 609, 607, 604, 1611, 1610, 1608, 1606, + 613, 1615, 1613, 2328, 926, 924, 892, 886, 899, 857, 850, 2505, 1778, + 824, 823, 821, 819, 2488, 818, 2486, 833, 831, 828, 840, 1761, 1759, + 2649, 2632, 2630, 2746, 2734, 2732, 2782, 2781, 570, 567, 1587, 531, + 527, 523, 540, 1566, 1564, 476, 467, 463, 2240, 486, 483, 1524, 1521, + 1518, 1529, 411, 403, 2192, 399, 2189, 423, 416, 1462, 1457, 1454, + 428, 1468, 1465, 2210, 366, 363, 2158, 360, 2156, 357, 2153, 376, 373, + 370, 2163, 1410, 1409, 1407, 1405, 382, 1402, 380, 1417, 1415, 1412, + 1421, 2175, 2174, 777, 774, 771, 784, 732, 725, 722, 2404, 743, 1716, + 676, 674, 668, 2363, 665, 2360, 685, 1684, 1681, 626, 624, 622, 2335, + 620, 2333, 617, 2330, 641, 635, 649, 1646, 1644, 1642, 2566, 928, 925, + 2530, 2527, 894, 891, 888, 2501, 2499, 2496, 858, 856, 854, 851, 1779, + 2692, 2668, 2665, 2645, 2643, 2640, 2651, 2768, 2759, 2757, 2744, + 2743, 2741, 2748, 352, 1382, 340, 337, 333, 1371, 1369, 307, 300, 296, + 2126, 315, 312, 1347, 1342, 1350, 261, 258, 250, 2097, 246, 2094, 271, + 268, 264, 1306, 1301, 1298, 276, 1312, 1309, 2115, 203, 2048, 195, + 2045, 191, 2041, 213, 209, 2056, 1246, 1244, 1238, 225, 1234, 222, + 1256, 1253, 1249, 1262, 2080, 2079, 154, 1997, 150, 1995, 147, 1992, + 1989, 163, 160, 2004, 156, 2001, 1175, 1174, 1172, 1170, 1167, 170, + 1164, 167, 1185, 1183, 1180, 1177, 174, 1190, 1188, 2025, 2024, 2022, + 587, 586, 564, 559, 556, 2290, 573, 1588, 520, 518, 512, 2268, 508, + 2265, 530, 1568, 1565, 461, 457, 2233, 450, 2230, 446, 2226, 479, 471, + 489, 1526, 1523, 1520, 397, 395, 2185, 392, 2183, 389, 2180, 2177, + 410, 2194, 402, 422, 1463, 1461, 1459, 1456, 1470, 2455, 799, 2433, + 2430, 779, 776, 773, 2397, 2394, 2390, 734, 728, 724, 746, 1717, 2356, + 2354, 2351, 2348, 1658, 677, 675, 673, 670, 667, 688, 1685, 1683, + 2606, 2589, 2586, 2559, 2556, 2552, 927, 2523, 2521, 2518, 2515, 1784, + 2532, 895, 893, 890, 2718, 2709, 2707, 2689, 2687, 2684, 2663, 2662, + 2660, 2658, 1825, 2667, 2769, 1852, 2760, 2758, 142, 141, 1139, 1138, + 134, 132, 129, 126, 1982, 1129, 1128, 1126, 1131, 113, 111, 108, 105, + 1972, 101, 1970, 120, 118, 115, 1109, 1108, 1106, 1104, 123, 1113, + 1111, 82, 79, 1951, 75, 1949, 72, 1946, 92, 89, 86, 1956, 1077, 1076, + 1074, 1072, 98, 1069, 96, 1084, 1082, 1079, 1088, 1968, 1967, 48, 45, + 1916, 42, 1914, 39, 1911, 1908, 60, 57, 54, 1923, 50, 1920, 1031, + 1030, 1028, 1026, 67, 1023, 65, 1020, 62, 1041, 1039, 1036, 1033, 69, + 1046, 1044, 1944, 1943, 1941, 11, 9, 1868, 7, 1865, 1862, 1859, 20, + 1878, 16, 1875, 13, 1872, 970, 968, 966, 963, 29, 960, 26, 23, 983, + 981, 978, 975, 33, 971, 31, 990, 988, 985, 1906, 1904, 1902, 993, 351, + 2145, 1383, 331, 330, 328, 326, 2137, 323, 2135, 339, 1372, 1370, 294, + 293, 291, 289, 2122, 286, 2120, 283, 2117, 309, 303, 317, 1348, 1346, + 1344, 245, 244, 242, 2090, 239, 2088, 236, 2085, 2082, 260, 2099, 249, + 270, 1307, 1305, 1303, 1300, 1314, 189, 2038, 186, 2036, 183, 2033, + 2030, 2026, 206, 198, 2047, 194, 216, 1247, 1245, 1243, 1240, 227, + 1237, 1255, 2310, 2302, 2300, 2286, 2284, 2281, 565, 563, 561, 558, + 575, 1589, 2261, 2259, 2256, 2253, 1542, 521, 519, 517, 514, 2270, + 511, 533, 1569, 1567, 2223, 2221, 2218, 2215, 1483, 2211, 1480, 459, + 456, 453, 2232, 449, 474, 491, 1527, 1525, 1522, 2475, 2467, 2465, + 2451, 2449, 2446, 801, 800, 2426, 2424, 2421, 2418, 1723, 2435, 780, + 778, 775, 2387, 2385, 2382, 2379, 1695, 2375, 1693, 2396, 735, 733, + 730, 727, 749, 1718, 2616, 2615, 2604, 2603, 2601, 2584, 2583, 2581, + 2579, 1800, 2591, 2550, 2549, 2547, 2545, 1792, 2542, 1790, 2558, 929, + 2719, 1841, 2710, 2708, 1833, 1831, 2690, 2688, 2686, 1815, 1809, + 1808, 1774, 1756, 1754, 1737, 1736, 1734, 1739, 1816, 1711, 1676, + 1674, 633, 629, 1638, 1636, 1633, 1641, 598, 1605, 1604, 1602, 1600, + 605, 1609, 1607, 2327, 887, 853, 1775, 822, 820, 1757, 1755, 1584, + 524, 1560, 1558, 468, 464, 1514, 1511, 1508, 1519, 408, 404, 400, + 1452, 1447, 1444, 417, 1458, 1455, 2208, 364, 361, 358, 2154, 1401, + 1400, 1398, 1396, 374, 1393, 371, 1408, 1406, 1403, 1413, 2173, 2172, + 772, 726, 723, 1712, 672, 669, 666, 682, 1678, 1675, 625, 623, 621, + 618, 2331, 636, 632, 1639, 1637, 1635, 920, 918, 884, 880, 889, 849, + 848, 847, 846, 2497, 855, 852, 1776, 2641, 2742, 2787, 1380, 334, + 1367, 1365, 301, 297, 1340, 1338, 1335, 1343, 255, 251, 247, 1296, + 1291, 1288, 265, 1302, 1299, 2113, 204, 196, 192, 2042, 1232, 1230, + 1224, 214, 1220, 210, 1242, 1239, 1235, 1250, 2077, 2075, 151, 148, + 1993, 144, 1990, 1163, 1162, 1160, 1158, 1155, 161, 1152, 157, 1173, + 1171, 1168, 1165, 168, 1181, 1178, 2021, 2020, 2018, 2023, 585, 560, + 557, 1585, 516, 509, 1562, 1559, 458, 447, 2227, 472, 1516, 1513, + 1510, 398, 396, 393, 390, 2181, 386, 2178, 407, 1453, 1451, 1449, + 1446, 420, 1460, 2209, 769, 764, 720, 712, 2391, 729, 1713, 664, 663, + 661, 659, 2352, 656, 2349, 671, 1679, 1677, 2553, 922, 919, 2519, + 2516, 885, 883, 881, 2685, 2661, 2659, 2767, 2756, 2755, 140, 1137, + 1136, 130, 127, 1125, 1124, 1122, 1127, 109, 106, 102, 1103, 1102, + 1100, 1098, 116, 1107, 1105, 1980, 80, 76, 73, 1947, 1068, 1067, 1065, + 1063, 90, 1060, 87, 1075, 1073, 1070, 1080, 1966, 1965, 46, 43, 40, + 1912, 36, 1909, 1019, 1018, 1016, 1014, 58, 1011, 55, 1008, 51, 1029, + 1027, 1024, 1021, 63, 1037, 1034, 1940, 1939, 1937, 1942, 8, 1866, 4, + 1863, 1, 1860, 956, 954, 952, 949, 946, 17, 14, 969, 967, 964, 961, + 27, 957, 24, 979, 976, 972, 1901, 1900, 1898, 1896, 986, 1905, 1903, + 350, 349, 1381, 329, 327, 324, 1368, 1366, 292, 290, 287, 284, 2118, + 304, 1341, 1339, 1337, 1345, 243, 240, 237, 2086, 233, 2083, 254, + 1297, 1295, 1293, 1290, 1304, 2114, 190, 187, 184, 2034, 180, 2031, + 177, 2027, 199, 1233, 1231, 1229, 1226, 217, 1223, 1241, 2078, 2076, + 584, 555, 554, 552, 550, 2282, 562, 1586, 507, 506, 504, 502, 2257, + 499, 2254, 515, 1563, 1561, 445, 443, 441, 2219, 438, 2216, 435, 2212, + 460, 454, 475, 1517, 1515, 1512, 2447, 798, 797, 2422, 2419, 770, 768, + 766, 2383, 2380, 2376, 721, 719, 717, 714, 731, 1714, 2602, 2582, + 2580, 2548, 2546, 2543, 923, 921, 2717, 2706, 2705, 2683, 2682, 2680, + 1771, 1752, 1750, 1733, 1732, 1731, 1735, 1814, 1707, 1670, 1668, + 1631, 1629, 1626, 1634, 1599, 1598, 1596, 1594, 1603, 1601, 2326, + 1772, 1753, 1751, 1581, 1554, 1552, 1504, 1501, 1498, 1509, 1442, + 1437, 1434, 401, 1448, 1445, 2206, 1392, 1391, 1389, 1387, 1384, 359, + 1399, 1397, 1394, 1404, 2171, 2170, 1708, 1672, 1669, 619, 1632, 1630, + 1628, 1773, 1378, 1363, 1361, 1333, 1328, 1336, 1286, 1281, 1278, 248, + 1292, 1289, 2111, 1218, 1216, 1210, 197, 1206, 193, 1228, 1225, 1221, + 1236, 2073, 2071, 1151, 1150, 1148, 1146, 152, 1143, 149, 1140, 145, + 1161, 1159, 1156, 1153, 158, 1169, 1166, 2017, 2016, 2014, 2019, 1582, + 510, 1556, 1553, 452, 448, 1506, 1500, 394, 391, 387, 1443, 1441, + 1439, 1436, 1450, 2207, 765, 716, 713, 1709, 662, 660, 657, 1673, + 1671, 916, 914, 879, 878, 877, 882, 1135, 1134, 1121, 1120, 1118, + 1123, 1097, 1096, 1094, 1092, 103, 1101, 1099, 1979, 1059, 1058, 1056, + 1054, 77, 1051, 74, 1066, 1064, 1061, 1071, 1964, 1963, 1007, 1006, + 1004, 1002, 999, 41, 996, 37, 1017, 1015, 1012, 1009, 52, 1025, 1022, + 1936, 1935, 1933, 1938, 942, 940, 938, 935, 932, 5, 2, 955, 953, 950, + 947, 18, 943, 15, 965, 962, 958, 1895, 1894, 1892, 1890, 973, 1899, + 1897, 1379, 325, 1364, 1362, 288, 285, 1334, 1332, 1330, 241, 238, + 234, 1287, 1285, 1283, 1280, 1294, 2112, 188, 185, 181, 178, 2028, + 1219, 1217, 1215, 1212, 200, 1209, 1227, 2074, 2072, 583, 553, 551, + 1583, 505, 503, 500, 513, 1557, 1555, 444, 442, 439, 436, 2213, 455, + 451, 1507, 1505, 1502, 796, 763, 762, 760, 767, 711, 710, 708, 706, + 2377, 718, 715, 1710, 2544, 917, 915, 2681, 1627, 1597, 1595, 2325, + 1769, 1749, 1747, 1499, 1438, 1435, 2204, 1390, 1388, 1385, 1395, + 2169, 2167, 1704, 1665, 1662, 1625, 1623, 1620, 1770, 1329, 1282, + 1279, 2109, 1214, 1207, 1222, 2068, 2065, 1149, 1147, 1144, 1141, 146, + 1157, 1154, 2013, 2011, 2008, 2015, 1579, 1549, 1546, 1495, 1487, + 1433, 1431, 1428, 1425, 388, 1440, 2205, 1705, 658, 1667, 1664, 1119, + 1095, 1093, 1978, 1057, 1055, 1052, 1062, 1962, 1960, 1005, 1003, + 1000, 997, 38, 1013, 1010, 1932, 1930, 1927, 1934, 941, 939, 936, 933, + 6, 930, 3, 951, 948, 944, 1889, 1887, 1884, 1881, 959, 1893, 1891, 35, + 1377, 1360, 1358, 1327, 1325, 1322, 1331, 1277, 1275, 1272, 1269, 235, + 1284, 2110, 1205, 1204, 1201, 1198, 182, 1195, 179, 1213, 2070, 2067, + 1580, 501, 1551, 1548, 440, 437, 1497, 1494, 1490, 1503, 761, 709, + 707, 1706, 913, 912, 2198, 1386, 2164, 2161, 1621, 1766, 2103, 1208, + 2058, 2054, 1145, 1142, 2005, 2002, 1999, 2009, 1488, 1429, 1426, + 2200, 1698, 1659, 1656, 1975, 1053, 1957, 1954, 1001, 998, 1924, 1921, + 1918, 1928, 937, 934, 931, 1879, 1876, 1873, 1870, 945, 1885, 1882, + 1323, 1273, 1270, 2105, 1202, 1199, 1196, 1211, 2061, 2057, 1576, + 1543, 1540, 1484, 1481, 1478, 1491, 1700}; +} diff --git a/src/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java b/src/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java new file mode 100644 index 0000000..f224e59 --- /dev/null +++ b/src/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java @@ -0,0 +1,629 @@ +/* + * Copyright 2009 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.pdf417.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.DecoderResult; + +/** + *This class contains the methods for decoding the PDF417 codewords.
+ * + * @author SITA Lab (kevin.osullivan@sita.aero) + */ +final class DecodedBitStreamParser { + + private static final int TEXT_COMPACTION_MODE_LATCH = 900; + private static final int BYTE_COMPACTION_MODE_LATCH = 901; + private static final int NUMERIC_COMPACTION_MODE_LATCH = 902; + private static final int BYTE_COMPACTION_MODE_LATCH_6 = 924; + private static final int BEGIN_MACRO_PDF417_CONTROL_BLOCK = 928; + private static final int BEGIN_MACRO_PDF417_OPTIONAL_FIELD = 923; + private static final int MACRO_PDF417_TERMINATOR = 922; + private static final int MODE_SHIFT_TO_BYTE_COMPACTION_MODE = 913; + private static final int MAX_NUMERIC_CODEWORDS = 15; + + private static final int ALPHA = 0; + private static final int LOWER = 1; + private static final int MIXED = 2; + private static final int PUNCT = 3; + private static final int PUNCT_SHIFT = 4; + + private static final int PL = 25; + private static final int LL = 27; + private static final int AS = 27; + private static final int ML = 28; + private static final int AL = 28; + private static final int PS = 29; + private static final int PAL = 29; + + private static final char[] PUNCT_CHARS = {';', '<', '>', '@', '[', 92, '}', '_', 96, '~', '!', + 13, 9, ',', ':', 10, '-', '.', '$', '/', 34, '|', '*', + '(', ')', '?', '{', '}', 39}; + + private static final char[] MIXED_CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '&', + 13, 9, ',', ':', '#', '-', '.', '$', '/', '+', '%', '*', + '=', '^'}; + + // Table containing values for the exponent of 900. + // This is used in the numeric compaction decode algorithm. + private static final String[] EXP900 = + { "000000000000000000000000000000000000000000001", + "000000000000000000000000000000000000000000900", + "000000000000000000000000000000000000000810000", + "000000000000000000000000000000000000729000000", + "000000000000000000000000000000000656100000000", + "000000000000000000000000000000590490000000000", + "000000000000000000000000000531441000000000000", + "000000000000000000000000478296900000000000000", + "000000000000000000000430467210000000000000000", + "000000000000000000387420489000000000000000000", + "000000000000000348678440100000000000000000000", + "000000000000313810596090000000000000000000000", + "000000000282429536481000000000000000000000000", + "000000254186582832900000000000000000000000000", + "000228767924549610000000000000000000000000000", + "205891132094649000000000000000000000000000000"}; + + private DecodedBitStreamParser() { + } + + static DecoderResult decode(int[] codewords) throws FormatException { + StringBuffer result = new StringBuffer(100); + // Get compaction mode + int codeIndex = 1; + int code = codewords[codeIndex++]; + while (codeIndex < codewords[0]) { + switch (code) { + case TEXT_COMPACTION_MODE_LATCH: { + codeIndex = textCompaction(codewords, codeIndex, result); + break; + } + case BYTE_COMPACTION_MODE_LATCH: { + codeIndex = byteCompaction(code, codewords, codeIndex, result); + break; + } + case NUMERIC_COMPACTION_MODE_LATCH: { + codeIndex = numericCompaction(codewords, codeIndex, result); + break; + } + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: { + codeIndex = byteCompaction(code, codewords, codeIndex, result); + break; + } + case BYTE_COMPACTION_MODE_LATCH_6: { + codeIndex = byteCompaction(code, codewords, codeIndex, result); + break; + } + default: { + // Default to text compaction. During testing numerous barcodes + // appeared to be missing the starting mode. In these cases defaulting + // to text compaction seems to work. + codeIndex--; + codeIndex = textCompaction(codewords, codeIndex, result); + break; + } + } + if (codeIndex < codewords.length) { + code = codewords[codeIndex++]; + } else { + throw FormatException.getFormatInstance(); + } + } + return new DecoderResult(null, result.toString(), null, null); + } + + /** + * Text Compaction mode (see 5.4.1.5) permits all printable ASCII characters to be + * encoded, i.e. values 32 - 126 inclusive in accordance with ISO/IEC 646 (IRV), as + * well as selected control characters. + * + * @param codewords The array of codewords (data + error) + * @param codeIndex The current index into the codeword array. + * @param result The decoded data is appended to the result. + * @return The next index into the codeword array. + */ + private static int textCompaction(int[] codewords, int codeIndex, StringBuffer result) { + // 2 character per codeword + int[] textCompactionData = new int[codewords[0] << 1]; + // Used to hold the byte compaction value if there is a mode shift + int[] byteCompactionData = new int[codewords[0] << 1]; + + int index = 0; + boolean end = false; + while ((codeIndex < codewords[0]) && !end) { + int code = codewords[codeIndex++]; + if (code < TEXT_COMPACTION_MODE_LATCH) { + textCompactionData[index] = code / 30; + textCompactionData[index + 1] = code % 30; + index += 2; + } else { + switch (code) { + case TEXT_COMPACTION_MODE_LATCH: { + codeIndex--; + end = true; + break; + } + case BYTE_COMPACTION_MODE_LATCH: { + codeIndex--; + end = true; + break; + } + case NUMERIC_COMPACTION_MODE_LATCH: { + codeIndex--; + end = true; + break; + } + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: { + // The Mode Shift codeword 913 shall cause a temporary + // switch from Text Compaction mode to Byte Compaction mode. + // This switch shall be in effect for only the next codeword, + // after which the mode shall revert to the prevailing sub-mode + // of the Text Compaction mode. Codeword 913 is only available + // in Text Compaction mode; its use is described in 5.4.2.4. + textCompactionData[index] = MODE_SHIFT_TO_BYTE_COMPACTION_MODE; + byteCompactionData[index] = code; //Integer.toHexString(code); + index++; + break; + } + case BYTE_COMPACTION_MODE_LATCH_6: { + codeIndex--; + end = true; + break; + } + } + } + } + decodeTextCompaction(textCompactionData, byteCompactionData, index, result); + return codeIndex; + } + + /** + * The Text Compaction mode includes all the printable ASCII characters + * (i.e. values from 32 to 126) and three ASCII control characters: HT or tab + * (ASCII value 9), LF or line feed (ASCII value 10), and CR or carriage + * return (ASCII value 13). The Text Compaction mode also includes various latch + * and shift characters which are used exclusively within the mode. The Text + * Compaction mode encodes up to 2 characters per codeword. The compaction rules + * for converting data into PDF417 codewords are defined in 5.4.2.2. The sub-mode + * switches are defined in 5.4.2.3. + * + * @param textCompactionData The text compaction data. + * @param byteCompactionData The byte compaction data if there + * was a mode shift. + * @param length The size of the text compaction and byte compaction data. + * @param result The decoded data is appended to the result. + */ + private static void decodeTextCompaction(int[] textCompactionData, + int[] byteCompactionData, + int length, + StringBuffer result) { + // Beginning from an initial state of the Alpha sub-mode + // The default compaction mode for PDF417 in effect at the start of each symbol shall always be Text + // Compaction mode Alpha sub-mode (uppercase alphabetic). A latch codeword from another mode to the Text + // Compaction mode shall always switch to the Text Compaction Alpha sub-mode. + int subMode = ALPHA; + int priorToShiftMode = ALPHA; + int i = 0; + while (i < length) { + int subModeCh = textCompactionData[i]; + char ch = 0; + switch (subMode) { + case ALPHA: + // Alpha (uppercase alphabetic) + if (subModeCh < 26) { + // Upper case Alpha Character + ch = (char) ('A' + subModeCh); + } else { + if (subModeCh == 26) { + ch = ' '; + } else if (subModeCh == LL) { + subMode = LOWER; + } else if (subModeCh == ML) { + subMode = MIXED; + } else if (subModeCh == PS) { + // Shift to punctuation + priorToShiftMode = subMode; + subMode = PUNCT_SHIFT; + } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) { + result.append((char) byteCompactionData[i]); + } + } + break; + + case LOWER: + // Lower (lowercase alphabetic) + if (subModeCh < 26) { + ch = (char) ('a' + subModeCh); + } else { + if (subModeCh == 26) { + ch = ' '; + } else if (subModeCh == AL) { + subMode = ALPHA; + } else if (subModeCh == ML) { + subMode = MIXED; + } else if (subModeCh == PS) { + // Shift to punctuation + priorToShiftMode = subMode; + subMode = PUNCT_SHIFT; + } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) { + result.append((char) byteCompactionData[i]); + } + } + break; + + case MIXED: + // Mixed (numeric and some punctuation) + if (subModeCh < PL) { + ch = MIXED_CHARS[subModeCh]; + } else { + if (subModeCh == PL) { + subMode = PUNCT; + } else if (subModeCh == 26) { + ch = ' '; + } else if (subModeCh == AS) { + //mode_change = true; + } else if (subModeCh == AL) { + subMode = ALPHA; + } else if (subModeCh == PS) { + // Shift to punctuation + priorToShiftMode = subMode; + subMode = PUNCT_SHIFT; + } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) { + result.append((char) byteCompactionData[i]); + } + } + break; + + case PUNCT: + // Punctuation + if (subModeCh < PS) { + ch = PUNCT_CHARS[subModeCh]; + } else { + if (subModeCh == PAL) { + subMode = ALPHA; + } else if (subModeCh == MODE_SHIFT_TO_BYTE_COMPACTION_MODE) { + result.append((char) byteCompactionData[i]); + } + } + break; + + case PUNCT_SHIFT: + // Restore sub-mode + subMode = priorToShiftMode; + if (subModeCh < PS) { + ch = PUNCT_CHARS[subModeCh]; + } else { + if (subModeCh == PAL) { + subMode = ALPHA; + } + } + break; + } + if (ch != 0) { + // Append decoded character to result + result.append(ch); + } + i++; + } + } + + /** + * Byte Compaction mode (see 5.4.3) permits all 256 possible 8-bit byte values to be encoded. + * This includes all ASCII characters value 0 to 127 inclusive and provides for international + * character set support. + * + * @param mode The byte compaction mode i.e. 901 or 924 + * @param codewords The array of codewords (data + error) + * @param codeIndex The current index into the codeword array. + * @param result The decoded data is appended to the result. + * @return The next index into the codeword array. + */ + private static int byteCompaction(int mode, int[] codewords, int codeIndex, StringBuffer result) { + if (mode == BYTE_COMPACTION_MODE_LATCH) { + // Total number of Byte Compaction characters to be encoded + // is not a multiple of 6 + int count = 0; + long value = 0; + char[] decodedData = new char[6]; + int[] byteCompactedCodewords = new int[6]; + boolean end = false; + while ((codeIndex < codewords[0]) && !end) { + int code = codewords[codeIndex++]; + if (code < TEXT_COMPACTION_MODE_LATCH) { + byteCompactedCodewords[count] = code; + count++; + // Base 900 + value = 900 * value + code; + } else { + if (code == TEXT_COMPACTION_MODE_LATCH || + code == BYTE_COMPACTION_MODE_LATCH || + code == NUMERIC_COMPACTION_MODE_LATCH || + code == BYTE_COMPACTION_MODE_LATCH_6 || + code == BEGIN_MACRO_PDF417_CONTROL_BLOCK || + code == BEGIN_MACRO_PDF417_OPTIONAL_FIELD || + code == MACRO_PDF417_TERMINATOR) { + codeIndex--; + end = true; + } + } + if ((count % 5 == 0) && (count > 0)) { + // Decode every 5 codewords + // Convert to Base 256 + for (int j = 0; j < 6; ++j) { + decodedData[5 - j] = (char) (value % 256); + value >>= 8; + } + result.append(decodedData); + count = 0; + } + } + // If Byte Compaction mode is invoked with codeword 901, + // the final group of codewords is interpreted directly + // as one byte per codeword, without compaction. + for (int i = ((count / 5) * 5); i < count; i++) { + result.append((char) byteCompactedCodewords[i]); + } + + } else if (mode == BYTE_COMPACTION_MODE_LATCH_6) { + // Total number of Byte Compaction characters to be encoded + // is an integer multiple of 6 + int count = 0; + long value = 0; + boolean end = false; + while (codeIndex < codewords[0] && !end) { + int code = codewords[codeIndex++]; + if (code < TEXT_COMPACTION_MODE_LATCH) { + count++; + // Base 900 + value = 900 * value + code; + } else { + if (code == TEXT_COMPACTION_MODE_LATCH || + code == BYTE_COMPACTION_MODE_LATCH || + code == NUMERIC_COMPACTION_MODE_LATCH || + code == BYTE_COMPACTION_MODE_LATCH_6 || + code == BEGIN_MACRO_PDF417_CONTROL_BLOCK || + code == BEGIN_MACRO_PDF417_OPTIONAL_FIELD || + code == MACRO_PDF417_TERMINATOR) { + codeIndex--; + end = true; + } + } + if ((count % 5 == 0) && (count > 0)) { + // Decode every 5 codewords + // Convert to Base 256 + char[] decodedData = new char[6]; + for (int j = 0; j < 6; ++j) { + decodedData[5 - j] = (char) (value & 0xFF); + value >>= 8; + } + result.append(decodedData); + } + } + } + return codeIndex; + } + + /** + * Numeric Compaction mode (see 5.4.4) permits efficient encoding of numeric data strings. + * + * @param codewords The array of codewords (data + error) + * @param codeIndex The current index into the codeword array. + * @param result The decoded data is appended to the result. + * @return The next index into the codeword array. + */ + private static int numericCompaction(int[] codewords, int codeIndex, StringBuffer result) { + int count = 0; + boolean end = false; + + int[] numericCodewords = new int[MAX_NUMERIC_CODEWORDS]; + + while (codeIndex < codewords[0] && !end) { + int code = codewords[codeIndex++]; + if (codeIndex == codewords[0]) { + end = true; + } + if (code < TEXT_COMPACTION_MODE_LATCH) { + numericCodewords[count] = code; + count++; + } else { + if (code == TEXT_COMPACTION_MODE_LATCH || + code == BYTE_COMPACTION_MODE_LATCH || + code == BYTE_COMPACTION_MODE_LATCH_6 || + code == BEGIN_MACRO_PDF417_CONTROL_BLOCK || + code == BEGIN_MACRO_PDF417_OPTIONAL_FIELD || + code == MACRO_PDF417_TERMINATOR) { + codeIndex--; + end = true; + } + } + if (count % MAX_NUMERIC_CODEWORDS == 0 || + code == NUMERIC_COMPACTION_MODE_LATCH || + end) { + // Re-invoking Numeric Compaction mode (by using codeword 902 + // while in Numeric Compaction mode) serves to terminate the + // current Numeric Compaction mode grouping as described in 5.4.4.2, + // and then to start a new one grouping. + String s = decodeBase900toBase10(numericCodewords, count); + result.append(s); + count = 0; + } + } + return codeIndex; + } + + /** + * Convert a list of Numeric Compacted codewords from Base 900 to Base 10. + * + * @param codewords The array of codewords + * @param count The number of codewords + * @return The decoded string representing the Numeric data. + */ + /* + EXAMPLE + Encode the fifteen digit numeric string 000213298174000 + Prefix the numeric string with a 1 and set the initial value of + t = 1 000 213 298 174 000 + Calculate codeword 0 + d0 = 1 000 213 298 174 000 mod 900 = 200 + + t = 1 000 213 298 174 000 div 900 = 1 111 348 109 082 + Calculate codeword 1 + d1 = 1 111 348 109 082 mod 900 = 282 + + t = 1 111 348 109 082 div 900 = 1 234 831 232 + Calculate codeword 2 + d2 = 1 234 831 232 mod 900 = 632 + + t = 1 234 831 232 div 900 = 1 372 034 + Calculate codeword 3 + d3 = 1 372 034 mod 900 = 434 + + t = 1 372 034 div 900 = 1 524 + Calculate codeword 4 + d4 = 1 524 mod 900 = 624 + + t = 1 524 div 900 = 1 + Calculate codeword 5 + d5 = 1 mod 900 = 1 + t = 1 div 900 = 0 + Codeword sequence is: 1, 624, 434, 632, 282, 200 + + Decode the above codewords involves + 1 x 900 power of 5 + 624 x 900 power of 4 + 434 x 900 power of 3 + + 632 x 900 power of 2 + 282 x 900 power of 1 + 200 x 900 power of 0 = 1000213298174000 + + Remove leading 1 => Result is 000213298174000 + + As there are huge numbers involved here we must use fake out the maths using string + tokens for the numbers. + BigDecimal is not supported by J2ME. + */ + private static String decodeBase900toBase10(int[] codewords, int count) { + StringBuffer accum = null; + for (int i = 0; i < count; i++) { + StringBuffer value = multiply(EXP900[count - i - 1], codewords[i]); + if (accum == null) { + // First time in accum=0 + accum = value; + } else { + accum = add(accum.toString(), value.toString()); + } + } + String result = null; + // Remove leading '1' which was inserted to preserve + // leading zeros + for (int i = 0; i < accum.length(); i++) { + if (accum.charAt(i) == '1') { + //result = accum.substring(i + 1); + result = accum.toString().substring(i + 1); + break; + } + } + if (result == null) { + // No leading 1 => just write the converted number. + result = accum.toString(); + } + return result; + } + + /** + * Multiplies two String numbers + * + * @param value1 Any number represented as a string. + * @param value2 A number <= 999. + * @return the result of value1 * value2. + */ + private static StringBuffer multiply(String value1, int value2) { + StringBuffer result = new StringBuffer(value1.length()); + for (int i = 0; i < value1.length(); i++) { + // Put zeros into the result. + result.append('0'); + } + int hundreds = value2 / 100; + int tens = (value2 / 10) % 10; + int ones = value2 % 10; + // Multiply by ones + for (int j = 0; j < ones; j++) { + result = add(result.toString(), value1); + } + // Multiply by tens + for (int j = 0; j < tens; j++) { + result = add(result.toString(), (value1 + '0').substring(1)); + } + // Multiply by hundreds + for (int j = 0; j < hundreds; j++) { + result = add(result.toString(), (value1 + "00").substring(2)); + } + return result; + } + + /** + * Add two numbers which are represented as strings. + * + * @param value1 + * @param value2 + * @return the result of value1 + value2 + */ + private static StringBuffer add(String value1, String value2) { + StringBuffer temp1 = new StringBuffer(5); + StringBuffer temp2 = new StringBuffer(5); + StringBuffer result = new StringBuffer(value1.length()); + for (int i = 0; i < value1.length(); i++) { + // Put zeros into the result. + result.append('0'); + } + int carry = 0; + for (int i = value1.length() - 3; i > -1; i -= 3) { + + temp1.setLength(0); + temp1.append(value1.charAt(i)); + temp1.append(value1.charAt(i + 1)); + temp1.append(value1.charAt(i + 2)); + + temp2.setLength(0); + temp2.append(value2.charAt(i)); + temp2.append(value2.charAt(i + 1)); + temp2.append(value2.charAt(i + 2)); + + int intValue1 = Integer.parseInt(temp1.toString()); + int intValue2 = Integer.parseInt(temp2.toString()); + + int sumval = (intValue1 + intValue2 + carry) % 1000; + carry = (intValue1 + intValue2 + carry) / 1000; + + result.setCharAt(i + 2, (char) ((sumval % 10) + '0')); + result.setCharAt(i + 1, (char) (((sumval / 10) % 10) + '0')); + result.setCharAt(i, (char) ((sumval / 100) + '0')); + } + return result; + } + + /* + private static String decodeBase900toBase10(int codewords[], int count) { + BigInteger accum = BigInteger.valueOf(0); + BigInteger value = null; + for (int i = 0; i < count; i++) { + value = BigInteger.valueOf(900).pow(count - i - 1); + value = value.multiply(BigInteger.valueOf(codewords[i])); + accum = accum.add(value); + } + if (debug) System.out.println("Big Integer " + accum); + String result = accum.toString().substring(1); + return result; + } + */ +} diff --git a/src/com/google/zxing/pdf417/decoder/Decoder.java b/src/com/google/zxing/pdf417/decoder/Decoder.java new file mode 100644 index 0000000..a3d13fd --- /dev/null +++ b/src/com/google/zxing/pdf417/decoder/Decoder.java @@ -0,0 +1,150 @@ +/* + * Copyright 2009 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.pdf417.decoder; + +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +//import com.google.zxing.pdf417.reedsolomon.ReedSolomonDecoder; + +/** + *The main class which implements PDF417 Code decoding -- as + * opposed to locating and extracting the PDF417 Code from an image.
+ * + * @author SITA Lab (kevin.osullivan@sita.aero) + */ +public final class Decoder { + + private static final int MAX_ERRORS = 3; + private static final int MAX_EC_CODEWORDS = 512; + //private final ReedSolomonDecoder rsDecoder; + + public Decoder() { + // TODO MGMG + //rsDecoder = new ReedSolomonDecoder(); + } + + /** + *Convenience method that can decode a PDF417 Code represented as a 2D array of booleans. + * "true" is taken to mean a black module.
+ * + * @param image booleans representing white/black PDF417 modules + * @return text and bytes encoded within the PDF417 Code + * @throws NotFoundException if the PDF417 Code cannot be decoded + */ + public DecoderResult decode(boolean[][] image) throws FormatException { + int dimension = image.length; + BitMatrix bits = new BitMatrix(dimension); + for (int i = 0; i < dimension; i++) { + for (int j = 0; j < dimension; j++) { + if (image[j][i]) { + bits.set(j, i); + } + } + } + return decode(bits); + } + + /** + *Decodes a PDF417 Code represented as a {@link BitMatrix}. + * A 1 or "true" is taken to mean a black module.
+ * + * @param bits booleans representing white/black PDF417 Code modules + * @return text and bytes encoded within the PDF417 Code + * @throws FormatException if the PDF417 Code cannot be decoded + */ + public DecoderResult decode(BitMatrix bits) throws FormatException { + // Construct a parser to read the data codewords and error-correction level + BitMatrixParser parser = new BitMatrixParser(bits); + int[] codewords = parser.readCodewords(); + if (codewords == null || codewords.length == 0) { + throw FormatException.getFormatInstance(); + } + + int ecLevel = parser.getECLevel(); + int numECCodewords = 1 << (ecLevel + 1); + int[] erasures = parser.getErasures(); + + correctErrors(codewords, erasures, numECCodewords); + verifyCodewordCount(codewords, numECCodewords); + + // Decode the codewords + return DecodedBitStreamParser.decode(codewords); + } + + /** + * Verify that all is OK with the codeword array. + * + * @param codewords + * @return an index to the first data codeword. + * @throws FormatException + */ + private static void verifyCodewordCount(int[] codewords, int numECCodewords) throws FormatException { + if (codewords.length < 4) { + // Codeword array size should be at least 4 allowing for + // Count CW, At least one Data CW, Error Correction CW, Error Correction CW + throw FormatException.getFormatInstance(); + } + // The first codeword, the Symbol Length Descriptor, shall always encode the total number of data + // codewords in the symbol, including the Symbol Length Descriptor itself, data codewords and pad + // codewords, but excluding the number of error correction codewords. + int numberOfCodewords = codewords[0]; + if (numberOfCodewords > codewords.length) { + throw FormatException.getFormatInstance(); + } + if (numberOfCodewords == 0) { + // Reset to the length of the array - 8 (Allow for at least level 3 Error Correction (8 Error Codewords) + if (numECCodewords < codewords.length) { + codewords[0] = codewords.length - numECCodewords; + } else { + throw FormatException.getFormatInstance(); + } + } + } + + /** + *Given data and error-correction codewords received, possibly corrupted by errors, attempts to + * correct the errors in-place using Reed-Solomon error correction.
+ * + * @param codewords data and error correction codewords + * @throws ChecksumException if error correction fails + */ + private static int correctErrors(int[] codewords, int[] erasures, int numECCodewords) throws FormatException { + if ((erasures != null && erasures.length > numECCodewords / 2 + MAX_ERRORS) || + (numECCodewords < 0 || numECCodewords > MAX_EC_CODEWORDS)) { + // Too many errors or EC Codewords is corrupted + throw FormatException.getFormatInstance(); + } + // Try to correct the errors + // TODO enable error correction + int result = 0; // rsDecoder.correctErrors(codewords, numECCodewords); + if (erasures != null) { + int numErasures = erasures.length; + if (result > 0) { + numErasures -= result; + } + if (numErasures > MAX_ERRORS) { + // Still too many errors + throw FormatException.getFormatInstance(); + } + } + return result; + } + +} diff --git a/src/com/google/zxing/pdf417/detector/Detector.java b/src/com/google/zxing/pdf417/detector/Detector.java new file mode 100644 index 0000000..a8ceef0 --- /dev/null +++ b/src/com/google/zxing/pdf417/detector/Detector.java @@ -0,0 +1,498 @@ +/* + * Copyright 2009 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.pdf417.detector; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.common.GridSampler; + +import java.util.Hashtable; + +/** + *Encapsulates logic that can detect a PDF417 Code in an image, even if the + * PDF417 Code is rotated or skewed, or partially obscured.
+ * + * @author SITA Lab (kevin.osullivan@sita.aero) + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class Detector { + + private static final int MAX_AVG_VARIANCE = (int) ((1 << 8) * 0.42f); + private static final int MAX_INDIVIDUAL_VARIANCE = (int) ((1 << 8) * 0.8f); + private static final int SKEW_THRESHOLD = 2; + + // B S B S B S B S Bar/Space pattern + // 11111111 0 1 0 1 0 1 000 + private static final int[] START_PATTERN = {8, 1, 1, 1, 1, 1, 1, 3}; + + // 11111111 0 1 0 1 0 1 000 + private static final int[] START_PATTERN_REVERSE = {3, 1, 1, 1, 1, 1, 1, 8}; + + // 1111111 0 1 000 1 0 1 00 1 + private static final int[] STOP_PATTERN = {7, 1, 1, 3, 1, 1, 1, 2, 1}; + + // B S B S B S B S B Bar/Space pattern + // 1111111 0 1 000 1 0 1 00 1 + private static final int[] STOP_PATTERN_REVERSE = {1, 2, 1, 1, 1, 3, 1, 1, 7}; + + private final BinaryBitmap image; + + public Detector(BinaryBitmap image) { + this.image = image; + } + + /** + *Detects a PDF417 Code in an image, simply.
+ * + * @return {@link DetectorResult} encapsulating results of detecting a PDF417 Code + * @throws NotFoundException if no QR Code can be found + */ + public DetectorResult detect() throws NotFoundException { + return detect(null); + } + + /** + *Detects a PDF417 Code in an image. Only checks 0 and 180 degree rotations.
+ * + * @param hints optional hints to detector + * @return {@link DetectorResult} encapsulating results of detecting a PDF417 Code + * @throws NotFoundException if no PDF417 Code can be found + */ + public DetectorResult detect(Hashtable hints) throws NotFoundException { + // Fetch the 1 bit matrix once up front. + BitMatrix matrix = image.getBlackMatrix(); + + // Try to find the vertices assuming the image is upright. + ResultPoint[] vertices = findVertices(matrix); + if (vertices == null) { + // Maybe the image is rotated 180 degrees? + vertices = findVertices180(matrix); + if (vertices != null) { + correctCodeWordVertices(vertices, true); + } + } else { + correctCodeWordVertices(vertices, false); + } + + if (vertices == null) { + throw NotFoundException.getNotFoundInstance(); + } + + float moduleWidth = computeModuleWidth(vertices); + if (moduleWidth < 1.0f) { + throw NotFoundException.getNotFoundInstance(); + } + + int dimension = computeDimension(vertices[4], vertices[6], + vertices[5], vertices[7], moduleWidth); + if (dimension < 1) { + throw NotFoundException.getNotFoundInstance(); + } + + // Deskew and sample image. + BitMatrix bits = sampleGrid(matrix, vertices[4], vertices[5], + vertices[6], vertices[7], dimension); + return new DetectorResult(bits, new ResultPoint[]{vertices[4], + vertices[5], vertices[6], vertices[7]}); + } + + /** + * Locate the vertices and the codewords area of a black blob using the Start + * and Stop patterns as locators. + * TODO: Scanning every row is very expensive. We should only do this for TRY_HARDER. + * + * @param matrix the scanned barcode image. + * @return an array containing the vertices: + * vertices[0] x, y top left barcode + * vertices[1] x, y bottom left barcode + * vertices[2] x, y top right barcode + * vertices[3] x, y bottom right barcode + * vertices[4] x, y top left codeword area + * vertices[5] x, y bottom left codeword area + * vertices[6] x, y top right codeword area + * vertices[7] x, y bottom right codeword area + */ + private static ResultPoint[] findVertices(BitMatrix matrix) { + int height = matrix.getHeight(); + int width = matrix.getWidth(); + + ResultPoint[] result = new ResultPoint[8]; + boolean found = false; + + // Top Left + for (int i = 0; i < height; i++) { + int[] loc = findGuardPattern(matrix, 0, i, width, false, START_PATTERN); + if (loc != null) { + result[0] = new ResultPoint(loc[0], i); + result[4] = new ResultPoint(loc[1], i); + found = true; + break; + } + } + // Bottom left + if (found) { // Found the Top Left vertex + found = false; + for (int i = height - 1; i > 0; i--) { + int[] loc = findGuardPattern(matrix, 0, i, width, false, START_PATTERN); + if (loc != null) { + result[1] = new ResultPoint(loc[0], i); + result[5] = new ResultPoint(loc[1], i); + found = true; + break; + } + } + } + // Top right + if (found) { // Found the Bottom Left vertex + found = false; + for (int i = 0; i < height; i++) { + int[] loc = findGuardPattern(matrix, 0, i, width, false, STOP_PATTERN); + if (loc != null) { + result[2] = new ResultPoint(loc[1], i); + result[6] = new ResultPoint(loc[0], i); + found = true; + break; + } + } + } + // Bottom right + if (found) { // Found the Top right vertex + found = false; + for (int i = height - 1; i > 0; i--) { + int[] loc = findGuardPattern(matrix, 0, i, width, false, STOP_PATTERN); + if (loc != null) { + result[3] = new ResultPoint(loc[1], i); + result[7] = new ResultPoint(loc[0], i); + found = true; + break; + } + } + } + return found ? result : null; + } + + /** + * Locate the vertices and the codewords area of a black blob using the Start + * and Stop patterns as locators. This assumes that the image is rotated 180 + * degrees and if it locates the start and stop patterns at it will re-map + * the vertices for a 0 degree rotation. + * TODO: Change assumption about barcode location. + * TODO: Scanning every row is very expensive. We should only do this for TRY_HARDER. + * + * @param matrix the scanned barcode image. + * @return an array containing the vertices: + * vertices[0] x, y top left barcode + * vertices[1] x, y bottom left barcode + * vertices[2] x, y top right barcode + * vertices[3] x, y bottom right barcode + * vertices[4] x, y top left codeword area + * vertices[5] x, y bottom left codeword area + * vertices[6] x, y top right codeword area + * vertices[7] x, y bottom right codeword area + */ + private static ResultPoint[] findVertices180(BitMatrix matrix) { + int height = matrix.getHeight(); + int width = matrix.getWidth(); + int halfWidth = width >> 1; + + ResultPoint[] result = new ResultPoint[8]; + boolean found = false; + + // Top Left + for (int i = height - 1; i > 0; i--) { + int[] loc = findGuardPattern(matrix, halfWidth, i, halfWidth, true, START_PATTERN_REVERSE); + if (loc != null) { + result[0] = new ResultPoint(loc[1], i); + result[4] = new ResultPoint(loc[0], i); + found = true; + break; + } + } + // Bottom Left + if (found) { // Found the Top Left vertex + found = false; + for (int i = 0; i < height; i++) { + int[] loc = findGuardPattern(matrix, halfWidth, i, halfWidth, true, START_PATTERN_REVERSE); + if (loc != null) { + result[1] = new ResultPoint(loc[1], i); + result[5] = new ResultPoint(loc[0], i); + found = true; + break; + } + } + } + // Top Right + if (found) { // Found the Bottom Left vertex + found = false; + for (int i = height - 1; i > 0; i--) { + int[] loc = findGuardPattern(matrix, 0, i, halfWidth, false, STOP_PATTERN_REVERSE); + if (loc != null) { + result[2] = new ResultPoint(loc[0], i); + result[6] = new ResultPoint(loc[1], i); + found = true; + break; + } + } + } + // Bottom Right + if (found) { // Found the Top Right vertex + found = false; + for (int i = 0; i < height; i++) { + int[] loc = findGuardPattern(matrix, 0, i, halfWidth, false, STOP_PATTERN_REVERSE); + if (loc != null) { + result[3] = new ResultPoint(loc[0], i); + result[7] = new ResultPoint(loc[1], i); + found = true; + break; + } + } + } + return found ? result : null; + } + + /** + * Because we scan horizontally to detect the start and stop patterns, the vertical component of + * the codeword coordinates will be slightly wrong if there is any skew or rotation in the image. + * This method moves those points back onto the edges of the theoretically perfect bounding + * quadrilateral if needed. + * + * @param vertices The eight vertices located by findVertices(). + */ + private static void correctCodeWordVertices(ResultPoint[] vertices, boolean upsideDown) { + float skew = vertices[4].getY() - vertices[6].getY(); + if (upsideDown) { + skew = -skew; + } + if (skew > SKEW_THRESHOLD) { + // Fix v4 + float length = vertices[4].getX() - vertices[0].getX(); + float deltax = vertices[6].getX() - vertices[0].getX(); + float deltay = vertices[6].getY() - vertices[0].getY(); + float correction = length * deltay / deltax; + vertices[4] = new ResultPoint(vertices[4].getX(), vertices[4].getY() + correction); + } else if (-skew > SKEW_THRESHOLD) { + // Fix v6 + float length = vertices[2].getX() - vertices[6].getX(); + float deltax = vertices[2].getX() - vertices[4].getX(); + float deltay = vertices[2].getY() - vertices[4].getY(); + float correction = length * deltay / deltax; + vertices[6] = new ResultPoint(vertices[6].getX(), vertices[6].getY() - correction); + } + + skew = vertices[7].getY() - vertices[5].getY(); + if (upsideDown) { + skew = -skew; + } + if (skew > SKEW_THRESHOLD) { + // Fix v5 + float length = vertices[5].getX() - vertices[1].getX(); + float deltax = vertices[7].getX() - vertices[1].getX(); + float deltay = vertices[7].getY() - vertices[1].getY(); + float correction = length * deltay / deltax; + vertices[5] = new ResultPoint(vertices[5].getX(), vertices[5].getY() + correction); + } else if (-skew > SKEW_THRESHOLD) { + // Fix v7 + float length = vertices[3].getX() - vertices[7].getX(); + float deltax = vertices[3].getX() - vertices[5].getX(); + float deltay = vertices[3].getY() - vertices[5].getY(); + float correction = length * deltay / deltax; + vertices[7] = new ResultPoint(vertices[7].getX(), vertices[7].getY() - correction); + } + } + + /** + *Estimates module size (pixels in a module) based on the Start and End + * finder patterns.
+ * + * @param vertices an array of vertices: + * vertices[0] x, y top left barcode + * vertices[1] x, y bottom left barcode + * vertices[2] x, y top right barcode + * vertices[3] x, y bottom right barcode + * vertices[4] x, y top left codeword area + * vertices[5] x, y bottom left codeword area + * vertices[6] x, y top right codeword area + * vertices[7] x, y bottom right codeword area + * @return the module size. + */ + private static float computeModuleWidth(ResultPoint[] vertices) { + float pixels1 = ResultPoint.distance(vertices[0], vertices[4]); + float pixels2 = ResultPoint.distance(vertices[1], vertices[5]); + float moduleWidth1 = (pixels1 + pixels2) / (17 * 2.0f); + float pixels3 = ResultPoint.distance(vertices[6], vertices[2]); + float pixels4 = ResultPoint.distance(vertices[7], vertices[3]); + float moduleWidth2 = (pixels3 + pixels4) / (18 * 2.0f); + return (moduleWidth1 + moduleWidth2) / 2.0f; + } + + /** + * Computes the dimension (number of modules in a row) of the PDF417 Code + * based on vertices of the codeword area and estimated module size. + * + * @param topLeft of codeword area + * @param topRight of codeword area + * @param bottomLeft of codeword area + * @param bottomRight of codeword are + * @param moduleWidth estimated module size + * @return the number of modules in a row. + */ + private static int computeDimension(ResultPoint topLeft, ResultPoint topRight, + ResultPoint bottomLeft, ResultPoint bottomRight, float moduleWidth) { + int topRowDimension = round(ResultPoint.distance(topLeft, topRight) / moduleWidth); + int bottomRowDimension = round(ResultPoint.distance(bottomLeft, bottomRight) / moduleWidth); + return ((((topRowDimension + bottomRowDimension) >> 1) + 8) / 17) * 17; + /* + * int topRowDimension = round(ResultPoint.distance(topLeft, + * topRight)); //moduleWidth); int bottomRowDimension = + * round(ResultPoint.distance(bottomLeft, bottomRight)); // + * moduleWidth); int dimension = ((topRowDimension + bottomRowDimension) + * >> 1); // Round up to nearest 17 modules i.e. there are 17 modules per + * codeword //int dimension = ((((topRowDimension + bottomRowDimension) >> + * 1) + 8) / 17) * 17; return dimension; + */ + } + + private static BitMatrix sampleGrid(BitMatrix matrix, ResultPoint topLeft, + ResultPoint bottomLeft, ResultPoint topRight, ResultPoint bottomRight, int dimension) + throws NotFoundException { + + // Note that unlike the QR Code sampler, we didn't find the center of modules, but the + // very corners. So there is no 0.5f here; 0.0f is right. + GridSampler sampler = GridSampler.getInstance(); + + return sampler.sampleGrid(matrix, dimension, 0.0f, // p1ToX + 0.0f, // p1ToY + dimension, // p2ToX + 0.0f, // p2ToY + dimension, // p3ToX + dimension, // p3ToY + 0.0f, // p4ToX + dimension, // p4ToY + topLeft.getX(), // p1FromX + topLeft.getY(), // p1FromY + topRight.getX(), // p2FromX + topRight.getY(), // p2FromY + bottomRight.getX(), // p3FromX + bottomRight.getY(), // p3FromY + bottomLeft.getX(), // p4FromX + bottomLeft.getY()); // p4FromY + } + + /** + * Ends up being a bit faster than Math.round(). This merely rounds its + * argument to the nearest int, where x.5 rounds up. + */ + private static int round(float d) { + return (int) (d + 0.5f); + } + + /** + * @param matrix row of black/white values to search + * @param column x position to start search + * @param row y position to start search + * @param width the number of pixels to search on this row + * @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. + */ + private static int[] findGuardPattern(BitMatrix matrix, int column, int row, int width, + boolean whiteFirst, int[] pattern) { + int patternLength = pattern.length; + // TODO: Find a way to cache this array, as this method is called hundreds of times + // per image, and we want to allocate as seldom as possible. + int[] counters = new int[patternLength]; + boolean isWhite = whiteFirst; + + int counterPosition = 0; + int patternStart = column; + for (int x = column; x < column + width; x++) { + boolean pixel = matrix.get(x, row); + if (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; + } + } + return null; + } + + /** + * Determines how closely a set of observed counts of runs of black/white + * values matches a given target pattern. This is reported as the ratio of + * the total variance from the expected pattern proportions across all + * pattern elements, to the length of the pattern. + * + * @param counters observed counters + * @param pattern expected pattern + * @param maxIndividualVariance The most any counter can differ before we give up + * @return ratio of total variance between counters and pattern compared to + * total pattern size, where the ratio has been multiplied by 256. + * So, 0 means no variance (perfect match); 256 means the total + * variance between counters and patterns equals the pattern length, + * higher values mean even more variance + */ + private static int patternMatchVariance(int[] counters, int[] pattern, int maxIndividualVariance) { + int numCounters = counters.length; + int total = 0; + int patternLength = 0; + for (int i = 0; i < numCounters; i++) { + total += counters[i]; + patternLength += pattern[i]; + } + if (total < patternLength) { + // If we don't even have one pixel per unit of bar width, assume this + // is too small to reliably match, so fail: + return Integer.MAX_VALUE; + } + // We're going to fake floating-point math in integers. We just need to use more bits. + // Scale up patternLength so that intermediate values below like scaledCounter will have + // more "significant digits". + int unitBarWidth = (total << 8) / patternLength; + maxIndividualVariance = (maxIndividualVariance * unitBarWidth) >> 8; + + int totalVariance = 0; + for (int x = 0; x < numCounters; x++) { + int counter = counters[x] << 8; + int scaledPattern = pattern[x] * unitBarWidth; + int variance = counter > scaledPattern ? counter - scaledPattern : scaledPattern - counter; + if (variance > maxIndividualVariance) { + return Integer.MAX_VALUE; + } + totalVariance += variance; + } + return totalVariance / total; + } + +} diff --git a/src/com/google/zxing/qrcode/QRCodeReader.java b/src/com/google/zxing/qrcode/QRCodeReader.java new file mode 100644 index 0000000..2a12c6d --- /dev/null +++ b/src/com/google/zxing/qrcode/QRCodeReader.java @@ -0,0 +1,166 @@ +/* + * Copyright 2007 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.qrcode; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.qrcode.decoder.Decoder; +import com.google.zxing.qrcode.detector.Detector; + +import java.util.Hashtable; + +/** + * This implementation can detect and decode QR Codes in an image. + * + * @author Sean Owen + */ +public class QRCodeReader implements Reader { + + private static final ResultPoint[] NO_POINTS = new ResultPoint[0]; + + private final Decoder decoder = new Decoder(); + + protected Decoder getDecoder() { + return decoder; + } + + /** + * Locates and decodes a QR code in an image. + * + * @return a String representing the content encoded by the QR code + * @throws NotFoundException if a QR code cannot be found + * @throws FormatException if a QR code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public Result decode(BinaryBitmap image) throws NotFoundException, ChecksumException, FormatException { + return decode(image, null); + } + + public Result decode(BinaryBitmap image, Hashtable hints) + throws NotFoundException, ChecksumException, FormatException { + DecoderResult decoderResult; + ResultPoint[] points; + if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) { + BitMatrix bits = extractPureBits(image.getBlackMatrix()); + decoderResult = decoder.decode(bits, hints); + points = NO_POINTS; + } else { + DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(hints); + decoderResult = decoder.decode(detectorResult.getBits(), hints); + points = detectorResult.getPoints(); + } + + Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.QR_CODE); + if (decoderResult.getByteSegments() != null) { + result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, decoderResult.getByteSegments()); + } + if (decoderResult.getECLevel() != null) { + result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, decoderResult.getECLevel().toString()); + } + return result; + } + + public void reset() { + // do nothing + } + + /** + * This method detects a barcode in a "pure" image -- that is, pure monochrome image + * which contains only an unrotated, unskewed, image of a barcode, with some white border + * around it. This is a specialized method that works exceptionally fast in this special + * case. + */ + public static BitMatrix extractPureBits(BitMatrix image) throws NotFoundException { + + int height = image.getHeight(); + int width = image.getWidth(); + int minDimension = Math.min(height, width); + + // And then keep tracking across the top-left black module to determine module size + //int moduleEnd = borderWidth; + int[] leftTopBlack = image.getTopLeftOnBit(); + if (leftTopBlack == null) { + throw NotFoundException.getNotFoundInstance(); + } + int x = leftTopBlack[0]; + int y = leftTopBlack[1]; + while (x < minDimension && y < minDimension && image.get(x, y)) { + x++; + y++; + } + if (x == minDimension || y == minDimension) { + throw NotFoundException.getNotFoundInstance(); + } + + int moduleSize = x - leftTopBlack[0]; + if (moduleSize == 0) { + throw NotFoundException.getNotFoundInstance(); + } + + // And now find where the rightmost black module on the first row ends + int rowEndOfSymbol = width - 1; + while (rowEndOfSymbol > x && !image.get(rowEndOfSymbol, y)) { + rowEndOfSymbol--; + } + if (rowEndOfSymbol <= x) { + throw NotFoundException.getNotFoundInstance(); + } + rowEndOfSymbol++; + + // Make sure width of barcode is a multiple of module size + if ((rowEndOfSymbol - x) % moduleSize != 0) { + throw NotFoundException.getNotFoundInstance(); + } + int dimension = 1 + ((rowEndOfSymbol - x) / moduleSize); + + // Push in the "border" by half the module width so that we start + // sampling in the middle of the module. Just in case the image is a + // little off, this will help recover. Need to back up at least 1. + int backOffAmount = moduleSize == 1 ? 1 : moduleSize >> 1; + x -= backOffAmount; + y -= backOffAmount; + + if ((x + (dimension - 1) * moduleSize) >= width || + (y + (dimension - 1) * moduleSize) >= height) { + throw NotFoundException.getNotFoundInstance(); + } + + // Now just read off the bits + BitMatrix bits = new BitMatrix(dimension); + for (int i = 0; i < dimension; i++) { + int iOffset = y + i * moduleSize; + for (int j = 0; j < dimension; j++) { + if (image.get(x + j * moduleSize, iOffset)) { + bits.set(j, i); + } + } + } + return bits; + } + +} diff --git a/src/com/google/zxing/qrcode/QRCodeWriter.java b/src/com/google/zxing/qrcode/QRCodeWriter.java new file mode 100644 index 0000000..c0a5e0a --- /dev/null +++ b/src/com/google/zxing/qrcode/QRCodeWriter.java @@ -0,0 +1,108 @@ +/* + * 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.qrcode; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.Writer; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.encoder.ByteMatrix; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.encoder.Encoder; +import com.google.zxing.qrcode.encoder.QRCode; + +import java.util.Hashtable; + +/** + * This object renders a QR Code as a BitMatrix 2D array of greyscale values. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class QRCodeWriter implements Writer { + + private static final int QUIET_ZONE_SIZE = 4; + + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) + throws WriterException { + + return encode(contents, format, width, height, null); + } + + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height, + Hashtable hints) throws WriterException { + + if (contents == null || contents.length() == 0) { + throw new IllegalArgumentException("Found empty contents"); + } + + if (format != BarcodeFormat.QR_CODE) { + throw new IllegalArgumentException("Can only encode QR_CODE, but got " + format); + } + + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Requested dimensions are too small: " + width + 'x' + + height); + } + + ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L; + if (hints != null) { + ErrorCorrectionLevel requestedECLevel = (ErrorCorrectionLevel) hints.get(EncodeHintType.ERROR_CORRECTION); + if (requestedECLevel != null) { + errorCorrectionLevel = requestedECLevel; + } + } + + QRCode code = new QRCode(); + Encoder.encode(contents, errorCorrectionLevel, hints, code); + return renderResult(code, width, height); + } + + // Note that the input matrix uses 0 == white, 1 == black, while the output matrix uses + // 0 == black, 255 == white (i.e. an 8 bit greyscale bitmap). + private static BitMatrix renderResult(QRCode code, int width, int height) { + ByteMatrix input = code.getMatrix(); + int inputWidth = input.getWidth(); + int inputHeight = input.getHeight(); + int qrWidth = inputWidth + (QUIET_ZONE_SIZE << 1); + int qrHeight = inputHeight + (QUIET_ZONE_SIZE << 1); + int outputWidth = Math.max(width, qrWidth); + int outputHeight = Math.max(height, qrHeight); + + int multiple = Math.min(outputWidth / qrWidth, outputHeight / qrHeight); + // Padding includes both the quiet zone and the extra white pixels to accommodate the requested + // dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone. + // If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will + // handle all the padding from 100x100 (the actual QR) up to 200x160. + int leftPadding = (outputWidth - (inputWidth * multiple)) / 2; + int topPadding = (outputHeight - (inputHeight * multiple)) / 2; + + BitMatrix output = new BitMatrix(outputWidth, outputHeight); + + for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) { + // Write the contents of this row of the barcode + for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) { + if (input.get(inputX, inputY) == 1) { + output.setRegion(outputX, outputY, multiple, multiple); + } + } + } + + return output; + } + +} diff --git a/src/com/google/zxing/qrcode/decoder/BitMatrixParser.java b/src/com/google/zxing/qrcode/decoder/BitMatrixParser.java new file mode 100644 index 0000000..dfe6fd3 --- /dev/null +++ b/src/com/google/zxing/qrcode/decoder/BitMatrixParser.java @@ -0,0 +1,203 @@ +/* + * Copyright 2007 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.qrcode.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; + +/** + * @author Sean Owen + */ +final class BitMatrixParser { + + private final BitMatrix bitMatrix; + private Version parsedVersion; + private FormatInformation parsedFormatInfo; + + /** + * @param bitMatrix {@link BitMatrix} to parse + * @throws FormatException if dimension is not >= 21 and 1 mod 4 + */ + BitMatrixParser(BitMatrix bitMatrix) throws FormatException { + int dimension = bitMatrix.getHeight(); + if (dimension < 21 || (dimension & 0x03) != 1) { + throw FormatException.getFormatInstance(); + } + this.bitMatrix = bitMatrix; + } + + /** + *Reads format information from one of its two locations within the QR Code.
+ * + * @return {@link FormatInformation} encapsulating the QR Code's format info + * @throws FormatException if both format information locations cannot be parsed as + * the valid encoding of format information + */ + FormatInformation readFormatInformation() throws FormatException { + + if (parsedFormatInfo != null) { + return parsedFormatInfo; + } + + // Read top-left format info bits + int formatInfoBits1 = 0; + for (int i = 0; i < 6; i++) { + formatInfoBits1 = copyBit(i, 8, formatInfoBits1); + } + // .. and skip a bit in the timing pattern ... + formatInfoBits1 = copyBit(7, 8, formatInfoBits1); + formatInfoBits1 = copyBit(8, 8, formatInfoBits1); + formatInfoBits1 = copyBit(8, 7, formatInfoBits1); + // .. and skip a bit in the timing pattern ... + for (int j = 5; j >= 0; j--) { + formatInfoBits1 = copyBit(8, j, formatInfoBits1); + } + + // Read the top-right/bottom-left pattern too + int dimension = bitMatrix.getHeight(); + int formatInfoBits2 = 0; + int iMin = dimension - 8; + for (int i = dimension - 1; i >= iMin; i--) { + formatInfoBits2 = copyBit(i, 8, formatInfoBits2); + } + for (int j = dimension - 7; j < dimension; j++) { + formatInfoBits2 = copyBit(8, j, formatInfoBits2); + } + + parsedFormatInfo = FormatInformation.decodeFormatInformation(formatInfoBits1, formatInfoBits2); + if (parsedFormatInfo != null) { + return parsedFormatInfo; + } + throw FormatException.getFormatInstance(); + } + + /** + *Reads version information from one of its two locations within the QR Code.
+ * + * @return {@link Version} encapsulating the QR Code's version + * @throws FormatException if both version information locations cannot be parsed as + * the valid encoding of version information + */ + Version readVersion() throws FormatException { + + if (parsedVersion != null) { + return parsedVersion; + } + + int dimension = bitMatrix.getHeight(); + + int provisionalVersion = (dimension - 17) >> 2; + if (provisionalVersion <= 6) { + return Version.getVersionForNumber(provisionalVersion); + } + + // Read top-right version info: 3 wide by 6 tall + int versionBits = 0; + int ijMin = dimension - 11; + for (int j = 5; j >= 0; j--) { + for (int i = dimension - 9; i >= ijMin; i--) { + versionBits = copyBit(i, j, versionBits); + } + } + + parsedVersion = Version.decodeVersionInformation(versionBits); + if (parsedVersion != null && parsedVersion.getDimensionForVersion() == dimension) { + return parsedVersion; + } + + // Hmm, failed. Try bottom left: 6 wide by 3 tall + versionBits = 0; + for (int i = 5; i >= 0; i--) { + for (int j = dimension - 9; j >= ijMin; j--) { + versionBits = copyBit(i, j, versionBits); + } + } + + parsedVersion = Version.decodeVersionInformation(versionBits); + if (parsedVersion != null && parsedVersion.getDimensionForVersion() == dimension) { + return parsedVersion; + } + throw FormatException.getFormatInstance(); + } + + private int copyBit(int i, int j, int versionBits) { + return bitMatrix.get(i, j) ? (versionBits << 1) | 0x1 : versionBits << 1; + } + + /** + *Reads the bits in the {@link BitMatrix} representing the finder pattern in the + * correct order in order to reconstitute the codewords bytes contained within the + * QR Code.
+ * + * @return bytes encoded within the QR Code + * @throws FormatException if the exact number of bytes expected is not read + */ + byte[] readCodewords() throws FormatException { + + FormatInformation formatInfo = readFormatInformation(); + Version version = readVersion(); + + // Get the data mask for the format used in this QR Code. This will exclude + // some bits from reading as we wind through the bit matrix. + DataMask dataMask = DataMask.forReference((int) formatInfo.getDataMask()); + int dimension = bitMatrix.getHeight(); + dataMask.unmaskBitMatrix(bitMatrix, dimension); + + BitMatrix functionPattern = version.buildFunctionPattern(); + + boolean readingUp = true; + byte[] result = new byte[version.getTotalCodewords()]; + int resultOffset = 0; + int currentByte = 0; + int bitsRead = 0; + // Read columns in pairs, from right to left + for (int j = dimension - 1; j > 0; j -= 2) { + if (j == 6) { + // Skip whole column with vertical alignment pattern; + // saves time and makes the other code proceed more cleanly + j--; + } + // Read alternatingly from bottom to top then top to bottom + for (int count = 0; count < dimension; count++) { + int i = readingUp ? dimension - 1 - count : count; + for (int col = 0; col < 2; col++) { + // Ignore bits covered by the function pattern + if (!functionPattern.get(j - col, i)) { + // Read a bit + bitsRead++; + currentByte <<= 1; + if (bitMatrix.get(j - col, i)) { + currentByte |= 1; + } + // If we've made a whole byte, save it off + if (bitsRead == 8) { + result[resultOffset++] = (byte) currentByte; + bitsRead = 0; + currentByte = 0; + } + } + } + } + readingUp ^= true; // readingUp = !readingUp; // switch directions + } + if (resultOffset != version.getTotalCodewords()) { + throw FormatException.getFormatInstance(); + } + return result; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/qrcode/decoder/DataBlock.java b/src/com/google/zxing/qrcode/decoder/DataBlock.java new file mode 100644 index 0000000..215f6c0 --- /dev/null +++ b/src/com/google/zxing/qrcode/decoder/DataBlock.java @@ -0,0 +1,123 @@ +/* + * Copyright 2007 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.qrcode.decoder; + +/** + *Encapsulates a block of data within a QR Code. QR Codes may split their data into + * multiple blocks, each of which is a unit of data and error-correction codewords. Each + * is represented by an instance of this class.
+ * + * @author Sean Owen + */ +final class DataBlock { + + private final int numDataCodewords; + private final byte[] codewords; + + private DataBlock(int numDataCodewords, byte[] codewords) { + this.numDataCodewords = numDataCodewords; + this.codewords = codewords; + } + + /** + *When QR Codes use multiple data blocks, they are actually interleaved. + * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This + * method will separate the data into original blocks.
+ * + * @param rawCodewords bytes as read directly from the QR Code + * @param version version of the QR Code + * @param ecLevel error-correction level of the QR Code + * @return {@link DataBlock}s containing original bytes, "de-interleaved" from representation in the + * QR Code + */ + static DataBlock[] getDataBlocks(byte[] rawCodewords, + Version version, + ErrorCorrectionLevel ecLevel) { + + if (rawCodewords.length != version.getTotalCodewords()) { + throw new IllegalArgumentException(); + } + + // Figure out the number and size of data blocks used by this version and + // error correction level + Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); + + // First count the total number of data blocks + int totalBlocks = 0; + Version.ECB[] ecBlockArray = ecBlocks.getECBlocks(); + for (int i = 0; i < ecBlockArray.length; i++) { + totalBlocks += ecBlockArray[i].getCount(); + } + + // Now establish DataBlocks of the appropriate size and number of data codewords + DataBlock[] result = new DataBlock[totalBlocks]; + int numResultBlocks = 0; + for (int j = 0; j < ecBlockArray.length; j++) { + Version.ECB ecBlock = ecBlockArray[j]; + for (int i = 0; i < ecBlock.getCount(); i++) { + int numDataCodewords = ecBlock.getDataCodewords(); + int numBlockCodewords = ecBlocks.getECCodewordsPerBlock() + numDataCodewords; + result[numResultBlocks++] = new DataBlock(numDataCodewords, new byte[numBlockCodewords]); + } + } + + // All blocks have the same amount of data, except that the last n + // (where n may be 0) have 1 more byte. Figure out where these start. + int shorterBlocksTotalCodewords = result[0].codewords.length; + int longerBlocksStartAt = result.length - 1; + while (longerBlocksStartAt >= 0) { + int numCodewords = result[longerBlocksStartAt].codewords.length; + if (numCodewords == shorterBlocksTotalCodewords) { + break; + } + longerBlocksStartAt--; + } + longerBlocksStartAt++; + + int shorterBlocksNumDataCodewords = shorterBlocksTotalCodewords - ecBlocks.getECCodewordsPerBlock(); + // The last elements of result may be 1 element longer; + // first fill out as many elements as all of them have + int rawCodewordsOffset = 0; + for (int i = 0; i < shorterBlocksNumDataCodewords; i++) { + for (int j = 0; j < numResultBlocks; j++) { + result[j].codewords[i] = rawCodewords[rawCodewordsOffset++]; + } + } + // Fill out the last data block in the longer ones + for (int j = longerBlocksStartAt; j < numResultBlocks; j++) { + result[j].codewords[shorterBlocksNumDataCodewords] = rawCodewords[rawCodewordsOffset++]; + } + // Now add in error correction blocks + int max = result[0].codewords.length; + for (int i = shorterBlocksNumDataCodewords; i < max; i++) { + for (int j = 0; j < numResultBlocks; j++) { + int iOffset = j < longerBlocksStartAt ? i : i + 1; + result[j].codewords[iOffset] = rawCodewords[rawCodewordsOffset++]; + } + } + return result; + } + + int getNumDataCodewords() { + return numDataCodewords; + } + + byte[] getCodewords() { + return codewords; + } + +} diff --git a/src/com/google/zxing/qrcode/decoder/DataMask.java b/src/com/google/zxing/qrcode/decoder/DataMask.java new file mode 100644 index 0000000..48036b1 --- /dev/null +++ b/src/com/google/zxing/qrcode/decoder/DataMask.java @@ -0,0 +1,155 @@ +/* + * Copyright 2007 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.qrcode.decoder; + +import com.google.zxing.common.BitMatrix; + +/** + *Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations + * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix, + * including areas used for finder patterns, timing patterns, etc. These areas should be unused + * after the point they are unmasked anyway.
+ * + *Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position + * and j is row position. In fact, as the text says, i is row position and j is column position.
+ * + * @author Sean Owen + */ +abstract class DataMask { + + /** + * See ISO 18004:2006 6.8.1 + */ + private static final DataMask[] DATA_MASKS = { + new DataMask000(), + new DataMask001(), + new DataMask010(), + new DataMask011(), + new DataMask100(), + new DataMask101(), + new DataMask110(), + new DataMask111(), + }; + + private DataMask() { + } + + /** + *Implementations of this method reverse the data masking process applied to a QR Code and + * make its bits ready to read.
+ * + * @param bits representation of QR Code bits + * @param dimension dimension of QR Code, represented by bits, being unmasked + */ + final void unmaskBitMatrix(BitMatrix bits, int dimension) { + for (int i = 0; i < dimension; i++) { + for (int j = 0; j < dimension; j++) { + if (isMasked(i, j)) { + bits.flip(j, i); + } + } + } + } + + abstract boolean isMasked(int i, int j); + + /** + * @param reference a value between 0 and 7 indicating one of the eight possible + * data mask patterns a QR Code may use + * @return {@link DataMask} encapsulating the data mask pattern + */ + static DataMask forReference(int reference) { + if (reference < 0 || reference > 7) { + throw new IllegalArgumentException(); + } + return DATA_MASKS[reference]; + } + + /** + * 000: mask bits for which (x + y) mod 2 == 0 + */ + private static class DataMask000 extends DataMask { + boolean isMasked(int i, int j) { + return ((i + j) & 0x01) == 0; + } + } + + /** + * 001: mask bits for which x mod 2 == 0 + */ + private static class DataMask001 extends DataMask { + boolean isMasked(int i, int j) { + return (i & 0x01) == 0; + } + } + + /** + * 010: mask bits for which y mod 3 == 0 + */ + private static class DataMask010 extends DataMask { + boolean isMasked(int i, int j) { + return j % 3 == 0; + } + } + + /** + * 011: mask bits for which (x + y) mod 3 == 0 + */ + private static class DataMask011 extends DataMask { + boolean isMasked(int i, int j) { + return (i + j) % 3 == 0; + } + } + + /** + * 100: mask bits for which (x/2 + y/3) mod 2 == 0 + */ + private static class DataMask100 extends DataMask { + boolean isMasked(int i, int j) { + return (((i >>> 1) + (j /3)) & 0x01) == 0; + } + } + + /** + * 101: mask bits for which xy mod 2 + xy mod 3 == 0 + */ + private static class DataMask101 extends DataMask { + boolean isMasked(int i, int j) { + int temp = i * j; + return (temp & 0x01) + (temp % 3) == 0; + } + } + + /** + * 110: mask bits for which (xy mod 2 + xy mod 3) mod 2 == 0 + */ + private static class DataMask110 extends DataMask { + boolean isMasked(int i, int j) { + int temp = i * j; + return (((temp & 0x01) + (temp % 3)) & 0x01) == 0; + } + } + + /** + * 111: mask bits for which ((x+y)mod 2 + xy mod 3) mod 2 == 0 + */ + private static class DataMask111 extends DataMask { + boolean isMasked(int i, int j) { + return ((((i + j) & 0x01) + ((i * j) % 3)) & 0x01) == 0; + } + } +} diff --git a/src/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java b/src/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java new file mode 100644 index 0000000..7064b8b --- /dev/null +++ b/src/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java @@ -0,0 +1,261 @@ +/* + * Copyright 2007 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.qrcode.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitSource; +import com.google.zxing.common.CharacterSetECI; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.StringUtils; + +import java.io.UnsupportedEncodingException; +import java.util.Hashtable; +import java.util.Vector; + +/** + *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.
+ * + *See ISO 18004:2006, 6.4.3 - 6.4.7
+ * + * @author Sean Owen + */ +final class DecodedBitStreamParser { + + /** + * See ISO 18004:2006, 6.4.4 Table 5 + */ + 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 DecodedBitStreamParser() { + } + + static DecoderResult decode(byte[] bytes, Version version, ErrorCorrectionLevel ecLevel, Hashtable hints) + throws FormatException { + BitSource bits = new BitSource(bytes); + StringBuffer result = new StringBuffer(50); + CharacterSetECI currentCharacterSetECI = null; + boolean fc1InEffect = false; + Vector byteSegments = new Vector(1); + Mode mode; + do { + // While still another segment to read... + if (bits.available() < 4) { + // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here + mode = Mode.TERMINATOR; + } else { + try { + mode = Mode.forBits(bits.readBits(4)); // mode is encoded by 4 bits + } catch (IllegalArgumentException iae) { + throw FormatException.getFormatInstance(); + } + } + if (!mode.equals(Mode.TERMINATOR)) { + if (mode.equals(Mode.FNC1_FIRST_POSITION) || mode.equals(Mode.FNC1_SECOND_POSITION)) { + // We do little with FNC1 except alter the parsed result a bit according to the spec + fc1InEffect = true; + } else if (mode.equals(Mode.STRUCTURED_APPEND)) { + // not really supported; all we do is ignore it + // Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue + bits.readBits(16); + } else if (mode.equals(Mode.ECI)) { + // Count doesn't apply to ECI + int value = parseECIValue(bits); + currentCharacterSetECI = CharacterSetECI.getCharacterSetECIByValue(value); + if (currentCharacterSetECI == null) { + throw FormatException.getFormatInstance(); + } + } else { + // 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, fc1InEffect); + } else if (mode.equals(Mode.BYTE)) { + decodeByteSegment(bits, result, count, currentCharacterSetECI, byteSegments, hints); + } else if (mode.equals(Mode.KANJI)) { + decodeKanjiSegment(bits, result, count); + } else { + throw FormatException.getFormatInstance(); + } + } + } + } while (!mode.equals(Mode.TERMINATOR)); + + return new DecoderResult(bytes, result.toString(), byteSegments.isEmpty() ? null : byteSegments, ecLevel); + } + + private static void decodeKanjiSegment(BitSource bits, + StringBuffer result, + int count) throws FormatException { + // 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) { + // In the 0x8140 to 0x9FFC range + assembledTwoBytes += 0x08140; + } else { + // In the 0xE040 to 0xEBBF range + assembledTwoBytes += 0x0C140; + } + buffer[offset] = (byte) (assembledTwoBytes >> 8); + buffer[offset + 1] = (byte) assembledTwoBytes; + offset += 2; + count--; + } + // Shift_JIS may not be supported in some environments: + try { + result.append(new String(buffer, StringUtils.SHIFT_JIS)); + } catch (UnsupportedEncodingException uee) { + throw FormatException.getFormatInstance(); + } + } + + private static void decodeByteSegment(BitSource bits, + StringBuffer result, + int count, + CharacterSetECI currentCharacterSetECI, + Vector byteSegments, + Hashtable hints) throws FormatException { + byte[] readBytes = new byte[count]; + if (count << 3 > bits.available()) { + throw FormatException.getFormatInstance(); + } + for (int i = 0; i < count; i++) { + readBytes[i] = (byte) bits.readBits(8); + } + String encoding; + if (currentCharacterSetECI == null) { + // The spec isn't clear on this mode; see + // section 6.4.5: t does not say which encoding to assuming + // upon decoding. I have seen ISO-8859-1 used as well as + // Shift_JIS -- without anything like an ECI designator to + // give a hint. + encoding = StringUtils.guessEncoding(readBytes, hints); + } else { + encoding = currentCharacterSetECI.getEncodingName(); + } + try { + result.append(new String(readBytes, encoding)); + } catch (UnsupportedEncodingException uce) { + throw FormatException.getFormatInstance(); + } + byteSegments.addElement(readBytes); + } + + private static char toAlphaNumericChar(int value) throws FormatException { + if (value >= ALPHANUMERIC_CHARS.length) { + throw FormatException.getFormatInstance(); + } + return ALPHANUMERIC_CHARS[value]; + } + + private static void decodeAlphanumericSegment(BitSource bits, + StringBuffer result, + int count, + boolean fc1InEffect) throws FormatException { + // Read two characters at a time + int start = result.length(); + while (count > 1) { + int nextTwoCharsBits = bits.readBits(11); + result.append(toAlphaNumericChar(nextTwoCharsBits / 45)); + result.append(toAlphaNumericChar(nextTwoCharsBits % 45)); + count -= 2; + } + if (count == 1) { + // special case: one character left + result.append(toAlphaNumericChar(bits.readBits(6))); + } + // See section 6.4.8.1, 6.4.8.2 + if (fc1InEffect) { + // We need to massage the result a bit if in an FNC1 mode: + for (int i = start; i < result.length(); i++) { + if (result.charAt(i) == '%') { + if (i < result.length() - 1 && result.charAt(i + 1) == '%') { + // %% is rendered as % + result.deleteCharAt(i + 1); + } else { + // In alpha mode, % should be converted to FNC1 separator 0x1D + result.setCharAt(i, (char) 0x1D); + } + } + } + } + } + + private static void decodeNumericSegment(BitSource bits, + StringBuffer result, + int count) throws FormatException { + // Read three digits at a time + while (count >= 3) { + // Each 10 bits encodes three digits + int threeDigitsBits = bits.readBits(10); + if (threeDigitsBits >= 1000) { + throw FormatException.getFormatInstance(); + } + result.append(toAlphaNumericChar(threeDigitsBits / 100)); + result.append(toAlphaNumericChar((threeDigitsBits / 10) % 10)); + result.append(toAlphaNumericChar(threeDigitsBits % 10)); + count -= 3; + } + if (count == 2) { + // Two digits left over to read, encoded in 7 bits + int twoDigitsBits = bits.readBits(7); + if (twoDigitsBits >= 100) { + throw FormatException.getFormatInstance(); + } + result.append(toAlphaNumericChar(twoDigitsBits / 10)); + result.append(toAlphaNumericChar(twoDigitsBits % 10)); + } else if (count == 1) { + // One digit left over to read + int digitBits = bits.readBits(4); + if (digitBits >= 10) { + throw FormatException.getFormatInstance(); + } + result.append(toAlphaNumericChar(digitBits)); + } + } + + private static int parseECIValue(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 secondThirdBytes = bits.readBits(16); + return ((firstByte & 0x1F) << 16) | secondThirdBytes; + } + throw new IllegalArgumentException("Bad ECI bits starting with byte " + firstByte); + } + +} diff --git a/src/com/google/zxing/qrcode/decoder/Decoder.java b/src/com/google/zxing/qrcode/decoder/Decoder.java new file mode 100644 index 0000000..3aad15e --- /dev/null +++ b/src/com/google/zxing/qrcode/decoder/Decoder.java @@ -0,0 +1,148 @@ +/* + * Copyright 2007 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.qrcode.decoder; + +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.reedsolomon.GF256; +import com.google.zxing.common.reedsolomon.ReedSolomonDecoder; +import com.google.zxing.common.reedsolomon.ReedSolomonException; + +import java.util.Hashtable; + +/** + *The main class which implements QR Code decoding -- as opposed to locating and extracting + * the QR Code from an image.
+ * + * @author Sean Owen + */ +public final class Decoder { + + private final ReedSolomonDecoder rsDecoder; + + public Decoder() { + rsDecoder = new ReedSolomonDecoder(GF256.QR_CODE_FIELD); + } + + public DecoderResult decode(boolean[][] image) + throws ChecksumException, FormatException, NotFoundException { + return decode(image, null); + } + + /** + *Convenience method that can decode a QR Code represented as a 2D array of booleans. + * "true" is taken to mean a black module.
+ * + * @param image booleans representing white/black QR Code modules + * @return text and bytes encoded within the QR Code + * @throws NotFoundException if the QR Code cannot be found + * @throws FormatException if the QR Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public DecoderResult decode(boolean[][] image, Hashtable hints) + throws ChecksumException, FormatException, NotFoundException { + int dimension = image.length; + BitMatrix bits = new BitMatrix(dimension); + for (int i = 0; i < dimension; i++) { + for (int j = 0; j < dimension; j++) { + if (image[i][j]) { + bits.set(j, i); + } + } + } + return decode(bits, hints); + } + + public DecoderResult decode(BitMatrix bits) throws ChecksumException, FormatException, NotFoundException { + return decode(bits, null); + } + + /** + *Decodes a QR Code represented as a {@link BitMatrix}. A 1 or "true" is taken to mean a black module.
+ * + * @param bits booleans representing white/black QR Code modules + * @return text and bytes encoded within the QR Code + * @throws FormatException if the QR Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public DecoderResult decode(BitMatrix bits, Hashtable hints) throws FormatException, ChecksumException { + + // Construct a parser and read version, error-correction level + BitMatrixParser parser = new BitMatrixParser(bits); + Version version = parser.readVersion(); + ErrorCorrectionLevel ecLevel = parser.readFormatInformation().getErrorCorrectionLevel(); + + // Read codewords + byte[] codewords = parser.readCodewords(); + // Separate into data blocks + DataBlock[] dataBlocks = DataBlock.getDataBlocks(codewords, version, ecLevel); + + // Count total number of data bytes + int totalBytes = 0; + for (int i = 0; i < dataBlocks.length; i++) { + totalBytes += dataBlocks[i].getNumDataCodewords(); + } + byte[] resultBytes = new byte[totalBytes]; + int resultOffset = 0; + + // Error-correct and copy data blocks together into a stream of bytes + for (int j = 0; j < dataBlocks.length; j++) { + DataBlock dataBlock = dataBlocks[j]; + byte[] codewordBytes = dataBlock.getCodewords(); + int numDataCodewords = dataBlock.getNumDataCodewords(); + correctErrors(codewordBytes, numDataCodewords); + for (int i = 0; i < numDataCodewords; i++) { + resultBytes[resultOffset++] = codewordBytes[i]; + } + } + + // Decode the contents of that stream of bytes + return DecodedBitStreamParser.decode(resultBytes, version, ecLevel, hints); + } + + /** + *Given data and error-correction codewords received, possibly corrupted by errors, attempts to + * correct the errors in-place using Reed-Solomon error correction.
+ * + * @param codewordBytes data and error correction codewords + * @param numDataCodewords number of codewords that are data bytes + * @throws ChecksumException if error correction fails + */ + private void correctErrors(byte[] codewordBytes, int numDataCodewords) throws ChecksumException { + int numCodewords = codewordBytes.length; + // First read into an array of ints + int[] codewordsInts = new int[numCodewords]; + for (int i = 0; i < numCodewords; i++) { + codewordsInts[i] = codewordBytes[i] & 0xFF; + } + int numECCodewords = codewordBytes.length - numDataCodewords; + try { + rsDecoder.decode(codewordsInts, numECCodewords); + } catch (ReedSolomonException rse) { + throw ChecksumException.getChecksumInstance(); + } + // Copy back into array of bytes -- only need to worry about the bytes that were data + // We don't care about errors in the error-correction codewords + for (int i = 0; i < numDataCodewords; i++) { + codewordBytes[i] = (byte) codewordsInts[i]; + } + } + +} diff --git a/src/com/google/zxing/qrcode/decoder/ErrorCorrectionLevel.java b/src/com/google/zxing/qrcode/decoder/ErrorCorrectionLevel.java new file mode 100644 index 0000000..400611a --- /dev/null +++ b/src/com/google/zxing/qrcode/decoder/ErrorCorrectionLevel.java @@ -0,0 +1,86 @@ +/* + * Copyright 2007 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.qrcode.decoder; + +/** + *See ISO 18004:2006, 6.5.1. This enum encapsulates the four error correction levels + * defined by the QR code standard.
+ * + * @author Sean Owen + */ +public final class ErrorCorrectionLevel { + + // No, we can't use an enum here. J2ME doesn't support it. + + /** + * L = ~7% correction + */ + public static final ErrorCorrectionLevel L = new ErrorCorrectionLevel(0, 0x01, "L"); + /** + * M = ~15% correction + */ + public static final ErrorCorrectionLevel M = new ErrorCorrectionLevel(1, 0x00, "M"); + /** + * Q = ~25% correction + */ + public static final ErrorCorrectionLevel Q = new ErrorCorrectionLevel(2, 0x03, "Q"); + /** + * H = ~30% correction + */ + public static final ErrorCorrectionLevel H = new ErrorCorrectionLevel(3, 0x02, "H"); + + private static final ErrorCorrectionLevel[] FOR_BITS = {M, L, H, Q}; + + private final int ordinal; + private final int bits; + private final String name; + + private ErrorCorrectionLevel(int ordinal, int bits, String name) { + this.ordinal = ordinal; + this.bits = bits; + this.name = name; + } + + public int ordinal() { + return ordinal; + } + + public int getBits() { + return bits; + } + + public String getName() { + return name; + } + + public String toString() { + return name; + } + + /** + * @param bits int containing the two bits encoding a QR Code's error correction level + * @return {@link ErrorCorrectionLevel} representing the encoded error correction level + */ + public static ErrorCorrectionLevel forBits(int bits) { + if (bits < 0 || bits >= FOR_BITS.length) { + throw new IllegalArgumentException(); + } + return FOR_BITS[bits]; + } + + +} diff --git a/src/com/google/zxing/qrcode/decoder/FormatInformation.java b/src/com/google/zxing/qrcode/decoder/FormatInformation.java new file mode 100644 index 0000000..1b76b0d --- /dev/null +++ b/src/com/google/zxing/qrcode/decoder/FormatInformation.java @@ -0,0 +1,171 @@ +/* + * Copyright 2007 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.qrcode.decoder; + +/** + *Encapsulates a QR Code's format information, including the data mask used and + * error correction level.
+ * + * @author Sean Owen + * @see DataMask + * @see ErrorCorrectionLevel + */ +final class FormatInformation { + + private static final int FORMAT_INFO_MASK_QR = 0x5412; + + /** + * See ISO 18004:2006, Annex C, Table C.1 + */ + private static final int[][] FORMAT_INFO_DECODE_LOOKUP = { + {0x5412, 0x00}, + {0x5125, 0x01}, + {0x5E7C, 0x02}, + {0x5B4B, 0x03}, + {0x45F9, 0x04}, + {0x40CE, 0x05}, + {0x4F97, 0x06}, + {0x4AA0, 0x07}, + {0x77C4, 0x08}, + {0x72F3, 0x09}, + {0x7DAA, 0x0A}, + {0x789D, 0x0B}, + {0x662F, 0x0C}, + {0x6318, 0x0D}, + {0x6C41, 0x0E}, + {0x6976, 0x0F}, + {0x1689, 0x10}, + {0x13BE, 0x11}, + {0x1CE7, 0x12}, + {0x19D0, 0x13}, + {0x0762, 0x14}, + {0x0255, 0x15}, + {0x0D0C, 0x16}, + {0x083B, 0x17}, + {0x355F, 0x18}, + {0x3068, 0x19}, + {0x3F31, 0x1A}, + {0x3A06, 0x1B}, + {0x24B4, 0x1C}, + {0x2183, 0x1D}, + {0x2EDA, 0x1E}, + {0x2BED, 0x1F}, + }; + + /** + * Offset i holds the number of 1 bits in the binary representation of i + */ + private static final int[] BITS_SET_IN_HALF_BYTE = + {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4}; + + private final ErrorCorrectionLevel errorCorrectionLevel; + private final byte dataMask; + + private FormatInformation(int formatInfo) { + // Bits 3,4 + errorCorrectionLevel = ErrorCorrectionLevel.forBits((formatInfo >> 3) & 0x03); + // Bottom 3 bits + dataMask = (byte) (formatInfo & 0x07); + } + + static int numBitsDiffering(int a, int b) { + a ^= b; // a now has a 1 bit exactly where its bit differs with b's + // Count bits set quickly with a series of lookups: + return BITS_SET_IN_HALF_BYTE[a & 0x0F] + + BITS_SET_IN_HALF_BYTE[(a >>> 4 & 0x0F)] + + BITS_SET_IN_HALF_BYTE[(a >>> 8 & 0x0F)] + + BITS_SET_IN_HALF_BYTE[(a >>> 12 & 0x0F)] + + BITS_SET_IN_HALF_BYTE[(a >>> 16 & 0x0F)] + + BITS_SET_IN_HALF_BYTE[(a >>> 20 & 0x0F)] + + BITS_SET_IN_HALF_BYTE[(a >>> 24 & 0x0F)] + + BITS_SET_IN_HALF_BYTE[(a >>> 28 & 0x0F)]; + } + + /** + * @param maskedFormatInfo1 format info indicator, with mask still applied + * @param maskedFormatInfo2 second copy of same info; both are checked at the same time + * to establish best match + * @return information about the format it specifies, ornull
+ * if doesn't seem to match any known pattern
+ */
+ static FormatInformation decodeFormatInformation(int maskedFormatInfo1, int maskedFormatInfo2) {
+ FormatInformation formatInfo = doDecodeFormatInformation(maskedFormatInfo1, maskedFormatInfo2);
+ if (formatInfo != null) {
+ return formatInfo;
+ }
+ // Should return null, but, some QR codes apparently
+ // do not mask this info. Try again by actually masking the pattern
+ // first
+ return doDecodeFormatInformation(maskedFormatInfo1 ^ FORMAT_INFO_MASK_QR,
+ maskedFormatInfo2 ^ FORMAT_INFO_MASK_QR);
+ }
+
+ private static FormatInformation doDecodeFormatInformation(int maskedFormatInfo1, int maskedFormatInfo2) {
+ // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing
+ int bestDifference = Integer.MAX_VALUE;
+ int bestFormatInfo = 0;
+ for (int i = 0; i < FORMAT_INFO_DECODE_LOOKUP.length; i++) {
+ int[] decodeInfo = FORMAT_INFO_DECODE_LOOKUP[i];
+ int targetInfo = decodeInfo[0];
+ if (targetInfo == maskedFormatInfo1 || targetInfo == maskedFormatInfo2) {
+ // Found an exact match
+ return new FormatInformation(decodeInfo[1]);
+ }
+ int bitsDifference = numBitsDiffering(maskedFormatInfo1, targetInfo);
+ if (bitsDifference < bestDifference) {
+ bestFormatInfo = decodeInfo[1];
+ bestDifference = bitsDifference;
+ }
+ if (maskedFormatInfo1 != maskedFormatInfo2) {
+ // also try the other option
+ bitsDifference = numBitsDiffering(maskedFormatInfo2, targetInfo);
+ if (bitsDifference < bestDifference) {
+ bestFormatInfo = decodeInfo[1];
+ bestDifference = bitsDifference;
+ }
+ }
+ }
+ // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits
+ // differing means we found a match
+ if (bestDifference <= 3) {
+ return new FormatInformation(bestFormatInfo);
+ }
+ return null;
+ }
+
+ ErrorCorrectionLevel getErrorCorrectionLevel() {
+ return errorCorrectionLevel;
+ }
+
+ byte getDataMask() {
+ return dataMask;
+ }
+
+ public int hashCode() {
+ return (errorCorrectionLevel.ordinal() << 3) | (int) dataMask;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof FormatInformation)) {
+ return false;
+ }
+ FormatInformation other = (FormatInformation) o;
+ return this.errorCorrectionLevel == other.errorCorrectionLevel &&
+ this.dataMask == other.dataMask;
+ }
+
+}
diff --git a/src/com/google/zxing/qrcode/decoder/Mode.java b/src/com/google/zxing/qrcode/decoder/Mode.java
new file mode 100644
index 0000000..24117dc
--- /dev/null
+++ b/src/com/google/zxing/qrcode/decoder/Mode.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2007 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.qrcode.decoder;
+
+/**
+ * See ISO 18004:2006, 6.4.1, Tables 2 and 3. This enum encapsulates the various modes in which + * data can be encoded to bits in the QR code standard.
+ * + * @author Sean Owen + */ +public final class Mode { + + // No, we can't use an enum here. J2ME doesn't support it. + + public static final Mode TERMINATOR = new Mode(new int[]{0, 0, 0}, 0x00, "TERMINATOR"); // Not really a mode... + public static final Mode NUMERIC = new Mode(new int[]{10, 12, 14}, 0x01, "NUMERIC"); + public static final Mode ALPHANUMERIC = new Mode(new int[]{9, 11, 13}, 0x02, "ALPHANUMERIC"); + public static final Mode STRUCTURED_APPEND = new Mode(new int[]{0, 0, 0}, 0x03, "STRUCTURED_APPEND"); // Not supported + public static final Mode BYTE = new Mode(new int[]{8, 16, 16}, 0x04, "BYTE"); + public static final Mode ECI = new Mode(null, 0x07, "ECI"); // character counts don't apply + public static final Mode KANJI = new Mode(new int[]{8, 10, 12}, 0x08, "KANJI"); + public static final Mode FNC1_FIRST_POSITION = new Mode(null, 0x05, "FNC1_FIRST_POSITION"); + public static final Mode FNC1_SECOND_POSITION = new Mode(null, 0x09, "FNC1_SECOND_POSITION"); + + private final int[] characterCountBitsForVersions; + private final int bits; + private final String name; + + private Mode(int[] characterCountBitsForVersions, int bits, String name) { + this.characterCountBitsForVersions = characterCountBitsForVersions; + this.bits = bits; + this.name = name; + } + + /** + * @param bits four bits encoding a QR Code data mode + * @return {@link Mode} encoded by these bits + * @throws IllegalArgumentException if bits do not correspond to a known mode + */ + public static Mode forBits(int bits) { + switch (bits) { + case 0x0: + return TERMINATOR; + case 0x1: + return NUMERIC; + case 0x2: + return ALPHANUMERIC; + case 0x3: + return STRUCTURED_APPEND; + case 0x4: + return BYTE; + case 0x5: + return FNC1_FIRST_POSITION; + case 0x7: + return ECI; + case 0x8: + return KANJI; + case 0x9: + return FNC1_SECOND_POSITION; + default: + throw new IllegalArgumentException(); + } + } + + /** + * @param version version in question + * @return number of bits used, in this QR Code symbol {@link Version}, to encode the + * count of characters that will follow encoded in this {@link Mode} + */ + public int getCharacterCountBits(Version version) { + if (characterCountBitsForVersions == null) { + throw new IllegalArgumentException("Character count doesn't apply to this mode"); + } + int number = version.getVersionNumber(); + int offset; + if (number <= 9) { + offset = 0; + } else if (number <= 26) { + offset = 1; + } else { + offset = 2; + } + return characterCountBitsForVersions[offset]; + } + + public int getBits() { + return bits; + } + + public String getName() { + return name; + } + + public String toString() { + return name; + } + +} diff --git a/src/com/google/zxing/qrcode/decoder/Version.java b/src/com/google/zxing/qrcode/decoder/Version.java new file mode 100644 index 0000000..e496005 --- /dev/null +++ b/src/com/google/zxing/qrcode/decoder/Version.java @@ -0,0 +1,586 @@ +/* + * Copyright 2007 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.qrcode.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; + +/** + * See ISO 18004:2006 Annex D + * + * @author Sean Owen + */ +public final class Version { + + /** + * See ISO 18004:2006 Annex D. + * Element i represents the raw version bits that specify version i + 7 + */ + private static final int[] VERSION_DECODE_INFO = { + 0x07C94, 0x085BC, 0x09A99, 0x0A4D3, 0x0BBF6, + 0x0C762, 0x0D847, 0x0E60D, 0x0F928, 0x10B78, + 0x1145D, 0x12A17, 0x13532, 0x149A6, 0x15683, + 0x168C9, 0x177EC, 0x18EC4, 0x191E1, 0x1AFAB, + 0x1B08E, 0x1CC1A, 0x1D33F, 0x1ED75, 0x1F250, + 0x209D5, 0x216F0, 0x228BA, 0x2379F, 0x24B0B, + 0x2542E, 0x26A64, 0x27541, 0x28C69 + }; + + private static final Version[] VERSIONS = buildVersions(); + + private final int versionNumber; + private final int[] alignmentPatternCenters; + private final ECBlocks[] ecBlocks; + private final int totalCodewords; + + private Version(int versionNumber, + int[] alignmentPatternCenters, + ECBlocks ecBlocks1, + ECBlocks ecBlocks2, + ECBlocks ecBlocks3, + ECBlocks ecBlocks4) { + this.versionNumber = versionNumber; + this.alignmentPatternCenters = alignmentPatternCenters; + this.ecBlocks = new ECBlocks[]{ecBlocks1, ecBlocks2, ecBlocks3, ecBlocks4}; + int total = 0; + int ecCodewords = ecBlocks1.getECCodewordsPerBlock(); + ECB[] ecbArray = ecBlocks1.getECBlocks(); + for (int i = 0; i < ecbArray.length; i++) { + ECB ecBlock = ecbArray[i]; + total += ecBlock.getCount() * (ecBlock.getDataCodewords() + ecCodewords); + } + this.totalCodewords = total; + } + + public int getVersionNumber() { + return versionNumber; + } + + public int[] getAlignmentPatternCenters() { + return alignmentPatternCenters; + } + + public int getTotalCodewords() { + return totalCodewords; + } + + public int getDimensionForVersion() { + return 17 + 4 * versionNumber; + } + + public ECBlocks getECBlocksForLevel(ErrorCorrectionLevel ecLevel) { + return ecBlocks[ecLevel.ordinal()]; + } + + /** + *Deduces version information purely from QR Code dimensions.
+ * + * @param dimension dimension in modules + * @return {@link Version} for a QR Code of that dimension + * @throws FormatException if dimension is not 1 mod 4 + */ + public static Version getProvisionalVersionForDimension(int dimension) throws FormatException { + if (dimension % 4 != 1) { + throw FormatException.getFormatInstance(); + } + try { + return getVersionForNumber((dimension - 17) >> 2); + } catch (IllegalArgumentException iae) { + throw FormatException.getFormatInstance(); + } + } + + public static Version getVersionForNumber(int versionNumber) { + if (versionNumber < 1 || versionNumber > 40) { + throw new IllegalArgumentException(); + } + return VERSIONS[versionNumber - 1]; + } + + static Version decodeVersionInformation(int versionBits) { + int bestDifference = Integer.MAX_VALUE; + int bestVersion = 0; + for (int i = 0; i < VERSION_DECODE_INFO.length; i++) { + int targetVersion = VERSION_DECODE_INFO[i]; + // Do the version info bits match exactly? done. + if (targetVersion == versionBits) { + return getVersionForNumber(i + 7); + } + // Otherwise see if this is the closest to a real version info bit string + // we have seen so far + int bitsDifference = FormatInformation.numBitsDiffering(versionBits, targetVersion); + if (bitsDifference < bestDifference) { + bestVersion = i + 7; + bestDifference = bitsDifference; + } + } + // We can tolerate up to 3 bits of error since no two version info codewords will + // differ in less than 8 bits. + if (bestDifference <= 3) { + return getVersionForNumber(bestVersion); + } + // If we didn't find a close enough match, fail + return null; + } + + /** + * See ISO 18004:2006 Annex E + */ + BitMatrix buildFunctionPattern() { + int dimension = getDimensionForVersion(); + BitMatrix bitMatrix = new BitMatrix(dimension); + + // Top left finder pattern + separator + format + bitMatrix.setRegion(0, 0, 9, 9); + // Top right finder pattern + separator + format + bitMatrix.setRegion(dimension - 8, 0, 8, 9); + // Bottom left finder pattern + separator + format + bitMatrix.setRegion(0, dimension - 8, 9, 8); + + // Alignment patterns + int max = alignmentPatternCenters.length; + for (int x = 0; x < max; x++) { + int i = alignmentPatternCenters[x] - 2; + for (int y = 0; y < max; y++) { + if ((x == 0 && (y == 0 || y == max - 1)) || (x == max - 1 && y == 0)) { + // No alignment patterns near the three finder paterns + continue; + } + bitMatrix.setRegion(alignmentPatternCenters[y] - 2, i, 5, 5); + } + } + + // Vertical timing pattern + bitMatrix.setRegion(6, 9, 1, dimension - 17); + // Horizontal timing pattern + bitMatrix.setRegion(9, 6, dimension - 17, 1); + + if (versionNumber > 6) { + // Version info, top right + bitMatrix.setRegion(dimension - 11, 0, 3, 6); + // Version info, bottom left + bitMatrix.setRegion(0, dimension - 11, 6, 3); + } + + return bitMatrix; + } + + /** + *Encapsulates a set of error-correction blocks in one symbol version. Most versions will + * use blocks of differing sizes within one version, so, this encapsulates the parameters for + * each set of blocks. It also holds the number of error-correction codewords per block since it + * will be the same across all blocks within one version.
+ */ + public static final class ECBlocks { + private final int ecCodewordsPerBlock; + private final ECB[] ecBlocks; + + ECBlocks(int ecCodewordsPerBlock, ECB ecBlocks) { + this.ecCodewordsPerBlock = ecCodewordsPerBlock; + this.ecBlocks = new ECB[]{ecBlocks}; + } + + ECBlocks(int ecCodewordsPerBlock, ECB ecBlocks1, ECB ecBlocks2) { + this.ecCodewordsPerBlock = ecCodewordsPerBlock; + this.ecBlocks = new ECB[]{ecBlocks1, ecBlocks2}; + } + + public int getECCodewordsPerBlock() { + return ecCodewordsPerBlock; + } + + public int getNumBlocks() { + int total = 0; + for (int i = 0; i < ecBlocks.length; i++) { + total += ecBlocks[i].getCount(); + } + return total; + } + + public int getTotalECCodewords() { + return ecCodewordsPerBlock * getNumBlocks(); + } + + public ECB[] getECBlocks() { + return ecBlocks; + } + } + + /** + *Encapsualtes the parameters for one error-correction block in one symbol version. + * This includes the number of data codewords, and the number of times a block with these + * parameters is used consecutively in the QR code version's format.
+ */ + public static final class ECB { + private final int count; + private final int dataCodewords; + + ECB(int count, int dataCodewords) { + this.count = count; + this.dataCodewords = dataCodewords; + } + + public int getCount() { + return count; + } + + public int getDataCodewords() { + return dataCodewords; + } + } + + public String toString() { + return String.valueOf(versionNumber); + } + + /** + * See ISO 18004:2006 6.5.1 Table 9 + */ + private static Version[] buildVersions() { + return new Version[]{ + new Version(1, new int[]{}, + new ECBlocks(7, new ECB(1, 19)), + new ECBlocks(10, new ECB(1, 16)), + new ECBlocks(13, new ECB(1, 13)), + new ECBlocks(17, new ECB(1, 9))), + new Version(2, new int[]{6, 18}, + new ECBlocks(10, new ECB(1, 34)), + new ECBlocks(16, new ECB(1, 28)), + new ECBlocks(22, new ECB(1, 22)), + new ECBlocks(28, new ECB(1, 16))), + new Version(3, new int[]{6, 22}, + new ECBlocks(15, new ECB(1, 55)), + new ECBlocks(26, new ECB(1, 44)), + new ECBlocks(18, new ECB(2, 17)), + new ECBlocks(22, new ECB(2, 13))), + new Version(4, new int[]{6, 26}, + new ECBlocks(20, new ECB(1, 80)), + new ECBlocks(18, new ECB(2, 32)), + new ECBlocks(26, new ECB(2, 24)), + new ECBlocks(16, new ECB(4, 9))), + new Version(5, new int[]{6, 30}, + new ECBlocks(26, new ECB(1, 108)), + new ECBlocks(24, new ECB(2, 43)), + new ECBlocks(18, new ECB(2, 15), + new ECB(2, 16)), + new ECBlocks(22, new ECB(2, 11), + new ECB(2, 12))), + new Version(6, new int[]{6, 34}, + new ECBlocks(18, new ECB(2, 68)), + new ECBlocks(16, new ECB(4, 27)), + new ECBlocks(24, new ECB(4, 19)), + new ECBlocks(28, new ECB(4, 15))), + new Version(7, new int[]{6, 22, 38}, + new ECBlocks(20, new ECB(2, 78)), + new ECBlocks(18, new ECB(4, 31)), + new ECBlocks(18, new ECB(2, 14), + new ECB(4, 15)), + new ECBlocks(26, new ECB(4, 13), + new ECB(1, 14))), + new Version(8, new int[]{6, 24, 42}, + new ECBlocks(24, new ECB(2, 97)), + new ECBlocks(22, new ECB(2, 38), + new ECB(2, 39)), + new ECBlocks(22, new ECB(4, 18), + new ECB(2, 19)), + new ECBlocks(26, new ECB(4, 14), + new ECB(2, 15))), + new Version(9, new int[]{6, 26, 46}, + new ECBlocks(30, new ECB(2, 116)), + new ECBlocks(22, new ECB(3, 36), + new ECB(2, 37)), + new ECBlocks(20, new ECB(4, 16), + new ECB(4, 17)), + new ECBlocks(24, new ECB(4, 12), + new ECB(4, 13))), + new Version(10, new int[]{6, 28, 50}, + new ECBlocks(18, new ECB(2, 68), + new ECB(2, 69)), + new ECBlocks(26, new ECB(4, 43), + new ECB(1, 44)), + new ECBlocks(24, new ECB(6, 19), + new ECB(2, 20)), + new ECBlocks(28, new ECB(6, 15), + new ECB(2, 16))), + new Version(11, new int[]{6, 30, 54}, + new ECBlocks(20, new ECB(4, 81)), + new ECBlocks(30, new ECB(1, 50), + new ECB(4, 51)), + new ECBlocks(28, new ECB(4, 22), + new ECB(4, 23)), + new ECBlocks(24, new ECB(3, 12), + new ECB(8, 13))), + new Version(12, new int[]{6, 32, 58}, + new ECBlocks(24, new ECB(2, 92), + new ECB(2, 93)), + new ECBlocks(22, new ECB(6, 36), + new ECB(2, 37)), + new ECBlocks(26, new ECB(4, 20), + new ECB(6, 21)), + new ECBlocks(28, new ECB(7, 14), + new ECB(4, 15))), + new Version(13, new int[]{6, 34, 62}, + new ECBlocks(26, new ECB(4, 107)), + new ECBlocks(22, new ECB(8, 37), + new ECB(1, 38)), + new ECBlocks(24, new ECB(8, 20), + new ECB(4, 21)), + new ECBlocks(22, new ECB(12, 11), + new ECB(4, 12))), + new Version(14, new int[]{6, 26, 46, 66}, + new ECBlocks(30, new ECB(3, 115), + new ECB(1, 116)), + new ECBlocks(24, new ECB(4, 40), + new ECB(5, 41)), + new ECBlocks(20, new ECB(11, 16), + new ECB(5, 17)), + new ECBlocks(24, new ECB(11, 12), + new ECB(5, 13))), + new Version(15, new int[]{6, 26, 48, 70}, + new ECBlocks(22, new ECB(5, 87), + new ECB(1, 88)), + new ECBlocks(24, new ECB(5, 41), + new ECB(5, 42)), + new ECBlocks(30, new ECB(5, 24), + new ECB(7, 25)), + new ECBlocks(24, new ECB(11, 12), + new ECB(7, 13))), + new Version(16, new int[]{6, 26, 50, 74}, + new ECBlocks(24, new ECB(5, 98), + new ECB(1, 99)), + new ECBlocks(28, new ECB(7, 45), + new ECB(3, 46)), + new ECBlocks(24, new ECB(15, 19), + new ECB(2, 20)), + new ECBlocks(30, new ECB(3, 15), + new ECB(13, 16))), + new Version(17, new int[]{6, 30, 54, 78}, + new ECBlocks(28, new ECB(1, 107), + new ECB(5, 108)), + new ECBlocks(28, new ECB(10, 46), + new ECB(1, 47)), + new ECBlocks(28, new ECB(1, 22), + new ECB(15, 23)), + new ECBlocks(28, new ECB(2, 14), + new ECB(17, 15))), + new Version(18, new int[]{6, 30, 56, 82}, + new ECBlocks(30, new ECB(5, 120), + new ECB(1, 121)), + new ECBlocks(26, new ECB(9, 43), + new ECB(4, 44)), + new ECBlocks(28, new ECB(17, 22), + new ECB(1, 23)), + new ECBlocks(28, new ECB(2, 14), + new ECB(19, 15))), + new Version(19, new int[]{6, 30, 58, 86}, + new ECBlocks(28, new ECB(3, 113), + new ECB(4, 114)), + new ECBlocks(26, new ECB(3, 44), + new ECB(11, 45)), + new ECBlocks(26, new ECB(17, 21), + new ECB(4, 22)), + new ECBlocks(26, new ECB(9, 13), + new ECB(16, 14))), + new Version(20, new int[]{6, 34, 62, 90}, + new ECBlocks(28, new ECB(3, 107), + new ECB(5, 108)), + new ECBlocks(26, new ECB(3, 41), + new ECB(13, 42)), + new ECBlocks(30, new ECB(15, 24), + new ECB(5, 25)), + new ECBlocks(28, new ECB(15, 15), + new ECB(10, 16))), + new Version(21, new int[]{6, 28, 50, 72, 94}, + new ECBlocks(28, new ECB(4, 116), + new ECB(4, 117)), + new ECBlocks(26, new ECB(17, 42)), + new ECBlocks(28, new ECB(17, 22), + new ECB(6, 23)), + new ECBlocks(30, new ECB(19, 16), + new ECB(6, 17))), + new Version(22, new int[]{6, 26, 50, 74, 98}, + new ECBlocks(28, new ECB(2, 111), + new ECB(7, 112)), + new ECBlocks(28, new ECB(17, 46)), + new ECBlocks(30, new ECB(7, 24), + new ECB(16, 25)), + new ECBlocks(24, new ECB(34, 13))), + new Version(23, new int[]{6, 30, 54, 78, 102}, + new ECBlocks(30, new ECB(4, 121), + new ECB(5, 122)), + new ECBlocks(28, new ECB(4, 47), + new ECB(14, 48)), + new ECBlocks(30, new ECB(11, 24), + new ECB(14, 25)), + new ECBlocks(30, new ECB(16, 15), + new ECB(14, 16))), + new Version(24, new int[]{6, 28, 54, 80, 106}, + new ECBlocks(30, new ECB(6, 117), + new ECB(4, 118)), + new ECBlocks(28, new ECB(6, 45), + new ECB(14, 46)), + new ECBlocks(30, new ECB(11, 24), + new ECB(16, 25)), + new ECBlocks(30, new ECB(30, 16), + new ECB(2, 17))), + new Version(25, new int[]{6, 32, 58, 84, 110}, + new ECBlocks(26, new ECB(8, 106), + new ECB(4, 107)), + new ECBlocks(28, new ECB(8, 47), + new ECB(13, 48)), + new ECBlocks(30, new ECB(7, 24), + new ECB(22, 25)), + new ECBlocks(30, new ECB(22, 15), + new ECB(13, 16))), + new Version(26, new int[]{6, 30, 58, 86, 114}, + new ECBlocks(28, new ECB(10, 114), + new ECB(2, 115)), + new ECBlocks(28, new ECB(19, 46), + new ECB(4, 47)), + new ECBlocks(28, new ECB(28, 22), + new ECB(6, 23)), + new ECBlocks(30, new ECB(33, 16), + new ECB(4, 17))), + new Version(27, new int[]{6, 34, 62, 90, 118}, + new ECBlocks(30, new ECB(8, 122), + new ECB(4, 123)), + new ECBlocks(28, new ECB(22, 45), + new ECB(3, 46)), + new ECBlocks(30, new ECB(8, 23), + new ECB(26, 24)), + new ECBlocks(30, new ECB(12, 15), + new ECB(28, 16))), + new Version(28, new int[]{6, 26, 50, 74, 98, 122}, + new ECBlocks(30, new ECB(3, 117), + new ECB(10, 118)), + new ECBlocks(28, new ECB(3, 45), + new ECB(23, 46)), + new ECBlocks(30, new ECB(4, 24), + new ECB(31, 25)), + new ECBlocks(30, new ECB(11, 15), + new ECB(31, 16))), + new Version(29, new int[]{6, 30, 54, 78, 102, 126}, + new ECBlocks(30, new ECB(7, 116), + new ECB(7, 117)), + new ECBlocks(28, new ECB(21, 45), + new ECB(7, 46)), + new ECBlocks(30, new ECB(1, 23), + new ECB(37, 24)), + new ECBlocks(30, new ECB(19, 15), + new ECB(26, 16))), + new Version(30, new int[]{6, 26, 52, 78, 104, 130}, + new ECBlocks(30, new ECB(5, 115), + new ECB(10, 116)), + new ECBlocks(28, new ECB(19, 47), + new ECB(10, 48)), + new ECBlocks(30, new ECB(15, 24), + new ECB(25, 25)), + new ECBlocks(30, new ECB(23, 15), + new ECB(25, 16))), + new Version(31, new int[]{6, 30, 56, 82, 108, 134}, + new ECBlocks(30, new ECB(13, 115), + new ECB(3, 116)), + new ECBlocks(28, new ECB(2, 46), + new ECB(29, 47)), + new ECBlocks(30, new ECB(42, 24), + new ECB(1, 25)), + new ECBlocks(30, new ECB(23, 15), + new ECB(28, 16))), + new Version(32, new int[]{6, 34, 60, 86, 112, 138}, + new ECBlocks(30, new ECB(17, 115)), + new ECBlocks(28, new ECB(10, 46), + new ECB(23, 47)), + new ECBlocks(30, new ECB(10, 24), + new ECB(35, 25)), + new ECBlocks(30, new ECB(19, 15), + new ECB(35, 16))), + new Version(33, new int[]{6, 30, 58, 86, 114, 142}, + new ECBlocks(30, new ECB(17, 115), + new ECB(1, 116)), + new ECBlocks(28, new ECB(14, 46), + new ECB(21, 47)), + new ECBlocks(30, new ECB(29, 24), + new ECB(19, 25)), + new ECBlocks(30, new ECB(11, 15), + new ECB(46, 16))), + new Version(34, new int[]{6, 34, 62, 90, 118, 146}, + new ECBlocks(30, new ECB(13, 115), + new ECB(6, 116)), + new ECBlocks(28, new ECB(14, 46), + new ECB(23, 47)), + new ECBlocks(30, new ECB(44, 24), + new ECB(7, 25)), + new ECBlocks(30, new ECB(59, 16), + new ECB(1, 17))), + new Version(35, new int[]{6, 30, 54, 78, 102, 126, 150}, + new ECBlocks(30, new ECB(12, 121), + new ECB(7, 122)), + new ECBlocks(28, new ECB(12, 47), + new ECB(26, 48)), + new ECBlocks(30, new ECB(39, 24), + new ECB(14, 25)), + new ECBlocks(30, new ECB(22, 15), + new ECB(41, 16))), + new Version(36, new int[]{6, 24, 50, 76, 102, 128, 154}, + new ECBlocks(30, new ECB(6, 121), + new ECB(14, 122)), + new ECBlocks(28, new ECB(6, 47), + new ECB(34, 48)), + new ECBlocks(30, new ECB(46, 24), + new ECB(10, 25)), + new ECBlocks(30, new ECB(2, 15), + new ECB(64, 16))), + new Version(37, new int[]{6, 28, 54, 80, 106, 132, 158}, + new ECBlocks(30, new ECB(17, 122), + new ECB(4, 123)), + new ECBlocks(28, new ECB(29, 46), + new ECB(14, 47)), + new ECBlocks(30, new ECB(49, 24), + new ECB(10, 25)), + new ECBlocks(30, new ECB(24, 15), + new ECB(46, 16))), + new Version(38, new int[]{6, 32, 58, 84, 110, 136, 162}, + new ECBlocks(30, new ECB(4, 122), + new ECB(18, 123)), + new ECBlocks(28, new ECB(13, 46), + new ECB(32, 47)), + new ECBlocks(30, new ECB(48, 24), + new ECB(14, 25)), + new ECBlocks(30, new ECB(42, 15), + new ECB(32, 16))), + new Version(39, new int[]{6, 26, 54, 82, 110, 138, 166}, + new ECBlocks(30, new ECB(20, 117), + new ECB(4, 118)), + new ECBlocks(28, new ECB(40, 47), + new ECB(7, 48)), + new ECBlocks(30, new ECB(43, 24), + new ECB(22, 25)), + new ECBlocks(30, new ECB(10, 15), + new ECB(67, 16))), + new Version(40, new int[]{6, 30, 58, 86, 114, 142, 170}, + new ECBlocks(30, new ECB(19, 118), + new ECB(6, 119)), + new ECBlocks(28, new ECB(18, 47), + new ECB(31, 48)), + new ECBlocks(30, new ECB(34, 24), + new ECB(34, 25)), + new ECBlocks(30, new ECB(20, 15), + new ECB(61, 16))) + }; + } + +} diff --git a/src/com/google/zxing/qrcode/detector/AlignmentPattern.java b/src/com/google/zxing/qrcode/detector/AlignmentPattern.java new file mode 100644 index 0000000..6fc1a2c --- /dev/null +++ b/src/com/google/zxing/qrcode/detector/AlignmentPattern.java @@ -0,0 +1,48 @@ +/* + * Copyright 2007 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.qrcode.detector; + +import com.google.zxing.ResultPoint; + +/** + *Encapsulates an alignment pattern, which are the smaller square patterns found in + * all but the simplest QR Codes.
+ * + * @author Sean Owen + */ +public final class AlignmentPattern extends ResultPoint { + + private final float estimatedModuleSize; + + AlignmentPattern(float posX, float posY, float estimatedModuleSize) { + super(posX, posY); + this.estimatedModuleSize = estimatedModuleSize; + } + + /** + *Determines if this alignment pattern "about equals" an alignment pattern at the stated + * position and size -- meaning, it is at nearly the same center with nearly the same size.
+ */ + boolean aboutEquals(float moduleSize, float i, float j) { + if (Math.abs(i - getY()) <= moduleSize && Math.abs(j - getX()) <= moduleSize) { + float moduleSizeDiff = Math.abs(moduleSize - estimatedModuleSize); + return moduleSizeDiff <= 1.0f || moduleSizeDiff / estimatedModuleSize <= 1.0f; + } + return false; + } + +} \ No newline at end of file diff --git a/src/com/google/zxing/qrcode/detector/AlignmentPatternFinder.java b/src/com/google/zxing/qrcode/detector/AlignmentPatternFinder.java new file mode 100644 index 0000000..6d5d2fe --- /dev/null +++ b/src/com/google/zxing/qrcode/detector/AlignmentPatternFinder.java @@ -0,0 +1,279 @@ +/* + * Copyright 2007 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.qrcode.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; + +import java.util.Vector; + +/** + *This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder + * patterns but are smaller and appear at regular intervals throughout the image.
+ * + *At the moment this only looks for the bottom-right alignment pattern.
+ * + *This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied, + * pasted and stripped down here for maximum performance but does unfortunately duplicate + * some code.
+ * + *This class is thread-safe but not reentrant. Each thread must allocate its own object. + * + * @author Sean Owen + */ +final class AlignmentPatternFinder { + + private final BitMatrix image; + private final Vector possibleCenters; + private final int startX; + private final int startY; + private final int width; + private final int height; + private final float moduleSize; + private final int[] crossCheckStateCount; + private final ResultPointCallback resultPointCallback; + + /** + *
Creates a finder that will look in a portion of the whole image.
+ * + * @param image image to search + * @param startX left column from which to start searching + * @param startY top row from which to start searching + * @param width width of region to search + * @param height height of region to search + * @param moduleSize estimated module size so far + */ + AlignmentPatternFinder(BitMatrix image, + int startX, + int startY, + int width, + int height, + float moduleSize, + ResultPointCallback resultPointCallback) { + this.image = image; + this.possibleCenters = new Vector(5); + this.startX = startX; + this.startY = startY; + this.width = width; + this.height = height; + this.moduleSize = moduleSize; + this.crossCheckStateCount = new int[3]; + this.resultPointCallback = resultPointCallback; + } + + /** + *This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since + * it's pretty performance-critical and so is written to be fast foremost.
+ * + * @return {@link AlignmentPattern} if found + * @throws NotFoundException if not found + */ + AlignmentPattern find() throws NotFoundException { + int startX = this.startX; + int height = this.height; + int maxJ = startX + width; + int middleI = startY + (height >> 1); + // We are looking for black/white/black modules in 1:1:1 ratio; + // this tracks the number of black/white/black modules seen so far + int[] stateCount = new int[3]; + for (int iGen = 0; iGen < height; iGen++) { + // Search from middle outwards + int i = middleI + ((iGen & 0x01) == 0 ? ((iGen + 1) >> 1) : -((iGen + 1) >> 1)); + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + int j = startX; + // Burn off leading white pixels before anything else; if we start in the middle of + // a white run, it doesn't make sense to count its length, since we don't know if the + // white run continued to the left of the start point + while (j < maxJ && !image.get(j, i)) { + j++; + } + int currentState = 0; + while (j < maxJ) { + if (image.get(j, i)) { + // Black pixel + if (currentState == 1) { // Counting black pixels + stateCount[currentState]++; + } else { // Counting white pixels + if (currentState == 2) { // A winner? + if (foundPatternCross(stateCount)) { // Yes + AlignmentPattern confirmed = handlePossibleCenter(stateCount, i, j); + if (confirmed != null) { + return confirmed; + } + } + stateCount[0] = stateCount[2]; + stateCount[1] = 1; + stateCount[2] = 0; + currentState = 1; + } else { + stateCount[++currentState]++; + } + } + } else { // White pixel + if (currentState == 1) { // Counting black pixels + currentState++; + } + stateCount[currentState]++; + } + j++; + } + if (foundPatternCross(stateCount)) { + AlignmentPattern confirmed = handlePossibleCenter(stateCount, i, maxJ); + if (confirmed != null) { + return confirmed; + } + } + + } + + // Hmm, nothing we saw was observed and confirmed twice. If we had + // any guess at all, return it. + if (!possibleCenters.isEmpty()) { + return (AlignmentPattern) possibleCenters.elementAt(0); + } + + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Given a count of black/white/black pixels just seen and an end position, + * figures the location of the center of this black/white/black run. + */ + private static float centerFromEnd(int[] stateCount, int end) { + return (float) (end - stateCount[2]) - stateCount[1] / 2.0f; + } + + /** + * @param stateCount count of black/white/black pixels just read + * @return true iff the proportions of the counts is close enough to the 1/1/1 ratios + * used by alignment patterns to be considered a match + */ + private boolean foundPatternCross(int[] stateCount) { + float moduleSize = this.moduleSize; + float maxVariance = moduleSize / 2.0f; + for (int i = 0; i < 3; i++) { + if (Math.abs(moduleSize - stateCount[i]) >= maxVariance) { + return false; + } + } + return true; + } + + /** + *After a horizontal scan finds a potential alignment pattern, this method + * "cross-checks" by scanning down vertically through the center of the possible + * alignment pattern to see if the same proportion is detected.
+ * + * @param startI row where an alignment pattern was detected + * @param centerJ center of the section that appears to cross an alignment pattern + * @param maxCount maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @return vertical center of alignment pattern, or {@link Float#NaN} if not found + */ + private float crossCheckVertical(int startI, int centerJ, int maxCount, + int originalStateCountTotal) { + BitMatrix image = this.image; + + int maxI = image.getHeight(); + int[] stateCount = crossCheckStateCount; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + + // Start counting up from center + int i = startI; + while (i >= 0 && image.get(centerJ, i) && stateCount[1] <= maxCount) { + stateCount[1]++; + i--; + } + // If already too many modules in this state or ran off the edge: + if (i < 0 || stateCount[1] > maxCount) { + return Float.NaN; + } + while (i >= 0 && !image.get(centerJ, i) && stateCount[0] <= maxCount) { + stateCount[0]++; + i--; + } + if (stateCount[0] > maxCount) { + return Float.NaN; + } + + // Now also count down from center + i = startI + 1; + while (i < maxI && image.get(centerJ, i) && stateCount[1] <= maxCount) { + stateCount[1]++; + i++; + } + if (i == maxI || stateCount[1] > maxCount) { + return Float.NaN; + } + while (i < maxI && !image.get(centerJ, i) && stateCount[2] <= maxCount) { + stateCount[2]++; + i++; + } + if (stateCount[2] > maxCount) { + return Float.NaN; + } + + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) { + return Float.NaN; + } + + return foundPatternCross(stateCount) ? centerFromEnd(stateCount, i) : Float.NaN; + } + + /** + *This is called when a horizontal scan finds a possible alignment pattern. It will + * cross check with a vertical scan, and if successful, will see if this pattern had been + * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have + * found the alignment pattern.
+ * + * @param stateCount reading state module counts from horizontal scan + * @param i row where alignment pattern may be found + * @param j end of possible alignment pattern in row + * @return {@link AlignmentPattern} if we have found the same pattern twice, or null if not + */ + private AlignmentPattern handlePossibleCenter(int[] stateCount, int i, int j) { + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2]; + float centerJ = centerFromEnd(stateCount, j); + float centerI = crossCheckVertical(i, (int) centerJ, 2 * stateCount[1], stateCountTotal); + if (!Float.isNaN(centerI)) { + float estimatedModuleSize = (float) (stateCount[0] + stateCount[1] + stateCount[2]) / 3.0f; + int max = possibleCenters.size(); + for (int index = 0; index < max; index++) { + AlignmentPattern center = (AlignmentPattern) possibleCenters.elementAt(index); + // Look for about the same center and module size: + if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) { + return new AlignmentPattern(centerJ, centerI, estimatedModuleSize); + } + } + // Hadn't found this before; save it + ResultPoint point = new AlignmentPattern(centerJ, centerI, estimatedModuleSize); + possibleCenters.addElement(point); + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(point); + } + } + return null; + } + +} diff --git a/src/com/google/zxing/qrcode/detector/Detector.java b/src/com/google/zxing/qrcode/detector/Detector.java new file mode 100644 index 0000000..e6b860b --- /dev/null +++ b/src/com/google/zxing/qrcode/detector/Detector.java @@ -0,0 +1,400 @@ +/* + * Copyright 2007 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.qrcode.detector; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.common.GridSampler; +import com.google.zxing.common.PerspectiveTransform; +import com.google.zxing.qrcode.decoder.Version; + +import java.util.Hashtable; + +/** + *Encapsulates logic that can detect a QR Code in an image, even if the QR Code + * is rotated or skewed, or partially obscured.
+ * + * @author Sean Owen + */ +public class Detector { + + private final BitMatrix image; + private ResultPointCallback resultPointCallback; + + public Detector(BitMatrix image) { + this.image = image; + } + + protected BitMatrix getImage() { + return image; + } + + protected ResultPointCallback getResultPointCallback() { + return resultPointCallback; + } + + /** + *Detects a QR Code in an image, simply.
+ * + * @return {@link DetectorResult} encapsulating results of detecting a QR Code + * @throws NotFoundException if no QR Code can be found + */ + public DetectorResult detect() throws NotFoundException, FormatException { + return detect(null); + } + + /** + *Detects a QR Code in an image, simply.
+ * + * @param hints optional hints to detector + * @return {@link NotFoundException} encapsulating results of detecting a QR Code + * @throws NotFoundException if QR Code cannot be found + * @throws FormatException if a QR Code cannot be decoded + */ + public DetectorResult detect(Hashtable hints) throws NotFoundException, FormatException { + + resultPointCallback = hints == null ? null : + (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + + FinderPatternFinder finder = new FinderPatternFinder(image, resultPointCallback); + FinderPatternInfo info = finder.find(hints); + + return processFinderPatternInfo(info); + } + + protected DetectorResult processFinderPatternInfo(FinderPatternInfo info) + throws NotFoundException, FormatException { + + FinderPattern topLeft = info.getTopLeft(); + FinderPattern topRight = info.getTopRight(); + FinderPattern bottomLeft = info.getBottomLeft(); + + float moduleSize = calculateModuleSize(topLeft, topRight, bottomLeft); + if (moduleSize < 1.0f) { + throw NotFoundException.getNotFoundInstance(); + } + int dimension = computeDimension(topLeft, topRight, bottomLeft, moduleSize); + Version provisionalVersion = Version.getProvisionalVersionForDimension(dimension); + int modulesBetweenFPCenters = provisionalVersion.getDimensionForVersion() - 7; + + AlignmentPattern alignmentPattern = null; + // Anything above version 1 has an alignment pattern + if (provisionalVersion.getAlignmentPatternCenters().length > 0) { + + // Guess where a "bottom right" finder pattern would have been + float bottomRightX = topRight.getX() - topLeft.getX() + bottomLeft.getX(); + float bottomRightY = topRight.getY() - topLeft.getY() + bottomLeft.getY(); + + // Estimate that alignment pattern is closer by 3 modules + // from "bottom right" to known top left location + float correctionToTopLeft = 1.0f - 3.0f / (float) modulesBetweenFPCenters; + int estAlignmentX = (int) (topLeft.getX() + correctionToTopLeft * (bottomRightX - topLeft.getX())); + int estAlignmentY = (int) (topLeft.getY() + correctionToTopLeft * (bottomRightY - topLeft.getY())); + + // Kind of arbitrary -- expand search radius before giving up + for (int i = 4; i <= 16; i <<= 1) { + try { + alignmentPattern = findAlignmentInRegion(moduleSize, + estAlignmentX, + estAlignmentY, + (float) i); + break; + } catch (NotFoundException re) { + // try next round + } + } + // If we didn't find alignment pattern... well try anyway without it + } + + PerspectiveTransform transform = + createTransform(topLeft, topRight, bottomLeft, alignmentPattern, dimension); + + BitMatrix bits = sampleGrid(image, transform, dimension); + + ResultPoint[] points; + if (alignmentPattern == null) { + points = new ResultPoint[]{bottomLeft, topLeft, topRight}; + } else { + points = new ResultPoint[]{bottomLeft, topLeft, topRight, alignmentPattern}; + } + return new DetectorResult(bits, points); + } + + public PerspectiveTransform createTransform(ResultPoint topLeft, + ResultPoint topRight, + ResultPoint bottomLeft, + ResultPoint alignmentPattern, + int dimension) { + float dimMinusThree = (float) dimension - 3.5f; + float bottomRightX; + float bottomRightY; + float sourceBottomRightX; + float sourceBottomRightY; + if (alignmentPattern != null) { + bottomRightX = alignmentPattern.getX(); + bottomRightY = alignmentPattern.getY(); + sourceBottomRightX = sourceBottomRightY = dimMinusThree - 3.0f; + } else { + // Don't have an alignment pattern, just make up the bottom-right point + bottomRightX = (topRight.getX() - topLeft.getX()) + bottomLeft.getX(); + bottomRightY = (topRight.getY() - topLeft.getY()) + bottomLeft.getY(); + sourceBottomRightX = sourceBottomRightY = dimMinusThree; + } + + return PerspectiveTransform.quadrilateralToQuadrilateral( + 3.5f, + 3.5f, + dimMinusThree, + 3.5f, + sourceBottomRightX, + sourceBottomRightY, + 3.5f, + dimMinusThree, + topLeft.getX(), + topLeft.getY(), + topRight.getX(), + topRight.getY(), + bottomRightX, + bottomRightY, + bottomLeft.getX(), + bottomLeft.getY()); + } + + private static BitMatrix sampleGrid(BitMatrix image, + PerspectiveTransform transform, + int dimension) throws NotFoundException { + + GridSampler sampler = GridSampler.getInstance(); + return sampler.sampleGrid(image, dimension, transform); + } + + /** + *Computes the dimension (number of modules on a size) of the QR Code based on the position + * of the finder patterns and estimated module size.
+ */ + protected static int computeDimension(ResultPoint topLeft, + ResultPoint topRight, + ResultPoint bottomLeft, + float moduleSize) throws NotFoundException { + int tltrCentersDimension = round(ResultPoint.distance(topLeft, topRight) / moduleSize); + int tlblCentersDimension = round(ResultPoint.distance(topLeft, bottomLeft) / moduleSize); + int dimension = ((tltrCentersDimension + tlblCentersDimension) >> 1) + 7; + switch (dimension & 0x03) { // mod 4 + case 0: + dimension++; + break; + // 1? do nothing + case 2: + dimension--; + break; + case 3: + throw NotFoundException.getNotFoundInstance(); + } + return dimension; + } + + /** + *Computes an average estimated module size based on estimated derived from the positions + * of the three finder patterns.
+ */ + protected float calculateModuleSize(ResultPoint topLeft, + ResultPoint topRight, + ResultPoint bottomLeft) { + // Take the average + return (calculateModuleSizeOneWay(topLeft, topRight) + + calculateModuleSizeOneWay(topLeft, bottomLeft)) / 2.0f; + } + + /** + *Estimates module size based on two finder patterns -- it uses + * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the + * width of each, measuring along the axis between their centers.
+ */ + private float calculateModuleSizeOneWay(ResultPoint pattern, ResultPoint otherPattern) { + float moduleSizeEst1 = sizeOfBlackWhiteBlackRunBothWays((int) pattern.getX(), + (int) pattern.getY(), + (int) otherPattern.getX(), + (int) otherPattern.getY()); + float moduleSizeEst2 = sizeOfBlackWhiteBlackRunBothWays((int) otherPattern.getX(), + (int) otherPattern.getY(), + (int) pattern.getX(), + (int) pattern.getY()); + if (Float.isNaN(moduleSizeEst1)) { + return moduleSizeEst2 / 7.0f; + } + if (Float.isNaN(moduleSizeEst2)) { + return moduleSizeEst1 / 7.0f; + } + // Average them, and divide by 7 since we've counted the width of 3 black modules, + // and 1 white and 1 black module on either side. Ergo, divide sum by 14. + return (moduleSizeEst1 + moduleSizeEst2) / 14.0f; + } + + /** + * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of + * a finder pattern by looking for a black-white-black run from the center in the direction + * of another point (another finder pattern center), and in the opposite direction too. + */ + private float sizeOfBlackWhiteBlackRunBothWays(int fromX, int fromY, int toX, int toY) { + + float result = sizeOfBlackWhiteBlackRun(fromX, fromY, toX, toY); + + // Now count other way -- don't run off image though of course + float scale = 1.0f; + int otherToX = fromX - (toX - fromX); + if (otherToX < 0) { + scale = (float) fromX / (float) (fromX - otherToX); + otherToX = 0; + } else if (otherToX > image.getWidth()) { + scale = (float) (image.getWidth() - fromX) / (float) (otherToX - fromX); + otherToX = image.getWidth(); + } + int otherToY = (int) (fromY - (toY - fromY) * scale); + + scale = 1.0f; + if (otherToY < 0) { + scale = (float) fromY / (float) (fromY - otherToY); + otherToY = 0; + } else if (otherToY > image.getHeight()) { + scale = (float) (image.getHeight() - fromY) / (float) (otherToY - fromY); + otherToY = image.getHeight(); + } + otherToX = (int) (fromX + (otherToX - fromX) * scale); + + result += sizeOfBlackWhiteBlackRun(fromX, fromY, otherToX, otherToY); + return result; + } + + /** + *This method traces a line from a point in the image, in the direction towards another point. + * It begins in a black region, and keeps going until it finds white, then black, then white again. + * It reports the distance from the start to this point.
+ * + *This is used when figuring out how wide a finder pattern is, when the finder pattern + * may be skewed or rotated.
+ */ + private float sizeOfBlackWhiteBlackRun(int fromX, int fromY, int toX, int toY) { + // Mild variant of Bresenham's algorithm; + // see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm + boolean steep = Math.abs(toY - fromY) > Math.abs(toX - fromX); + if (steep) { + int temp = fromX; + fromX = fromY; + fromY = temp; + temp = toX; + toX = toY; + toY = temp; + } + + int dx = Math.abs(toX - fromX); + int dy = Math.abs(toY - fromY); + int error = -dx >> 1; + int ystep = fromY < toY ? 1 : -1; + int xstep = fromX < toX ? 1 : -1; + int state = 0; // In black pixels, looking for white, first or second time + for (int x = fromX, y = fromY; x != toX; x += xstep) { + + int realX = steep ? y : x; + int realY = steep ? x : y; + if (state == 1) { // In white pixels, looking for black + if (image.get(realX, realY)) { + state++; + } + } else { + if (!image.get(realX, realY)) { + state++; + } + } + + if (state == 3) { // Found black, white, black, and stumbled back onto white; done + int diffX = x - fromX; + int diffY = y - fromY; + if (xstep < 0) { + diffX++; + } + return (float) Math.sqrt((double) (diffX * diffX + diffY * diffY)); + } + error += dy; + if (error > 0) { + if (y == toY) { + break; + } + y += ystep; + error -= dx; + } + } + int diffX = toX - fromX; + int diffY = toY - fromY; + return (float) Math.sqrt((double) (diffX * diffX + diffY * diffY)); + } + + /** + *Attempts to locate an alignment pattern in a limited region of the image, which is + * guessed to contain it. This method uses {@link AlignmentPattern}.
+ * + * @param overallEstModuleSize estimated module size so far + * @param estAlignmentX x coordinate of center of area probably containing alignment pattern + * @param estAlignmentY y coordinate of above + * @param allowanceFactor number of pixels in all directions to search from the center + * @return {@link AlignmentPattern} if found, or null otherwise + * @throws NotFoundException if an unexpected error occurs during detection + */ + protected AlignmentPattern findAlignmentInRegion(float overallEstModuleSize, + int estAlignmentX, + int estAlignmentY, + float allowanceFactor) + throws NotFoundException { + // Look for an alignment pattern (3 modules in size) around where it + // should be + int allowance = (int) (allowanceFactor * overallEstModuleSize); + int alignmentAreaLeftX = Math.max(0, estAlignmentX - allowance); + int alignmentAreaRightX = Math.min(image.getWidth() - 1, estAlignmentX + allowance); + if (alignmentAreaRightX - alignmentAreaLeftX < overallEstModuleSize * 3) { + throw NotFoundException.getNotFoundInstance(); + } + + int alignmentAreaTopY = Math.max(0, estAlignmentY - allowance); + int alignmentAreaBottomY = Math.min(image.getHeight() - 1, estAlignmentY + allowance); + if (alignmentAreaBottomY - alignmentAreaTopY < overallEstModuleSize * 3) { + throw NotFoundException.getNotFoundInstance(); + } + + AlignmentPatternFinder alignmentFinder = + new AlignmentPatternFinder( + image, + alignmentAreaLeftX, + alignmentAreaTopY, + alignmentAreaRightX - alignmentAreaLeftX, + alignmentAreaBottomY - alignmentAreaTopY, + overallEstModuleSize, + resultPointCallback); + return alignmentFinder.find(); + } + + /** + * Ends up being a bit faster than Math.round(). This merely rounds its argument to the nearest int, + * where x.5 rounds up. + */ + private static int round(float d) { + return (int) (d + 0.5f); + } +} diff --git a/src/com/google/zxing/qrcode/detector/FinderPattern.java b/src/com/google/zxing/qrcode/detector/FinderPattern.java new file mode 100644 index 0000000..7a9914d --- /dev/null +++ b/src/com/google/zxing/qrcode/detector/FinderPattern.java @@ -0,0 +1,63 @@ +/* + * Copyright 2007 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.qrcode.detector; + +import com.google.zxing.ResultPoint; + +/** + *Encapsulates a finder pattern, which are the three square patterns found in + * the corners of QR Codes. It also encapsulates a count of similar finder patterns, + * as a convenience to the finder's bookkeeping.
+ * + * @author Sean Owen + */ +public final class FinderPattern extends ResultPoint { + + private final float estimatedModuleSize; + private int count; + + FinderPattern(float posX, float posY, float estimatedModuleSize) { + super(posX, posY); + this.estimatedModuleSize = estimatedModuleSize; + this.count = 1; + } + + public float getEstimatedModuleSize() { + return estimatedModuleSize; + } + + int getCount() { + return count; + } + + void incrementCount() { + this.count++; + } + + /** + *Determines if this finder pattern "about equals" a finder pattern at the stated + * position and size -- meaning, it is at nearly the same center with nearly the same size.
+ */ + boolean aboutEquals(float moduleSize, float i, float j) { + if (Math.abs(i - getY()) <= moduleSize && Math.abs(j - getX()) <= moduleSize) { + float moduleSizeDiff = Math.abs(moduleSize - estimatedModuleSize); + return moduleSizeDiff <= 1.0f || moduleSizeDiff / estimatedModuleSize <= 1.0f; + } + return false; + } + +} diff --git a/src/com/google/zxing/qrcode/detector/FinderPatternFinder.java b/src/com/google/zxing/qrcode/detector/FinderPatternFinder.java new file mode 100644 index 0000000..2b4078b --- /dev/null +++ b/src/com/google/zxing/qrcode/detector/FinderPatternFinder.java @@ -0,0 +1,585 @@ +/* + * Copyright 2007 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.qrcode.detector; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.Collections; +import com.google.zxing.common.Comparator; + +import java.util.Hashtable; +import java.util.Vector; + +/** + *This class attempts to find finder patterns in a QR Code. Finder patterns are the square + * markers at three corners of a QR Code.
+ * + *This class is thread-safe but not reentrant. Each thread must allocate its own object. + * + * @author Sean Owen + */ +public class FinderPatternFinder { + + private static final int CENTER_QUORUM = 2; + protected static final int MIN_SKIP = 3; // 1 pixel/module times 3 modules/center + protected static final int MAX_MODULES = 57; // support up to version 10 for mobile clients + private static final int INTEGER_MATH_SHIFT = 8; + + private final BitMatrix image; + private final Vector possibleCenters; + private boolean hasSkipped; + private final int[] crossCheckStateCount; + private final ResultPointCallback resultPointCallback; + + /** + *
Creates a finder that will search the image for three finder patterns.
+ * + * @param image image to search + */ + public FinderPatternFinder(BitMatrix image) { + this(image, null); + } + + public FinderPatternFinder(BitMatrix image, ResultPointCallback resultPointCallback) { + this.image = image; + this.possibleCenters = new Vector(); + this.crossCheckStateCount = new int[5]; + this.resultPointCallback = resultPointCallback; + } + + protected BitMatrix getImage() { + return image; + } + + protected Vector getPossibleCenters() { + return possibleCenters; + } + + FinderPatternInfo find(Hashtable hints) throws NotFoundException { + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + int maxI = image.getHeight(); + int maxJ = image.getWidth(); + // We are looking for black/white/black/white/black modules in + // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far + + // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the + // image, and then account for the center being 3 modules in size. This gives the smallest + // number of pixels the center could be, so skip this often. When trying harder, look for all + // QR versions regardless of how dense they are. + int iSkip = (3 * maxI) / (4 * MAX_MODULES); + if (iSkip < MIN_SKIP || tryHarder) { + iSkip = MIN_SKIP; + } + + boolean done = false; + int[] stateCount = new int[5]; + for (int i = iSkip - 1; i < maxI && !done; i += iSkip) { + // Get a row of black/white values + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + int currentState = 0; + for (int j = 0; j < maxJ; j++) { + if (image.get(j, i)) { + // Black pixel + if ((currentState & 1) == 1) { // Counting white pixels + currentState++; + } + stateCount[currentState]++; + } else { // White pixel + if ((currentState & 1) == 0) { // Counting black pixels + if (currentState == 4) { // A winner? + if (foundPatternCross(stateCount)) { // Yes + boolean confirmed = handlePossibleCenter(stateCount, i, j); + if (confirmed) { + // Start examining every other line. Checking each line turned out to be too + // expensive and didn't improve performance. + iSkip = 2; + if (hasSkipped) { + done = haveMultiplyConfirmedCenters(); + } else { + int rowSkip = findRowSkip(); + if (rowSkip > stateCount[2]) { + // Skip rows between row of lower confirmed center + // and top of presumed third confirmed center + // but back up a bit to get a full chance of detecting + // it, entire width of center of finder pattern + + // Skip by rowSkip, but back off by stateCount[2] (size of last center + // of pattern we saw) to be conservative, and also back off by iSkip which + // is about to be re-added + i += rowSkip - stateCount[2] - iSkip; + j = maxJ - 1; + } + } + } else { + stateCount[0] = stateCount[2]; + stateCount[1] = stateCount[3]; + stateCount[2] = stateCount[4]; + stateCount[3] = 1; + stateCount[4] = 0; + currentState = 3; + continue; + } + // Clear state to start looking again + currentState = 0; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + } else { // No, shift counts back by two + stateCount[0] = stateCount[2]; + stateCount[1] = stateCount[3]; + stateCount[2] = stateCount[4]; + stateCount[3] = 1; + stateCount[4] = 0; + currentState = 3; + } + } else { + stateCount[++currentState]++; + } + } else { // Counting white pixels + stateCount[currentState]++; + } + } + } + if (foundPatternCross(stateCount)) { + boolean confirmed = handlePossibleCenter(stateCount, i, maxJ); + if (confirmed) { + iSkip = stateCount[0]; + if (hasSkipped) { + // Found a third one + done = haveMultiplyConfirmedCenters(); + } + } + } + } + + FinderPattern[] patternInfo = selectBestPatterns(); + ResultPoint.orderBestPatterns(patternInfo); + + return new FinderPatternInfo(patternInfo); + } + + /** + * Given a count of black/white/black/white/black pixels just seen and an end position, + * figures the location of the center of this run. + */ + private static float centerFromEnd(int[] stateCount, int end) { + return (float) (end - stateCount[4] - stateCount[3]) - stateCount[2] / 2.0f; + } + + /** + * @param stateCount count of black/white/black/white/black pixels just read + * @return true iff the proportions of the counts is close enough to the 1/1/3/1/1 ratios + * used by finder patterns to be considered a match + */ + protected static boolean foundPatternCross(int[] stateCount) { + int totalModuleSize = 0; + for (int i = 0; i < 5; i++) { + int count = stateCount[i]; + if (count == 0) { + return false; + } + totalModuleSize += count; + } + if (totalModuleSize < 7) { + return false; + } + int moduleSize = (totalModuleSize << INTEGER_MATH_SHIFT) / 7; + int maxVariance = moduleSize / 2; + // Allow less than 50% variance from 1-1-3-1-1 proportions + return Math.abs(moduleSize - (stateCount[0] << INTEGER_MATH_SHIFT)) < maxVariance && + Math.abs(moduleSize - (stateCount[1] << INTEGER_MATH_SHIFT)) < maxVariance && + Math.abs(3 * moduleSize - (stateCount[2] << INTEGER_MATH_SHIFT)) < 3 * maxVariance && + Math.abs(moduleSize - (stateCount[3] << INTEGER_MATH_SHIFT)) < maxVariance && + Math.abs(moduleSize - (stateCount[4] << INTEGER_MATH_SHIFT)) < maxVariance; + } + + private int[] getCrossCheckStateCount() { + crossCheckStateCount[0] = 0; + crossCheckStateCount[1] = 0; + crossCheckStateCount[2] = 0; + crossCheckStateCount[3] = 0; + crossCheckStateCount[4] = 0; + return crossCheckStateCount; + } + + /** + *After a horizontal scan finds a potential finder pattern, this method + * "cross-checks" by scanning down vertically through the center of the possible + * finder pattern to see if the same proportion is detected.
+ * + * @param startI row where a finder pattern was detected + * @param centerJ center of the section that appears to cross a finder pattern + * @param maxCount maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @return vertical center of finder pattern, or {@link Float#NaN} if not found + */ + private float crossCheckVertical(int startI, int centerJ, int maxCount, + int originalStateCountTotal) { + BitMatrix image = this.image; + + int maxI = image.getHeight(); + int[] stateCount = getCrossCheckStateCount(); + + // Start counting up from center + int i = startI; + while (i >= 0 && image.get(centerJ, i)) { + stateCount[2]++; + i--; + } + if (i < 0) { + return Float.NaN; + } + while (i >= 0 && !image.get(centerJ, i) && stateCount[1] <= maxCount) { + stateCount[1]++; + i--; + } + // If already too many modules in this state or ran off the edge: + if (i < 0 || stateCount[1] > maxCount) { + return Float.NaN; + } + while (i >= 0 && image.get(centerJ, i) && stateCount[0] <= maxCount) { + stateCount[0]++; + i--; + } + if (stateCount[0] > maxCount) { + return Float.NaN; + } + + // Now also count down from center + i = startI + 1; + while (i < maxI && image.get(centerJ, i)) { + stateCount[2]++; + i++; + } + if (i == maxI) { + return Float.NaN; + } + while (i < maxI && !image.get(centerJ, i) && stateCount[3] < maxCount) { + stateCount[3]++; + i++; + } + if (i == maxI || stateCount[3] >= maxCount) { + return Float.NaN; + } + while (i < maxI && image.get(centerJ, i) && stateCount[4] < maxCount) { + stateCount[4]++; + i++; + } + if (stateCount[4] >= maxCount) { + return Float.NaN; + } + + // If we found a finder-pattern-like section, but its size is more than 40% different than + // the original, assume it's a false positive + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + + stateCount[4]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) { + return Float.NaN; + } + + return foundPatternCross(stateCount) ? centerFromEnd(stateCount, i) : Float.NaN; + } + + /** + *Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical, + * except it reads horizontally instead of vertically. This is used to cross-cross + * check a vertical cross check and locate the real center of the alignment pattern.
+ */ + private float crossCheckHorizontal(int startJ, int centerI, int maxCount, + int originalStateCountTotal) { + BitMatrix image = this.image; + + int maxJ = image.getWidth(); + int[] stateCount = getCrossCheckStateCount(); + + int j = startJ; + while (j >= 0 && image.get(j, centerI)) { + stateCount[2]++; + j--; + } + if (j < 0) { + return Float.NaN; + } + while (j >= 0 && !image.get(j, centerI) && stateCount[1] <= maxCount) { + stateCount[1]++; + j--; + } + if (j < 0 || stateCount[1] > maxCount) { + return Float.NaN; + } + while (j >= 0 && image.get(j, centerI) && stateCount[0] <= maxCount) { + stateCount[0]++; + j--; + } + if (stateCount[0] > maxCount) { + return Float.NaN; + } + + j = startJ + 1; + while (j < maxJ && image.get(j, centerI)) { + stateCount[2]++; + j++; + } + if (j == maxJ) { + return Float.NaN; + } + while (j < maxJ && !image.get(j, centerI) && stateCount[3] < maxCount) { + stateCount[3]++; + j++; + } + if (j == maxJ || stateCount[3] >= maxCount) { + return Float.NaN; + } + while (j < maxJ && image.get(j, centerI) && stateCount[4] < maxCount) { + stateCount[4]++; + j++; + } + if (stateCount[4] >= maxCount) { + return Float.NaN; + } + + // If we found a finder-pattern-like section, but its size is significantly different than + // the original, assume it's a false positive + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + + stateCount[4]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= originalStateCountTotal) { + return Float.NaN; + } + + return foundPatternCross(stateCount) ? centerFromEnd(stateCount, j) : Float.NaN; + } + + /** + *This is called when a horizontal scan finds a possible alignment pattern. It will + * cross check with a vertical scan, and if successful, will, ah, cross-cross-check + * with another horizontal scan. This is needed primarily to locate the real horizontal + * center of the pattern in cases of extreme skew.
+ * + *If that succeeds the finder pattern location is added to a list that tracks + * the number of times each location has been nearly-matched as a finder pattern. + * Each additional find is more evidence that the location is in fact a finder + * pattern center + * + * @param stateCount reading state module counts from horizontal scan + * @param i row where finder pattern may be found + * @param j end of possible finder pattern in row + * @return true if a finder pattern candidate was found this time + */ + protected boolean handlePossibleCenter(int[] stateCount, int i, int j) { + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + + stateCount[4]; + float centerJ = centerFromEnd(stateCount, j); + float centerI = crossCheckVertical(i, (int) centerJ, stateCount[2], stateCountTotal); + if (!Float.isNaN(centerI)) { + // Re-cross check + centerJ = crossCheckHorizontal((int) centerJ, (int) centerI, stateCount[2], stateCountTotal); + if (!Float.isNaN(centerJ)) { + float estimatedModuleSize = (float) stateCountTotal / 7.0f; + boolean found = false; + int max = possibleCenters.size(); + for (int index = 0; index < max; index++) { + FinderPattern center = (FinderPattern) possibleCenters.elementAt(index); + // Look for about the same center and module size: + if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) { + center.incrementCount(); + found = true; + break; + } + } + if (!found) { + ResultPoint point = new FinderPattern(centerJ, centerI, estimatedModuleSize); + possibleCenters.addElement(point); + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(point); + } + } + return true; + } + } + return false; + } + + /** + * @return number of rows we could safely skip during scanning, based on the first + * two finder patterns that have been located. In some cases their position will + * allow us to infer that the third pattern must lie below a certain point farther + * down in the image. + */ + private int findRowSkip() { + int max = possibleCenters.size(); + if (max <= 1) { + return 0; + } + FinderPattern firstConfirmedCenter = null; + for (int i = 0; i < max; i++) { + FinderPattern center = (FinderPattern) possibleCenters.elementAt(i); + if (center.getCount() >= CENTER_QUORUM) { + if (firstConfirmedCenter == null) { + firstConfirmedCenter = center; + } else { + // We have two confirmed centers + // How far down can we skip before resuming looking for the next + // pattern? In the worst case, only the difference between the + // difference in the x / y coordinates of the two centers. + // This is the case where you find top left last. + hasSkipped = true; + return (int) (Math.abs(firstConfirmedCenter.getX() - center.getX()) - + Math.abs(firstConfirmedCenter.getY() - center.getY())) / 2; + } + } + } + return 0; + } + + /** + * @return true iff we have found at least 3 finder patterns that have been detected + * at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the + * candidates is "pretty similar" + */ + private boolean haveMultiplyConfirmedCenters() { + int confirmedCount = 0; + float totalModuleSize = 0.0f; + int max = possibleCenters.size(); + for (int i = 0; i < max; i++) { + FinderPattern pattern = (FinderPattern) possibleCenters.elementAt(i); + if (pattern.getCount() >= CENTER_QUORUM) { + confirmedCount++; + totalModuleSize += pattern.getEstimatedModuleSize(); + } + } + if (confirmedCount < 3) { + return false; + } + // OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive" + // and that we need to keep looking. We detect this by asking if the estimated module sizes + // vary too much. We arbitrarily say that when the total deviation from average exceeds + // 5% of the total module size estimates, it's too much. + float average = totalModuleSize / (float) max; + float totalDeviation = 0.0f; + for (int i = 0; i < max; i++) { + FinderPattern pattern = (FinderPattern) possibleCenters.elementAt(i); + totalDeviation += Math.abs(pattern.getEstimatedModuleSize() - average); + } + return totalDeviation <= 0.05f * totalModuleSize; + } + + /** + * @return the 3 best {@link FinderPattern}s from our list of candidates. The "best" are + * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module + * size differs from the average among those patterns the least + * @throws NotFoundException if 3 such finder patterns do not exist + */ + private FinderPattern[] selectBestPatterns() throws NotFoundException { + + int startSize = possibleCenters.size(); + if (startSize < 3) { + // Couldn't find enough finder patterns + throw NotFoundException.getNotFoundInstance(); + } + + // Filter outlier possibilities whose module size is too different + if (startSize > 3) { + // But we can only afford to do so if we have at least 4 possibilities to choose from + float totalModuleSize = 0.0f; + float square = 0.0f; + for (int i = 0; i < startSize; i++) { + float size = ((FinderPattern) possibleCenters.elementAt(i)).getEstimatedModuleSize(); + totalModuleSize += size; + square += size * size; + } + float average = totalModuleSize / (float) startSize; + float stdDev = (float) Math.sqrt(square / startSize - average * average); + + Collections.insertionSort(possibleCenters, new FurthestFromAverageComparator(average)); + + float limit = Math.max(0.2f * average, stdDev); + + for (int i = 0; i < possibleCenters.size() && possibleCenters.size() > 3; i++) { + FinderPattern pattern = (FinderPattern) possibleCenters.elementAt(i); + if (Math.abs(pattern.getEstimatedModuleSize() - average) > limit) { + possibleCenters.removeElementAt(i); + i--; + } + } + } + + if (possibleCenters.size() > 3) { + // Throw away all but those first size candidate points we found. + + float totalModuleSize = 0.0f; + for (int i = 0; i < possibleCenters.size(); i++) { + totalModuleSize += ((FinderPattern) possibleCenters.elementAt(i)).getEstimatedModuleSize(); + } + + float average = totalModuleSize / (float) possibleCenters.size(); + + Collections.insertionSort(possibleCenters, new CenterComparator(average)); + + possibleCenters.setSize(3); + } + + return new FinderPattern[]{ + (FinderPattern) possibleCenters.elementAt(0), + (FinderPattern) possibleCenters.elementAt(1), + (FinderPattern) possibleCenters.elementAt(2) + }; + } + + /** + *
Orders by furthest from average
+ */ + private static class FurthestFromAverageComparator implements Comparator { + private final float average; + public FurthestFromAverageComparator(float f) { + average = f; + } + public int compare(Object center1, Object center2) { + float dA = Math.abs(((FinderPattern) center2).getEstimatedModuleSize() - average); + float dB = Math.abs(((FinderPattern) center1).getEstimatedModuleSize() - average); + return dA < dB ? -1 : (dA == dB ? 0 : 1); + } + } + + /** + *Orders by {@link FinderPattern#getCount()}, descending.
+ */ + private static class CenterComparator implements Comparator { + private final float average; + public CenterComparator(float f) { + average = f; + } + public int compare(Object center1, Object center2) { + if (((FinderPattern) center2).getCount() != ((FinderPattern) center1).getCount()) { + return ((FinderPattern) center2).getCount() - ((FinderPattern) center1).getCount(); + } else { + float dA = Math.abs(((FinderPattern) center2).getEstimatedModuleSize() - average); + float dB = Math.abs(((FinderPattern) center1).getEstimatedModuleSize() - average); + return dA < dB ? 1 : (dA == dB ? 0 : -1); + } + } + } + +} diff --git a/src/com/google/zxing/qrcode/detector/FinderPatternInfo.java b/src/com/google/zxing/qrcode/detector/FinderPatternInfo.java new file mode 100644 index 0000000..3c34010 --- /dev/null +++ b/src/com/google/zxing/qrcode/detector/FinderPatternInfo.java @@ -0,0 +1,49 @@ +/* + * Copyright 2007 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.qrcode.detector; + +/** + *Encapsulates information about finder patterns in an image, including the location of + * the three finder patterns, and their estimated module size.
+ * + * @author Sean Owen + */ +public final class FinderPatternInfo { + + private final FinderPattern bottomLeft; + private final FinderPattern topLeft; + private final FinderPattern topRight; + + public FinderPatternInfo(FinderPattern[] patternCenters) { + this.bottomLeft = patternCenters[0]; + this.topLeft = patternCenters[1]; + this.topRight = patternCenters[2]; + } + + public FinderPattern getBottomLeft() { + return bottomLeft; + } + + public FinderPattern getTopLeft() { + return topLeft; + } + + public FinderPattern getTopRight() { + return topRight; + } + +} diff --git a/src/com/google/zxing/qrcode/encoder/BlockPair.java b/src/com/google/zxing/qrcode/encoder/BlockPair.java new file mode 100644 index 0000000..5714d9c --- /dev/null +++ b/src/com/google/zxing/qrcode/encoder/BlockPair.java @@ -0,0 +1,37 @@ +/* + * 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.qrcode.encoder; + +final class BlockPair { + + private final byte[] dataBytes; + private final byte[] errorCorrectionBytes; + + BlockPair(byte[] data, byte[] errorCorrection) { + dataBytes = data; + errorCorrectionBytes = errorCorrection; + } + + public byte[] getDataBytes() { + return dataBytes; + } + + public byte[] getErrorCorrectionBytes() { + return errorCorrectionBytes; + } + +} diff --git a/src/com/google/zxing/qrcode/encoder/ByteMatrix.java b/src/com/google/zxing/qrcode/encoder/ByteMatrix.java new file mode 100644 index 0000000..eb248a2 --- /dev/null +++ b/src/com/google/zxing/qrcode/encoder/ByteMatrix.java @@ -0,0 +1,97 @@ +/* + * 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.qrcode.encoder; + +/** + * A class which wraps a 2D array of bytes. The default usage is signed. If you want to use it as a + * unsigned container, it's up to you to do byteValue & 0xff at each location. + * + * JAVAPORT: The original code was a 2D array of ints, but since it only ever gets assigned + * -1, 0, and 1, I'm going to use less memory and go with bytes. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ByteMatrix { + + private final byte[][] bytes; + private final int width; + private final int height; + + public ByteMatrix(int width, int height) { + bytes = new byte[height][width]; + this.width = width; + this.height = height; + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public byte get(int x, int y) { + return bytes[y][x]; + } + + public byte[][] getArray() { + return bytes; + } + + public void set(int x, int y, byte value) { + bytes[y][x] = value; + } + + public void set(int x, int y, int value) { + bytes[y][x] = (byte) value; + } + + public void set(int x, int y, boolean value) { + bytes[y][x] = (byte) (value ? 1 : 0); + } + + public void clear(byte value) { + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + bytes[y][x] = value; + } + } + } + + public String toString() { + StringBuffer result = new StringBuffer(2 * width * height + 2); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + switch (bytes[y][x]) { + case 0: + result.append(" 0"); + break; + case 1: + result.append(" 1"); + break; + default: + result.append(" "); + break; + } + } + result.append('\n'); + } + return result.toString(); + } + +} diff --git a/src/com/google/zxing/qrcode/encoder/Encoder.java b/src/com/google/zxing/qrcode/encoder/Encoder.java new file mode 100644 index 0000000..e93eb0b --- /dev/null +++ b/src/com/google/zxing/qrcode/encoder/Encoder.java @@ -0,0 +1,557 @@ +/* + * 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.qrcode.encoder; + +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitArray; +import com.google.zxing.common.CharacterSetECI; +import com.google.zxing.common.ECI; +import com.google.zxing.common.reedsolomon.GF256; +import com.google.zxing.common.reedsolomon.ReedSolomonEncoder; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.decoder.Mode; +import com.google.zxing.qrcode.decoder.Version; + +import java.io.UnsupportedEncodingException; +import java.util.Hashtable; +import java.util.Vector; + +/** + * @author satorux@google.com (Satoru Takabayashi) - creator + * @author dswitkin@google.com (Daniel Switkin) - ported from C++ + */ +public final class Encoder { + + // The original table is defined in the table 5 of JISX0510:2004 (p.19). + private static final int[] ALPHANUMERIC_TABLE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0x5f + }; + + static final String DEFAULT_BYTE_MODE_ENCODING = "UTF-8"; + + private Encoder() { + } + + // The mask penalty calculation is complicated. See Table 21 of JISX0510:2004 (p.45) for details. + // Basically it applies four rules and summate all penalties. + private static int calculateMaskPenalty(ByteMatrix matrix) { + int penalty = 0; + penalty += MaskUtil.applyMaskPenaltyRule1(matrix); + penalty += MaskUtil.applyMaskPenaltyRule2(matrix); + penalty += MaskUtil.applyMaskPenaltyRule3(matrix); + penalty += MaskUtil.applyMaskPenaltyRule4(matrix); + return penalty; + } + + /** + * Encode "bytes" with the error correction level "ecLevel". The encoding mode will be chosen + * internally by chooseMode(). On success, store the result in "qrCode". + * + * We recommend you to use QRCode.EC_LEVEL_L (the lowest level) for + * "getECLevel" since our primary use is to show QR code on desktop screens. We don't need very + * strong error correction for this purpose. + * + * Note that there is no way to encode bytes in MODE_KANJI. We might want to add EncodeWithMode() + * with which clients can specify the encoding mode. For now, we don't need the functionality. + */ + public static void encode(String content, ErrorCorrectionLevel ecLevel, QRCode qrCode) + throws WriterException { + encode(content, ecLevel, null, qrCode); + } + + public static void encode(String content, ErrorCorrectionLevel ecLevel, Hashtable hints, + QRCode qrCode) throws WriterException { + + String encoding = hints == null ? null : (String) hints.get(EncodeHintType.CHARACTER_SET); + if (encoding == null) { + encoding = DEFAULT_BYTE_MODE_ENCODING; + } + + // Step 1: Choose the mode (encoding). + Mode mode = chooseMode(content, encoding); + + // Step 2: Append "bytes" into "dataBits" in appropriate encoding. + BitArray dataBits = new BitArray(); + appendBytes(content, mode, dataBits, encoding); + // Step 3: Initialize QR code that can contain "dataBits". + int numInputBytes = dataBits.getSizeInBytes(); + initQRCode(numInputBytes, ecLevel, mode, qrCode); + + // Step 4: Build another bit vector that contains header and data. + BitArray headerAndDataBits = new BitArray(); + + // Step 4.5: Append ECI message if applicable + if (mode == Mode.BYTE && !DEFAULT_BYTE_MODE_ENCODING.equals(encoding)) { + CharacterSetECI eci = CharacterSetECI.getCharacterSetECIByName(encoding); + if (eci != null) { + appendECI(eci, headerAndDataBits); + } + } + + appendModeInfo(mode, headerAndDataBits); + + int numLetters = mode.equals(Mode.BYTE) ? dataBits.getSizeInBytes() : content.length(); + appendLengthInfo(numLetters, qrCode.getVersion(), mode, headerAndDataBits); + headerAndDataBits.appendBitArray(dataBits); + + // Step 5: Terminate the bits properly. + terminateBits(qrCode.getNumDataBytes(), headerAndDataBits); + + // Step 6: Interleave data bits with error correction code. + BitArray finalBits = new BitArray(); + interleaveWithECBytes(headerAndDataBits, qrCode.getNumTotalBytes(), qrCode.getNumDataBytes(), + qrCode.getNumRSBlocks(), finalBits); + + // Step 7: Choose the mask pattern and set to "qrCode". + ByteMatrix matrix = new ByteMatrix(qrCode.getMatrixWidth(), qrCode.getMatrixWidth()); + qrCode.setMaskPattern(chooseMaskPattern(finalBits, qrCode.getECLevel(), qrCode.getVersion(), + matrix)); + + // Step 8. Build the matrix and set it to "qrCode". + MatrixUtil.buildMatrix(finalBits, qrCode.getECLevel(), qrCode.getVersion(), + qrCode.getMaskPattern(), matrix); + qrCode.setMatrix(matrix); + // Step 9. Make sure we have a valid QR Code. + if (!qrCode.isValid()) { + throw new WriterException("Invalid QR code: " + qrCode.toString()); + } + } + + /** + * @return the code point of the table used in alphanumeric mode or + * -1 if there is no corresponding code in the table. + */ + static int getAlphanumericCode(int code) { + if (code < ALPHANUMERIC_TABLE.length) { + return ALPHANUMERIC_TABLE[code]; + } + return -1; + } + + public static Mode chooseMode(String content) { + return chooseMode(content, null); + } + + /** + * Choose the best mode by examining the content. Note that 'encoding' is used as a hint; + * if it is Shift_JIS, and the input is only double-byte Kanji, then we return {@link Mode#KANJI}. + */ + public static Mode chooseMode(String content, String encoding) { + if ("Shift_JIS".equals(encoding)) { + // Choose Kanji mode if all input are double-byte characters + return isOnlyDoubleByteKanji(content) ? Mode.KANJI : Mode.BYTE; + } + boolean hasNumeric = false; + boolean hasAlphanumeric = false; + for (int i = 0; i < content.length(); ++i) { + char c = content.charAt(i); + if (c >= '0' && c <= '9') { + hasNumeric = true; + } else if (getAlphanumericCode(c) != -1) { + hasAlphanumeric = true; + } else { + return Mode.BYTE; + } + } + if (hasAlphanumeric) { + return Mode.ALPHANUMERIC; + } else if (hasNumeric) { + return Mode.NUMERIC; + } + return Mode.BYTE; + } + + private static boolean isOnlyDoubleByteKanji(String content) { + byte[] bytes; + try { + bytes = content.getBytes("Shift_JIS"); + } catch (UnsupportedEncodingException uee) { + return false; + } + int length = bytes.length; + if (length % 2 != 0) { + return false; + } + for (int i = 0; i < length; i += 2) { + int byte1 = bytes[i] & 0xFF; + if ((byte1 < 0x81 || byte1 > 0x9F) && (byte1 < 0xE0 || byte1 > 0xEB)) { + return false; + } + } + return true; + } + + private static int chooseMaskPattern(BitArray bits, ErrorCorrectionLevel ecLevel, int version, + ByteMatrix matrix) throws WriterException { + + int minPenalty = Integer.MAX_VALUE; // Lower penalty is better. + int bestMaskPattern = -1; + // We try all mask patterns to choose the best one. + for (int maskPattern = 0; maskPattern < QRCode.NUM_MASK_PATTERNS; maskPattern++) { + MatrixUtil.buildMatrix(bits, ecLevel, version, maskPattern, matrix); + int penalty = calculateMaskPenalty(matrix); + if (penalty < minPenalty) { + minPenalty = penalty; + bestMaskPattern = maskPattern; + } + } + return bestMaskPattern; + } + + /** + * Initialize "qrCode" according to "numInputBytes", "ecLevel", and "mode". On success, + * modify "qrCode". + */ + private static void initQRCode(int numInputBytes, ErrorCorrectionLevel ecLevel, Mode mode, + QRCode qrCode) throws WriterException { + qrCode.setECLevel(ecLevel); + qrCode.setMode(mode); + + // In the following comments, we use numbers of Version 7-H. + for (int versionNum = 1; versionNum <= 40; versionNum++) { + Version version = Version.getVersionForNumber(versionNum); + // numBytes = 196 + int numBytes = version.getTotalCodewords(); + // getNumECBytes = 130 + Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); + int numEcBytes = ecBlocks.getTotalECCodewords(); + // getNumRSBlocks = 5 + int numRSBlocks = ecBlocks.getNumBlocks(); + // getNumDataBytes = 196 - 130 = 66 + int numDataBytes = numBytes - numEcBytes; + // We want to choose the smallest version which can contain data of "numInputBytes" + some + // extra bits for the header (mode info and length info). The header can be three bytes + // (precisely 4 + 16 bits) at most. Hence we do +3 here. + if (numDataBytes >= numInputBytes + 3) { + // Yay, we found the proper rs block info! + qrCode.setVersion(versionNum); + qrCode.setNumTotalBytes(numBytes); + qrCode.setNumDataBytes(numDataBytes); + qrCode.setNumRSBlocks(numRSBlocks); + // getNumECBytes = 196 - 66 = 130 + qrCode.setNumECBytes(numEcBytes); + // matrix width = 21 + 6 * 4 = 45 + qrCode.setMatrixWidth(version.getDimensionForVersion()); + return; + } + } + throw new WriterException("Cannot find proper rs block info (input data too big?)"); + } + + /** + * Terminate bits as described in 8.4.8 and 8.4.9 of JISX0510:2004 (p.24). + */ + static void terminateBits(int numDataBytes, BitArray bits) throws WriterException { + int capacity = numDataBytes << 3; + if (bits.getSize() > capacity) { + throw new WriterException("data bits cannot fit in the QR Code" + bits.getSize() + " > " + + capacity); + } + for (int i = 0; i < 4 && bits.getSize() < capacity; ++i) { + bits.appendBit(false); + } + // Append termination bits. See 8.4.8 of JISX0510:2004 (p.24) for details. + // If the last byte isn't 8-bit aligned, we'll add padding bits. + int numBitsInLastByte = bits.getSize() & 0x07; + if (numBitsInLastByte > 0) { + for (int i = numBitsInLastByte; i < 8; i++) { + bits.appendBit(false); + } + } + // If we have more space, we'll fill the space with padding patterns defined in 8.4.9 (p.24). + int numPaddingBytes = numDataBytes - bits.getSizeInBytes(); + for (int i = 0; i < numPaddingBytes; ++i) { + bits.appendBits(((i & 0x01) == 0) ? 0xEC : 0x11, 8); + } + if (bits.getSize() != capacity) { + throw new WriterException("Bits size does not equal capacity"); + } + } + + /** + * Get number of data bytes and number of error correction bytes for block id "blockID". Store + * the result in "numDataBytesInBlock", and "numECBytesInBlock". See table 12 in 8.5.1 of + * JISX0510:2004 (p.30) + */ + static void getNumDataBytesAndNumECBytesForBlockID(int numTotalBytes, int numDataBytes, + int numRSBlocks, int blockID, int[] numDataBytesInBlock, + int[] numECBytesInBlock) throws WriterException { + if (blockID >= numRSBlocks) { + throw new WriterException("Block ID too large"); + } + // numRsBlocksInGroup2 = 196 % 5 = 1 + int numRsBlocksInGroup2 = numTotalBytes % numRSBlocks; + // numRsBlocksInGroup1 = 5 - 1 = 4 + int numRsBlocksInGroup1 = numRSBlocks - numRsBlocksInGroup2; + // numTotalBytesInGroup1 = 196 / 5 = 39 + int numTotalBytesInGroup1 = numTotalBytes / numRSBlocks; + // numTotalBytesInGroup2 = 39 + 1 = 40 + int numTotalBytesInGroup2 = numTotalBytesInGroup1 + 1; + // numDataBytesInGroup1 = 66 / 5 = 13 + int numDataBytesInGroup1 = numDataBytes / numRSBlocks; + // numDataBytesInGroup2 = 13 + 1 = 14 + int numDataBytesInGroup2 = numDataBytesInGroup1 + 1; + // numEcBytesInGroup1 = 39 - 13 = 26 + int numEcBytesInGroup1 = numTotalBytesInGroup1 - numDataBytesInGroup1; + // numEcBytesInGroup2 = 40 - 14 = 26 + int numEcBytesInGroup2 = numTotalBytesInGroup2 - numDataBytesInGroup2; + // Sanity checks. + // 26 = 26 + if (numEcBytesInGroup1 != numEcBytesInGroup2) { + throw new WriterException("EC bytes mismatch"); + } + // 5 = 4 + 1. + if (numRSBlocks != numRsBlocksInGroup1 + numRsBlocksInGroup2) { + throw new WriterException("RS blocks mismatch"); + } + // 196 = (13 + 26) * 4 + (14 + 26) * 1 + if (numTotalBytes != + ((numDataBytesInGroup1 + numEcBytesInGroup1) * + numRsBlocksInGroup1) + + ((numDataBytesInGroup2 + numEcBytesInGroup2) * + numRsBlocksInGroup2)) { + throw new WriterException("Total bytes mismatch"); + } + + if (blockID < numRsBlocksInGroup1) { + numDataBytesInBlock[0] = numDataBytesInGroup1; + numECBytesInBlock[0] = numEcBytesInGroup1; + } else { + numDataBytesInBlock[0] = numDataBytesInGroup2; + numECBytesInBlock[0] = numEcBytesInGroup2; + } + } + + /** + * Interleave "bits" with corresponding error correction bytes. On success, store the result in + * "result". The interleave rule is complicated. See 8.6 of JISX0510:2004 (p.37) for details. + */ + static void interleaveWithECBytes(BitArray bits, int numTotalBytes, + int numDataBytes, int numRSBlocks, BitArray result) throws WriterException { + + // "bits" must have "getNumDataBytes" bytes of data. + if (bits.getSizeInBytes() != numDataBytes) { + throw new WriterException("Number of bits and data bytes does not match"); + } + + // Step 1. Divide data bytes into blocks and generate error correction bytes for them. We'll + // store the divided data bytes blocks and error correction bytes blocks into "blocks". + int dataBytesOffset = 0; + int maxNumDataBytes = 0; + int maxNumEcBytes = 0; + + // Since, we know the number of reedsolmon blocks, we can initialize the vector with the number. + Vector blocks = new Vector(numRSBlocks); + + for (int i = 0; i < numRSBlocks; ++i) { + int[] numDataBytesInBlock = new int[1]; + int[] numEcBytesInBlock = new int[1]; + getNumDataBytesAndNumECBytesForBlockID( + numTotalBytes, numDataBytes, numRSBlocks, i, + numDataBytesInBlock, numEcBytesInBlock); + + int size = numDataBytesInBlock[0]; + byte[] dataBytes = new byte[size]; + bits.toBytes(8*dataBytesOffset, dataBytes, 0, size); + byte[] ecBytes = generateECBytes(dataBytes, numEcBytesInBlock[0]); + blocks.addElement(new BlockPair(dataBytes, ecBytes)); + + maxNumDataBytes = Math.max(maxNumDataBytes, size); + maxNumEcBytes = Math.max(maxNumEcBytes, ecBytes.length); + dataBytesOffset += numDataBytesInBlock[0]; + } + if (numDataBytes != dataBytesOffset) { + throw new WriterException("Data bytes does not match offset"); + } + + // First, place data blocks. + for (int i = 0; i < maxNumDataBytes; ++i) { + for (int j = 0; j < blocks.size(); ++j) { + byte[] dataBytes = ((BlockPair) blocks.elementAt(j)).getDataBytes(); + if (i < dataBytes.length) { + result.appendBits(dataBytes[i], 8); + } + } + } + // Then, place error correction blocks. + for (int i = 0; i < maxNumEcBytes; ++i) { + for (int j = 0; j < blocks.size(); ++j) { + byte[] ecBytes = ((BlockPair) blocks.elementAt(j)).getErrorCorrectionBytes(); + if (i < ecBytes.length) { + result.appendBits(ecBytes[i], 8); + } + } + } + if (numTotalBytes != result.getSizeInBytes()) { // Should be same. + throw new WriterException("Interleaving error: " + numTotalBytes + " and " + + result.getSizeInBytes() + " differ."); + } + } + + static byte[] generateECBytes(byte[] dataBytes, int numEcBytesInBlock) { + int numDataBytes = dataBytes.length; + int[] toEncode = new int[numDataBytes + numEcBytesInBlock]; + for (int i = 0; i < numDataBytes; i++) { + toEncode[i] = dataBytes[i] & 0xFF; + } + new ReedSolomonEncoder(GF256.QR_CODE_FIELD).encode(toEncode, numEcBytesInBlock); + + byte[] ecBytes = new byte[numEcBytesInBlock]; + for (int i = 0; i < numEcBytesInBlock; i++) { + ecBytes[i] = (byte) toEncode[numDataBytes + i]; + } + return ecBytes; + } + + /** + * Append mode info. On success, store the result in "bits". + */ + static void appendModeInfo(Mode mode, BitArray bits) { + bits.appendBits(mode.getBits(), 4); + } + + + /** + * Append length info. On success, store the result in "bits". + */ + static void appendLengthInfo(int numLetters, int version, Mode mode, BitArray bits) + throws WriterException { + int numBits = mode.getCharacterCountBits(Version.getVersionForNumber(version)); + if (numLetters > ((1 << numBits) - 1)) { + throw new WriterException(numLetters + "is bigger than" + ((1 << numBits) - 1)); + } + bits.appendBits(numLetters, numBits); + } + + /** + * Append "bytes" in "mode" mode (encoding) into "bits". On success, store the result in "bits". + */ + static void appendBytes(String content, Mode mode, BitArray bits, String encoding) + throws WriterException { + if (mode.equals(Mode.NUMERIC)) { + appendNumericBytes(content, bits); + } else if (mode.equals(Mode.ALPHANUMERIC)) { + appendAlphanumericBytes(content, bits); + } else if (mode.equals(Mode.BYTE)) { + append8BitBytes(content, bits, encoding); + } else if (mode.equals(Mode.KANJI)) { + appendKanjiBytes(content, bits); + } else { + throw new WriterException("Invalid mode: " + mode); + } + } + + static void appendNumericBytes(String content, BitArray bits) { + int length = content.length(); + int i = 0; + while (i < length) { + int num1 = content.charAt(i) - '0'; + if (i + 2 < length) { + // Encode three numeric letters in ten bits. + int num2 = content.charAt(i + 1) - '0'; + int num3 = content.charAt(i + 2) - '0'; + bits.appendBits(num1 * 100 + num2 * 10 + num3, 10); + i += 3; + } else if (i + 1 < length) { + // Encode two numeric letters in seven bits. + int num2 = content.charAt(i + 1) - '0'; + bits.appendBits(num1 * 10 + num2, 7); + i += 2; + } else { + // Encode one numeric letter in four bits. + bits.appendBits(num1, 4); + i++; + } + } + } + + static void appendAlphanumericBytes(String content, BitArray bits) throws WriterException { + int length = content.length(); + int i = 0; + while (i < length) { + int code1 = getAlphanumericCode(content.charAt(i)); + if (code1 == -1) { + throw new WriterException(); + } + if (i + 1 < length) { + int code2 = getAlphanumericCode(content.charAt(i + 1)); + if (code2 == -1) { + throw new WriterException(); + } + // Encode two alphanumeric letters in 11 bits. + bits.appendBits(code1 * 45 + code2, 11); + i += 2; + } else { + // Encode one alphanumeric letter in six bits. + bits.appendBits(code1, 6); + i++; + } + } + } + + static void append8BitBytes(String content, BitArray bits, String encoding) + throws WriterException { + byte[] bytes; + try { + bytes = content.getBytes(encoding); + } catch (UnsupportedEncodingException uee) { + throw new WriterException(uee.toString()); + } + for (int i = 0; i < bytes.length; ++i) { + bits.appendBits(bytes[i], 8); + } + } + + static void appendKanjiBytes(String content, BitArray bits) throws WriterException { + byte[] bytes; + try { + bytes = content.getBytes("Shift_JIS"); + } catch (UnsupportedEncodingException uee) { + throw new WriterException(uee.toString()); + } + int length = bytes.length; + for (int i = 0; i < length; i += 2) { + int byte1 = bytes[i] & 0xFF; + int byte2 = bytes[i + 1] & 0xFF; + int code = (byte1 << 8) | byte2; + int subtracted = -1; + if (code >= 0x8140 && code <= 0x9ffc) { + subtracted = code - 0x8140; + } else if (code >= 0xe040 && code <= 0xebbf) { + subtracted = code - 0xc140; + } + if (subtracted == -1) { + throw new WriterException("Invalid byte sequence"); + } + int encoded = ((subtracted >> 8) * 0xc0) + (subtracted & 0xff); + bits.appendBits(encoded, 13); + } + } + + private static void appendECI(ECI eci, BitArray bits) { + bits.appendBits(Mode.ECI.getBits(), 4); + // This is correct for values up to 127, which is all we need now. + bits.appendBits(eci.getValue(), 8); + } + +} diff --git a/src/com/google/zxing/qrcode/encoder/MaskUtil.java b/src/com/google/zxing/qrcode/encoder/MaskUtil.java new file mode 100644 index 0000000..c7f3c48 --- /dev/null +++ b/src/com/google/zxing/qrcode/encoder/MaskUtil.java @@ -0,0 +1,217 @@ +/* + * 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.qrcode.encoder; + +/** + * @author satorux@google.com (Satoru Takabayashi) - creator + * @author dswitkin@google.com (Daniel Switkin) - ported from C++ + */ +public final class MaskUtil { + + private MaskUtil() { + // do nothing + } + + // Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and + // give penalty to them. Example: 00000 or 11111. + public static int applyMaskPenaltyRule1(ByteMatrix matrix) { + return applyMaskPenaltyRule1Internal(matrix, true) + applyMaskPenaltyRule1Internal(matrix, false); + } + + // Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give + // penalty to them. + public static int applyMaskPenaltyRule2(ByteMatrix matrix) { + int penalty = 0; + byte[][] array = matrix.getArray(); + int width = matrix.getWidth(); + int height = matrix.getHeight(); + for (int y = 0; y < height - 1; ++y) { + for (int x = 0; x < width - 1; ++x) { + int value = array[y][x]; + if (value == array[y][x + 1] && value == array[y + 1][x] && value == array[y + 1][x + 1]) { + penalty += 3; + } + } + } + return penalty; + } + + // Apply mask penalty rule 3 and return the penalty. Find consecutive cells of 00001011101 or + // 10111010000, and give penalty to them. If we find patterns like 000010111010000, we give + // penalties twice (i.e. 40 * 2). + public static int applyMaskPenaltyRule3(ByteMatrix matrix) { + int penalty = 0; + byte[][] array = matrix.getArray(); + int width = matrix.getWidth(); + int height = matrix.getHeight(); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + // Tried to simplify following conditions but failed. + if (x + 6 < width && + array[y][x] == 1 && + array[y][x + 1] == 0 && + array[y][x + 2] == 1 && + array[y][x + 3] == 1 && + array[y][x + 4] == 1 && + array[y][x + 5] == 0 && + array[y][x + 6] == 1 && + ((x + 10 < width && + array[y][x + 7] == 0 && + array[y][x + 8] == 0 && + array[y][x + 9] == 0 && + array[y][x + 10] == 0) || + (x - 4 >= 0 && + array[y][x - 1] == 0 && + array[y][x - 2] == 0 && + array[y][x - 3] == 0 && + array[y][x - 4] == 0))) { + penalty += 40; + } + if (y + 6 < height && + array[y][x] == 1 && + array[y + 1][x] == 0 && + array[y + 2][x] == 1 && + array[y + 3][x] == 1 && + array[y + 4][x] == 1 && + array[y + 5][x] == 0 && + array[y + 6][x] == 1 && + ((y + 10 < height && + array[y + 7][x] == 0 && + array[y + 8][x] == 0 && + array[y + 9][x] == 0 && + array[y + 10][x] == 0) || + (y - 4 >= 0 && + array[y - 1][x] == 0 && + array[y - 2][x] == 0 && + array[y - 3][x] == 0 && + array[y - 4][x] == 0))) { + penalty += 40; + } + } + } + return penalty; + } + + // Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give + // penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance. Examples: + // - 0% => 100 + // - 40% => 20 + // - 45% => 10 + // - 50% => 0 + // - 55% => 10 + // - 55% => 20 + // - 100% => 100 + public static int applyMaskPenaltyRule4(ByteMatrix matrix) { + int numDarkCells = 0; + byte[][] array = matrix.getArray(); + int width = matrix.getWidth(); + int height = matrix.getHeight(); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + if (array[y][x] == 1) { + numDarkCells += 1; + } + } + } + int numTotalCells = matrix.getHeight() * matrix.getWidth(); + double darkRatio = (double) numDarkCells / numTotalCells; + return Math.abs((int) (darkRatio * 100 - 50)) / 5 * 10; + } + + // Return the mask bit for "getMaskPattern" at "x" and "y". See 8.8 of JISX0510:2004 for mask + // pattern conditions. + public static boolean getDataMaskBit(int maskPattern, int x, int y) { + if (!QRCode.isValidMaskPattern(maskPattern)) { + throw new IllegalArgumentException("Invalid mask pattern"); + } + int intermediate, temp; + switch (maskPattern) { + case 0: + intermediate = (y + x) & 0x1; + break; + case 1: + intermediate = y & 0x1; + break; + case 2: + intermediate = x % 3; + break; + case 3: + intermediate = (y + x) % 3; + break; + case 4: + intermediate = ((y >>> 1) + (x / 3)) & 0x1; + break; + case 5: + temp = y * x; + intermediate = (temp & 0x1) + (temp % 3); + break; + case 6: + temp = y * x; + intermediate = (((temp & 0x1) + (temp % 3)) & 0x1); + break; + case 7: + temp = y * x; + intermediate = (((temp % 3) + ((y + x) & 0x1)) & 0x1); + break; + default: + throw new IllegalArgumentException("Invalid mask pattern: " + maskPattern); + } + return intermediate == 0; + } + + // Helper function for applyMaskPenaltyRule1. We need this for doing this calculation in both + // vertical and horizontal orders respectively. + private static int applyMaskPenaltyRule1Internal(ByteMatrix matrix, boolean isHorizontal) { + int penalty = 0; + int numSameBitCells = 0; + int prevBit = -1; + // Horizontal mode: + // for (int i = 0; i < matrix.height(); ++i) { + // for (int j = 0; j < matrix.width(); ++j) { + // int bit = matrix.get(i, j); + // Vertical mode: + // for (int i = 0; i < matrix.width(); ++i) { + // for (int j = 0; j < matrix.height(); ++j) { + // int bit = matrix.get(j, i); + int iLimit = isHorizontal ? matrix.getHeight() : matrix.getWidth(); + int jLimit = isHorizontal ? matrix.getWidth() : matrix.getHeight(); + byte[][] array = matrix.getArray(); + for (int i = 0; i < iLimit; ++i) { + for (int j = 0; j < jLimit; ++j) { + int bit = isHorizontal ? array[i][j] : array[j][i]; + if (bit == prevBit) { + numSameBitCells += 1; + // Found five repetitive cells with the same color (bit). + // We'll give penalty of 3. + if (numSameBitCells == 5) { + penalty += 3; + } else if (numSameBitCells > 5) { + // After five repetitive cells, we'll add the penalty one + // by one. + penalty += 1; + } + } else { + numSameBitCells = 1; // Include the cell itself. + prevBit = bit; + } + } + numSameBitCells = 0; // Clear at each row/column. + } + return penalty; + } + +} diff --git a/src/com/google/zxing/qrcode/encoder/MatrixUtil.java b/src/com/google/zxing/qrcode/encoder/MatrixUtil.java new file mode 100644 index 0000000..76bdb18 --- /dev/null +++ b/src/com/google/zxing/qrcode/encoder/MatrixUtil.java @@ -0,0 +1,524 @@ +/* + * 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.qrcode.encoder; + +import com.google.zxing.WriterException; +import com.google.zxing.common.BitArray; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +/** + * @author satorux@google.com (Satoru Takabayashi) - creator + * @author dswitkin@google.com (Daniel Switkin) - ported from C++ + */ +public final class MatrixUtil { + + private MatrixUtil() { + // do nothing + } + + private static final int[][] POSITION_DETECTION_PATTERN = { + {1, 1, 1, 1, 1, 1, 1}, + {1, 0, 0, 0, 0, 0, 1}, + {1, 0, 1, 1, 1, 0, 1}, + {1, 0, 1, 1, 1, 0, 1}, + {1, 0, 1, 1, 1, 0, 1}, + {1, 0, 0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1, 1, 1}, + }; + + private static final int[][] HORIZONTAL_SEPARATION_PATTERN = { + {0, 0, 0, 0, 0, 0, 0, 0}, + }; + + private static final int[][] VERTICAL_SEPARATION_PATTERN = { + {0}, {0}, {0}, {0}, {0}, {0}, {0}, + }; + + private static final int[][] POSITION_ADJUSTMENT_PATTERN = { + {1, 1, 1, 1, 1}, + {1, 0, 0, 0, 1}, + {1, 0, 1, 0, 1}, + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + }; + + // From Appendix E. Table 1, JIS0510X:2004 (p 71). The table was double-checked by komatsu. + private static final int[][] POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = { + {-1, -1, -1, -1, -1, -1, -1}, // Version 1 + { 6, 18, -1, -1, -1, -1, -1}, // Version 2 + { 6, 22, -1, -1, -1, -1, -1}, // Version 3 + { 6, 26, -1, -1, -1, -1, -1}, // Version 4 + { 6, 30, -1, -1, -1, -1, -1}, // Version 5 + { 6, 34, -1, -1, -1, -1, -1}, // Version 6 + { 6, 22, 38, -1, -1, -1, -1}, // Version 7 + { 6, 24, 42, -1, -1, -1, -1}, // Version 8 + { 6, 26, 46, -1, -1, -1, -1}, // Version 9 + { 6, 28, 50, -1, -1, -1, -1}, // Version 10 + { 6, 30, 54, -1, -1, -1, -1}, // Version 11 + { 6, 32, 58, -1, -1, -1, -1}, // Version 12 + { 6, 34, 62, -1, -1, -1, -1}, // Version 13 + { 6, 26, 46, 66, -1, -1, -1}, // Version 14 + { 6, 26, 48, 70, -1, -1, -1}, // Version 15 + { 6, 26, 50, 74, -1, -1, -1}, // Version 16 + { 6, 30, 54, 78, -1, -1, -1}, // Version 17 + { 6, 30, 56, 82, -1, -1, -1}, // Version 18 + { 6, 30, 58, 86, -1, -1, -1}, // Version 19 + { 6, 34, 62, 90, -1, -1, -1}, // Version 20 + { 6, 28, 50, 72, 94, -1, -1}, // Version 21 + { 6, 26, 50, 74, 98, -1, -1}, // Version 22 + { 6, 30, 54, 78, 102, -1, -1}, // Version 23 + { 6, 28, 54, 80, 106, -1, -1}, // Version 24 + { 6, 32, 58, 84, 110, -1, -1}, // Version 25 + { 6, 30, 58, 86, 114, -1, -1}, // Version 26 + { 6, 34, 62, 90, 118, -1, -1}, // Version 27 + { 6, 26, 50, 74, 98, 122, -1}, // Version 28 + { 6, 30, 54, 78, 102, 126, -1}, // Version 29 + { 6, 26, 52, 78, 104, 130, -1}, // Version 30 + { 6, 30, 56, 82, 108, 134, -1}, // Version 31 + { 6, 34, 60, 86, 112, 138, -1}, // Version 32 + { 6, 30, 58, 86, 114, 142, -1}, // Version 33 + { 6, 34, 62, 90, 118, 146, -1}, // Version 34 + { 6, 30, 54, 78, 102, 126, 150}, // Version 35 + { 6, 24, 50, 76, 102, 128, 154}, // Version 36 + { 6, 28, 54, 80, 106, 132, 158}, // Version 37 + { 6, 32, 58, 84, 110, 136, 162}, // Version 38 + { 6, 26, 54, 82, 110, 138, 166}, // Version 39 + { 6, 30, 58, 86, 114, 142, 170}, // Version 40 + }; + + // Type info cells at the left top corner. + private static final int[][] TYPE_INFO_COORDINATES = { + {8, 0}, + {8, 1}, + {8, 2}, + {8, 3}, + {8, 4}, + {8, 5}, + {8, 7}, + {8, 8}, + {7, 8}, + {5, 8}, + {4, 8}, + {3, 8}, + {2, 8}, + {1, 8}, + {0, 8}, + }; + + // From Appendix D in JISX0510:2004 (p. 67) + private static final int VERSION_INFO_POLY = 0x1f25; // 1 1111 0010 0101 + + // From Appendix C in JISX0510:2004 (p.65). + private static final int TYPE_INFO_POLY = 0x537; + private static final int TYPE_INFO_MASK_PATTERN = 0x5412; + + // Set all cells to -1. -1 means that the cell is empty (not set yet). + // + // JAVAPORT: We shouldn't need to do this at all. The code should be rewritten to begin encoding + // with the ByteMatrix initialized all to zero. + public static void clearMatrix(ByteMatrix matrix) { + matrix.clear((byte) -1); + } + + // Build 2D matrix of QR Code from "dataBits" with "ecLevel", "version" and "getMaskPattern". On + // success, store the result in "matrix" and return true. + public static void buildMatrix(BitArray dataBits, ErrorCorrectionLevel ecLevel, int version, + int maskPattern, ByteMatrix matrix) throws WriterException { + clearMatrix(matrix); + embedBasicPatterns(version, matrix); + // Type information appear with any version. + embedTypeInfo(ecLevel, maskPattern, matrix); + // Version info appear if version >= 7. + maybeEmbedVersionInfo(version, matrix); + // Data should be embedded at end. + embedDataBits(dataBits, maskPattern, matrix); + } + + // Embed basic patterns. On success, modify the matrix and return true. + // The basic patterns are: + // - Position detection patterns + // - Timing patterns + // - Dark dot at the left bottom corner + // - Position adjustment patterns, if need be + public static void embedBasicPatterns(int version, ByteMatrix matrix) throws WriterException { + // Let's get started with embedding big squares at corners. + embedPositionDetectionPatternsAndSeparators(matrix); + // Then, embed the dark dot at the left bottom corner. + embedDarkDotAtLeftBottomCorner(matrix); + + // Position adjustment patterns appear if version >= 2. + maybeEmbedPositionAdjustmentPatterns(version, matrix); + // Timing patterns should be embedded after position adj. patterns. + embedTimingPatterns(matrix); + } + + // Embed type information. On success, modify the matrix. + public static void embedTypeInfo(ErrorCorrectionLevel ecLevel, int maskPattern, ByteMatrix matrix) + throws WriterException { + BitArray typeInfoBits = new BitArray(); + makeTypeInfoBits(ecLevel, maskPattern, typeInfoBits); + + for (int i = 0; i < typeInfoBits.getSize(); ++i) { + // Place bits in LSB to MSB order. LSB (least significant bit) is the last value in + // "typeInfoBits". + boolean bit = typeInfoBits.get(typeInfoBits.getSize() - 1 - i); + + // Type info bits at the left top corner. See 8.9 of JISX0510:2004 (p.46). + int x1 = TYPE_INFO_COORDINATES[i][0]; + int y1 = TYPE_INFO_COORDINATES[i][1]; + matrix.set(x1, y1, bit); + + if (i < 8) { + // Right top corner. + int x2 = matrix.getWidth() - i - 1; + int y2 = 8; + matrix.set(x2, y2, bit); + } else { + // Left bottom corner. + int x2 = 8; + int y2 = matrix.getHeight() - 7 + (i - 8); + matrix.set(x2, y2, bit); + } + } + } + + // Embed version information if need be. On success, modify the matrix and return true. + // See 8.10 of JISX0510:2004 (p.47) for how to embed version information. + public static void maybeEmbedVersionInfo(int version, ByteMatrix matrix) throws WriterException { + if (version < 7) { // Version info is necessary if version >= 7. + return; // Don't need version info. + } + BitArray versionInfoBits = new BitArray(); + makeVersionInfoBits(version, versionInfoBits); + + int bitIndex = 6 * 3 - 1; // It will decrease from 17 to 0. + for (int i = 0; i < 6; ++i) { + for (int j = 0; j < 3; ++j) { + // Place bits in LSB (least significant bit) to MSB order. + boolean bit = versionInfoBits.get(bitIndex); + bitIndex--; + // Left bottom corner. + matrix.set(i, matrix.getHeight() - 11 + j, bit); + // Right bottom corner. + matrix.set(matrix.getHeight() - 11 + j, i, bit); + } + } + } + + // Embed "dataBits" using "getMaskPattern". On success, modify the matrix and return true. + // For debugging purposes, it skips masking process if "getMaskPattern" is -1. + // See 8.7 of JISX0510:2004 (p.38) for how to embed data bits. + public static void embedDataBits(BitArray dataBits, int maskPattern, ByteMatrix matrix) + throws WriterException { + int bitIndex = 0; + int direction = -1; + // Start from the right bottom cell. + int x = matrix.getWidth() - 1; + int y = matrix.getHeight() - 1; + while (x > 0) { + // Skip the vertical timing pattern. + if (x == 6) { + x -= 1; + } + while (y >= 0 && y < matrix.getHeight()) { + for (int i = 0; i < 2; ++i) { + int xx = x - i; + // Skip the cell if it's not empty. + if (!isEmpty(matrix.get(xx, y))) { + continue; + } + boolean bit; + if (bitIndex < dataBits.getSize()) { + bit = dataBits.get(bitIndex); + ++bitIndex; + } else { + // Padding bit. If there is no bit left, we'll fill the left cells with 0, as described + // in 8.4.9 of JISX0510:2004 (p. 24). + bit = false; + } + + // Skip masking if mask_pattern is -1. + if (maskPattern != -1) { + if (MaskUtil.getDataMaskBit(maskPattern, xx, y)) { + bit = !bit; + } + } + matrix.set(xx, y, bit); + } + y += direction; + } + direction = -direction; // Reverse the direction. + y += direction; + x -= 2; // Move to the left. + } + // All bits should be consumed. + if (bitIndex != dataBits.getSize()) { + throw new WriterException("Not all bits consumed: " + bitIndex + '/' + dataBits.getSize()); + } + } + + // Return the position of the most significant bit set (to one) in the "value". The most + // significant bit is position 32. If there is no bit set, return 0. Examples: + // - findMSBSet(0) => 0 + // - findMSBSet(1) => 1 + // - findMSBSet(255) => 8 + public static int findMSBSet(int value) { + int numDigits = 0; + while (value != 0) { + value >>>= 1; + ++numDigits; + } + return numDigits; + } + + // Calculate BCH (Bose-Chaudhuri-Hocquenghem) code for "value" using polynomial "poly". The BCH + // code is used for encoding type information and version information. + // Example: Calculation of version information of 7. + // f(x) is created from 7. + // - 7 = 000111 in 6 bits + // - f(x) = x^2 + x^1 + x^0 + // g(x) is given by the standard (p. 67) + // - g(x) = x^12 + x^11 + x^10 + x^9 + x^8 + x^5 + x^2 + 1 + // Multiply f(x) by x^(18 - 6) + // - f'(x) = f(x) * x^(18 - 6) + // - f'(x) = x^14 + x^13 + x^12 + // Calculate the remainder of f'(x) / g(x) + // x^2 + // __________________________________________________ + // g(x) )x^14 + x^13 + x^12 + // x^14 + x^13 + x^12 + x^11 + x^10 + x^7 + x^4 + x^2 + // -------------------------------------------------- + // x^11 + x^10 + x^7 + x^4 + x^2 + // + // The remainder is x^11 + x^10 + x^7 + x^4 + x^2 + // Encode it in binary: 110010010100 + // The return value is 0xc94 (1100 1001 0100) + // + // Since all coefficients in the polynomials are 1 or 0, we can do the calculation by bit + // operations. We don't care if cofficients are positive or negative. + public static int calculateBCHCode(int value, int poly) { + // If poly is "1 1111 0010 0101" (version info poly), msbSetInPoly is 13. We'll subtract 1 + // from 13 to make it 12. + int msbSetInPoly = findMSBSet(poly); + value <<= msbSetInPoly - 1; + // Do the division business using exclusive-or operations. + while (findMSBSet(value) >= msbSetInPoly) { + value ^= poly << (findMSBSet(value) - msbSetInPoly); + } + // Now the "value" is the remainder (i.e. the BCH code) + return value; + } + + // Make bit vector of type information. On success, store the result in "bits" and return true. + // Encode error correction level and mask pattern. See 8.9 of + // JISX0510:2004 (p.45) for details. + public static void makeTypeInfoBits(ErrorCorrectionLevel ecLevel, int maskPattern, BitArray bits) + throws WriterException { + if (!QRCode.isValidMaskPattern(maskPattern)) { + throw new WriterException("Invalid mask pattern"); + } + int typeInfo = (ecLevel.getBits() << 3) | maskPattern; + bits.appendBits(typeInfo, 5); + + int bchCode = calculateBCHCode(typeInfo, TYPE_INFO_POLY); + bits.appendBits(bchCode, 10); + + BitArray maskBits = new BitArray(); + maskBits.appendBits(TYPE_INFO_MASK_PATTERN, 15); + bits.xor(maskBits); + + if (bits.getSize() != 15) { // Just in case. + throw new WriterException("should not happen but we got: " + bits.getSize()); + } + } + + // Make bit vector of version information. On success, store the result in "bits" and return true. + // See 8.10 of JISX0510:2004 (p.45) for details. + public static void makeVersionInfoBits(int version, BitArray bits) throws WriterException { + bits.appendBits(version, 6); + int bchCode = calculateBCHCode(version, VERSION_INFO_POLY); + bits.appendBits(bchCode, 12); + + if (bits.getSize() != 18) { // Just in case. + throw new WriterException("should not happen but we got: " + bits.getSize()); + } + } + + // Check if "value" is empty. + private static boolean isEmpty(int value) { + return value == -1; + } + + // Check if "value" is valid. + private static boolean isValidValue(int value) { + return (value == -1 || // Empty. + value == 0 || // Light (white). + value == 1); // Dark (black). + } + + private static void embedTimingPatterns(ByteMatrix matrix) throws WriterException { + // -8 is for skipping position detection patterns (size 7), and two horizontal/vertical + // separation patterns (size 1). Thus, 8 = 7 + 1. + for (int i = 8; i < matrix.getWidth() - 8; ++i) { + int bit = (i + 1) % 2; + // Horizontal line. + if (!isValidValue(matrix.get(i, 6))) { + throw new WriterException(); + } + if (isEmpty(matrix.get(i, 6))) { + matrix.set(i, 6, bit); + } + // Vertical line. + if (!isValidValue(matrix.get(6, i))) { + throw new WriterException(); + } + if (isEmpty(matrix.get(6, i))) { + matrix.set(6, i, bit); + } + } + } + + // Embed the lonely dark dot at left bottom corner. JISX0510:2004 (p.46) + private static void embedDarkDotAtLeftBottomCorner(ByteMatrix matrix) throws WriterException { + if (matrix.get(8, matrix.getHeight() - 8) == 0) { + throw new WriterException(); + } + matrix.set(8, matrix.getHeight() - 8, 1); + } + + private static void embedHorizontalSeparationPattern(int xStart, int yStart, + ByteMatrix matrix) throws WriterException { + // We know the width and height. + if (HORIZONTAL_SEPARATION_PATTERN[0].length != 8 || HORIZONTAL_SEPARATION_PATTERN.length != 1) { + throw new WriterException("Bad horizontal separation pattern"); + } + for (int x = 0; x < 8; ++x) { + if (!isEmpty(matrix.get(xStart + x, yStart))) { + throw new WriterException(); + } + matrix.set(xStart + x, yStart, HORIZONTAL_SEPARATION_PATTERN[0][x]); + } + } + + private static void embedVerticalSeparationPattern(int xStart, int yStart, + ByteMatrix matrix) throws WriterException { + // We know the width and height. + if (VERTICAL_SEPARATION_PATTERN[0].length != 1 || VERTICAL_SEPARATION_PATTERN.length != 7) { + throw new WriterException("Bad vertical separation pattern"); + } + for (int y = 0; y < 7; ++y) { + if (!isEmpty(matrix.get(xStart, yStart + y))) { + throw new WriterException(); + } + matrix.set(xStart, yStart + y, VERTICAL_SEPARATION_PATTERN[y][0]); + } + } + + // Note that we cannot unify the function with embedPositionDetectionPattern() despite they are + // almost identical, since we cannot write a function that takes 2D arrays in different sizes in + // C/C++. We should live with the fact. + private static void embedPositionAdjustmentPattern(int xStart, int yStart, + ByteMatrix matrix) throws WriterException { + // We know the width and height. + if (POSITION_ADJUSTMENT_PATTERN[0].length != 5 || POSITION_ADJUSTMENT_PATTERN.length != 5) { + throw new WriterException("Bad position adjustment"); + } + for (int y = 0; y < 5; ++y) { + for (int x = 0; x < 5; ++x) { + if (!isEmpty(matrix.get(xStart + x, yStart + y))) { + throw new WriterException(); + } + matrix.set(xStart + x, yStart + y, POSITION_ADJUSTMENT_PATTERN[y][x]); + } + } + } + + private static void embedPositionDetectionPattern(int xStart, int yStart, + ByteMatrix matrix) throws WriterException { + // We know the width and height. + if (POSITION_DETECTION_PATTERN[0].length != 7 || POSITION_DETECTION_PATTERN.length != 7) { + throw new WriterException("Bad position detection pattern"); + } + for (int y = 0; y < 7; ++y) { + for (int x = 0; x < 7; ++x) { + if (!isEmpty(matrix.get(xStart + x, yStart + y))) { + throw new WriterException(); + } + matrix.set(xStart + x, yStart + y, POSITION_DETECTION_PATTERN[y][x]); + } + } + } + + // Embed position detection patterns and surrounding vertical/horizontal separators. + private static void embedPositionDetectionPatternsAndSeparators(ByteMatrix matrix) throws WriterException { + // Embed three big squares at corners. + int pdpWidth = POSITION_DETECTION_PATTERN[0].length; + // Left top corner. + embedPositionDetectionPattern(0, 0, matrix); + // Right top corner. + embedPositionDetectionPattern(matrix.getWidth() - pdpWidth, 0, matrix); + // Left bottom corner. + embedPositionDetectionPattern(0, matrix.getWidth() - pdpWidth, matrix); + + // Embed horizontal separation patterns around the squares. + int hspWidth = HORIZONTAL_SEPARATION_PATTERN[0].length; + // Left top corner. + embedHorizontalSeparationPattern(0, hspWidth - 1, matrix); + // Right top corner. + embedHorizontalSeparationPattern(matrix.getWidth() - hspWidth, + hspWidth - 1, matrix); + // Left bottom corner. + embedHorizontalSeparationPattern(0, matrix.getWidth() - hspWidth, matrix); + + // Embed vertical separation patterns around the squares. + int vspSize = VERTICAL_SEPARATION_PATTERN.length; + // Left top corner. + embedVerticalSeparationPattern(vspSize, 0, matrix); + // Right top corner. + embedVerticalSeparationPattern(matrix.getHeight() - vspSize - 1, 0, matrix); + // Left bottom corner. + embedVerticalSeparationPattern(vspSize, matrix.getHeight() - vspSize, + matrix); + } + + // Embed position adjustment patterns if need be. + private static void maybeEmbedPositionAdjustmentPatterns(int version, ByteMatrix matrix) + throws WriterException { + if (version < 2) { // The patterns appear if version >= 2 + return; + } + int index = version - 1; + int[] coordinates = POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[index]; + int numCoordinates = POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[index].length; + for (int i = 0; i < numCoordinates; ++i) { + for (int j = 0; j < numCoordinates; ++j) { + int y = coordinates[i]; + int x = coordinates[j]; + if (x == -1 || y == -1) { + continue; + } + // If the cell is unset, we embed the position adjustment pattern here. + if (isEmpty(matrix.get(x, y))) { + // -2 is necessary since the x/y coordinates point to the center of the pattern, not the + // left top corner. + embedPositionAdjustmentPattern(x - 2, y - 2, matrix); + } + } + } + } + +} diff --git a/src/com/google/zxing/qrcode/encoder/QRCode.java b/src/com/google/zxing/qrcode/encoder/QRCode.java new file mode 100644 index 0000000..05c8185 --- /dev/null +++ b/src/com/google/zxing/qrcode/encoder/QRCode.java @@ -0,0 +1,239 @@ +/* + * 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.qrcode.encoder; + +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.decoder.Mode; + +/** + * @author satorux@google.com (Satoru Takabayashi) - creator + * @author dswitkin@google.com (Daniel Switkin) - ported from C++ + */ +public final class QRCode { + + public static final int NUM_MASK_PATTERNS = 8; + + private Mode mode; + private ErrorCorrectionLevel ecLevel; + private int version; + private int matrixWidth; + private int maskPattern; + private int numTotalBytes; + private int numDataBytes; + private int numECBytes; + private int numRSBlocks; + private ByteMatrix matrix; + + public QRCode() { + mode = null; + ecLevel = null; + version = -1; + matrixWidth = -1; + maskPattern = -1; + numTotalBytes = -1; + numDataBytes = -1; + numECBytes = -1; + numRSBlocks = -1; + matrix = null; + } + + // Mode of the QR Code. + public Mode getMode() { + return mode; + } + + // Error correction level of the QR Code. + public ErrorCorrectionLevel getECLevel() { + return ecLevel; + } + + // Version of the QR Code. The bigger size, the bigger version. + public int getVersion() { + return version; + } + + // ByteMatrix width of the QR Code. + public int getMatrixWidth() { + return matrixWidth; + } + + // Mask pattern of the QR Code. + public int getMaskPattern() { + return maskPattern; + } + + // Number of total bytes in the QR Code. + public int getNumTotalBytes() { + return numTotalBytes; + } + + // Number of data bytes in the QR Code. + public int getNumDataBytes() { + return numDataBytes; + } + + // Number of error correction bytes in the QR Code. + public int getNumECBytes() { + return numECBytes; + } + + // Number of Reedsolomon blocks in the QR Code. + public int getNumRSBlocks() { + return numRSBlocks; + } + + // ByteMatrix data of the QR Code. + public ByteMatrix getMatrix() { + return matrix; + } + + + // Return the value of the module (cell) pointed by "x" and "y" in the matrix of the QR Code. They + // call cells in the matrix "modules". 1 represents a black cell, and 0 represents a white cell. + public int at(int x, int y) { + // The value must be zero or one. + int value = matrix.get(x, y); + if (!(value == 0 || value == 1)) { + // this is really like an assert... not sure what better exception to use? + throw new RuntimeException("Bad value"); + } + return value; + } + + // Checks all the member variables are set properly. Returns true on success. Otherwise, returns + // false. + public boolean isValid() { + return + // First check if all version are not uninitialized. + mode != null && + ecLevel != null && + version != -1 && + matrixWidth != -1 && + maskPattern != -1 && + numTotalBytes != -1 && + numDataBytes != -1 && + numECBytes != -1 && + numRSBlocks != -1 && + // Then check them in other ways.. + isValidMaskPattern(maskPattern) && + numTotalBytes == numDataBytes + numECBytes && + // ByteMatrix stuff. + matrix != null && + matrixWidth == matrix.getWidth() && + // See 7.3.1 of JISX0510:2004 (p.5). + matrix.getWidth() == matrix.getHeight(); // Must be square. + } + + // Return debug String. + public String toString() { + StringBuffer result = new StringBuffer(200); + result.append("<<\n"); + result.append(" mode: "); + result.append(mode); + result.append("\n ecLevel: "); + result.append(ecLevel); + result.append("\n version: "); + result.append(version); + result.append("\n matrixWidth: "); + result.append(matrixWidth); + result.append("\n maskPattern: "); + result.append(maskPattern); + result.append("\n numTotalBytes: "); + result.append(numTotalBytes); + result.append("\n numDataBytes: "); + result.append(numDataBytes); + result.append("\n numECBytes: "); + result.append(numECBytes); + result.append("\n numRSBlocks: "); + result.append(numRSBlocks); + if (matrix == null) { + result.append("\n matrix: null\n"); + } else { + result.append("\n matrix:\n"); + result.append(matrix.toString()); + } + result.append(">>\n"); + return result.toString(); + } + + public void setMode(Mode value) { + mode = value; + } + + public void setECLevel(ErrorCorrectionLevel value) { + ecLevel = value; + } + + public void setVersion(int value) { + version = value; + } + + public void setMatrixWidth(int value) { + matrixWidth = value; + } + + public void setMaskPattern(int value) { + maskPattern = value; + } + + public void setNumTotalBytes(int value) { + numTotalBytes = value; + } + + public void setNumDataBytes(int value) { + numDataBytes = value; + } + + public void setNumECBytes(int value) { + numECBytes = value; + } + + public void setNumRSBlocks(int value) { + numRSBlocks = value; + } + + // This takes ownership of the 2D array. + public void setMatrix(ByteMatrix value) { + matrix = value; + } + + // Check if "mask_pattern" is valid. + public static boolean isValidMaskPattern(int maskPattern) { + return maskPattern >= 0 && maskPattern < NUM_MASK_PATTERNS; + } + + // Return true if the all values in the matrix are binary numbers. + // + // JAVAPORT: This is going to be super expensive and unnecessary, we should not call this in + // production. I'm leaving it because it may be useful for testing. It should be removed entirely + // if ByteMatrix is changed never to contain a -1. + /* + private static boolean EverythingIsBinary(final ByteMatrix matrix) { + for (int y = 0; y < matrix.height(); ++y) { + for (int x = 0; x < matrix.width(); ++x) { + int value = matrix.get(y, x); + if (!(value == 0 || value == 1)) { + // Found non zero/one value. + return false; + } + } + } + return true; + } + */ + +} diff --git a/src/zxing-icon.png b/src/zxing-icon.png new file mode 100644 index 0000000..ee33194 Binary files /dev/null and b/src/zxing-icon.png differ