fundagain
fundagain

Reputation: 398

Why is ZXing decoding so poorly?

The following program uses ZXing (and PDFBox) to encode a byte array (lengths N=1,2,3,...) as a QR code, which is then embedded into a PDF document, rendered, extracted as a BufferedImage, and decoded. The decoded and encoded byte arrays are compared. Decoding is attempted first without the decoding option PURE_BARCODE, and if this fails, decoding is then attempted with the PURE_BARCODE option.

Decoding starts failing around N=20 (N=28 for the example random seed). Note that until decoding fails, the byte arrays are correctly decoded.

When the decoding fails, the failed QR code is displayed in a dialog box and the payload String printed to the console.

If I point my phone at this dialog box, however, my phone has no problem decoding this QR Code image, and the decoded string matches that on the console.

What mistake am I making?

package zxing;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.DecodeHintType;
import com.google.zxing.EncodeHintType;
import com.google.zxing.FormatException;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.Map;
import java.util.PrimitiveIterator;
import java.util.Random;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;

public class Problem {

    public static void main(String[] args) throws IOException,
            WriterException,
            NotFoundException,
            ChecksumException,
            FormatException {
        Random r = new Random(12345);

        PrimitiveIterator.OfInt rb = r.ints(-128, 127).iterator();

        for (int N = 1; N < 100; N += 1) { // encode and decode random byte arrays of size N
            System.out.println(N);
            for (int numRuns = 0; numRuns < 10; numRuns++) { // number of tries at size N 

                byte[] payload = new byte[N]; // payload to be encoded

                for (int i = 0; i < N; i++) { // payload random initialization 
                    payload[i] = rb.next().byteValue();
                }
                final String payloadString = new String(payload, STRING_ENCODING); // encode as string 

                // encode using zxing
                final BufferedImage qr = MatrixToImageWriter.toBufferedImage(new QRCodeWriter().encode(payloadString,
                        BarcodeFormat.QR_CODE, 256, 256, ENCODING_HINTS));

                // insert into PDF
                PDDocument pdDocument = new PDDocument();
                PDPage page = new PDPage();
                pdDocument.addPage(page);
                PDPageContentStream pageContent = new PDPageContentStream(pdDocument, page);
                pageContent.drawImage(JPEGFactory.createFromImage(pdDocument, qr), 0, 0, 200, 200);
                pageContent.close();
                // render pdf and extract qr code image
                BufferedImage pageImage = new PDFRenderer(pdDocument).renderImage(0);
                BufferedImage qrcodeImage = pageImage.getSubimage(0, pageImage.getHeight() - 200, 200, 200);
                pdDocument.close();

                byte[] resultPayload;
                try { // try zxing decode qrcodeImage *not* in PURE_BARCODE mode
                    resultPayload = new QRCodeReader().decode(
                            new BinaryBitmap(new HybridBinarizer(new RGBLuminanceSource(qrcodeImage.getWidth(),
                                    qrcodeImage.getHeight(),
                                    qrcodeImage.getRGB(0, 0, qrcodeImage.getWidth(), qrcodeImage.getHeight(), null, 0,
                                            qrcodeImage.getWidth())))), DECODING_HINTS_IMPURE).getText().getBytes(
                                    STRING_ENCODING);
                } catch (Throwable ex) {
                    try { // failed so try zxing decode qrcodeImage in PURE_BARCODE mode
                        resultPayload = new QRCodeReader().decode(
                                new BinaryBitmap(new HybridBinarizer(new RGBLuminanceSource(qrcodeImage.getWidth(),
                                        qrcodeImage.getHeight(),
                                        qrcodeImage.getRGB(0, 0, qrcodeImage.getWidth(), qrcodeImage.getHeight(), null,
                                                0,
                                                qrcodeImage.getWidth())))), DECODING_HINTS_PURE).getText().getBytes(
                                        STRING_ENCODING);
                    } catch (Throwable ex2) { // both decodings failed, display image
                        JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(qrcodeImage)),
                                "N=" + Integer.toString(N),
                                JOptionPane.PLAIN_MESSAGE, null);
                        System.out.println("Encoded=" + payloadString);
                        throw ex2;
                    }
                }

                if (!Arrays.equals(payload, resultPayload)) {
                    throw new RuntimeException("Payload mismatch.");
                }


            }
        }

    }
    private final static String STRING_ENCODING = "ISO-8859-1";
    final private static Map<EncodeHintType, Object> ENCODING_HINTS = new EnumMap<EncodeHintType, Object>(
            EncodeHintType.class);

    static {
        ENCODING_HINTS.put(EncodeHintType.CHARACTER_SET, STRING_ENCODING);
        ENCODING_HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
    }

    final private static Map<DecodeHintType, Object> DECODING_HINTS_IMPURE = new EnumMap<DecodeHintType, Object>(
            DecodeHintType.class);

    static {
        DECODING_HINTS_IMPURE.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
        DECODING_HINTS_IMPURE.put(DecodeHintType.CHARACTER_SET, STRING_ENCODING);
    }

    final private static Map<DecodeHintType, Object> DECODING_HINTS_PURE = new EnumMap<DecodeHintType, Object>(
            DecodeHintType.class);

    static {
        DECODING_HINTS_PURE.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
        DECODING_HINTS_PURE.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE);
        DECODING_HINTS_PURE.put(DecodeHintType.CHARACTER_SET, STRING_ENCODING);
    }

}

[... ZXing throws a FormatException ...]

[... The ZXing phone application can read the dialog QR Code too ...]

Upvotes: 3

Views: 1824

Answers (1)

fundagain
fundagain

Reputation: 398

The following changes improves the accuracy to about N=64 for high quality error correction and N=100 with no error correction (which suffices for my needs).

Three changes are needed.

Firstly, as suggested by @Tilman Hausherr, use a lossless factory when inserting the QR Code image into the PDF. While this does not make any difference on the code as presented in the OP, it is a necessary co-condition for the other changes to be effective.

Further, render the PDF in binary rather than rgb, and at a higher scale. The default scale (1.0) is only 72dpi.

pageContent.drawImage(LosslessFactory.createFromImage(pdDocument, qr), 0, 0, 200, 200);
...
BufferedImage pageImage = new PDFRenderer(pdDocument)
    .renderImage(0,3.0f,ImageType.BINARY);

Upvotes: 2

Related Questions