9

I am developping an Android app, and I need to encode and decode a bytes array in a QRCode generated with the ZXing app. My problem is that my message decoded does not exactly match the generated byte array. I tried to create a QRCode based on a byte array containing incrementing indexes, i.e.

input = [0, 1, 2, ..., 124, 125, 126, 127, -128, -127,... -3, -2, -1, 0, 1, 2, ...]

And after encoding the message in the QRCode and decoding it on the responder side, I obtain the following byte array output:

output = [0, 1, 2, ..., 124, 125, 126, 127, 63, 63,... 63, 63, 63, 0, 1, 2, ...]

All the "negative" byte values are turned to ASCII char 63: '?' question mark characters. I assume that something is going wrong with the encoding charset, but since I am using ISO-8859-1 which everyone claims to be the solution of such kind of issue (other topic treating the same kind of issue or here), I don't see where is my mistake, or if I am skipping a step during the instanciation of the encoding or the decoding. Here is the code that I execute to encode a given byte array:

String text = "";
byte[] res = new byte[272];
for (int i = 0; i < res.length; i++) {
    res[i] = (byte) (i%256);
}
try {
    text = new String(res, "ISO8859_1");
} catch (UnsupportedEncodingException e) {
    // TODO
}
Intent intent = new Intent(Intents.Encode.ACTION);
Intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
intent.putExtra(Intents.Encode.TYPE, Contents.Type.TEXT);
intent.putExtra(Intents.Encode.FORMAT, "ISO8859_1");
intent.putExtra(Intents.Encode.DATA, text);
intent.putExtra(Intents.Encode.FORMAT, BarcodeFormat.QR_CODE.toString());

boolean useVCard = intent.getBooleanExtra(USE_VCARD_KEY, false);
QRCodeEncoder qrCodeEncoder = new QRCodeEncoder(activity, intent, dimension, useVCard);
Bitmap bitmap = qrCodeEncoder.encodeAsBitmap();

And to decode a QRCode, I send the following Intent

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.qrcodeDecoding);

    Intent intent = new Intent(Intents.Scan.ACTION);
    intent.putExtra(Intents.Scan.MODE, Intents.Scan.QR_CODE_MODE);
    startActivityForResult(intent, 0);
}

And wait for Result:

@Override
protected void onActivityResult(int request, int result, Intent data)
{
    if(request == 0)
    {
        //action
        if(result == RESULT_OK)
        {
            String res = data.getStringExtra(Intents.Scan.RESULT);
            byte[] dat = null;

            try{
                    dat = res.getBytes("ISO8859_1");
            } catch(UnsopportedEncodingException e) {
                    //TODO
            }
        }
        else if(result == RESULT_CANCELED)
        {
            //TODO
        }
    }

}

Could you please tell me where are my mistakes, or where should I look at?

Thank you a lot,

Franck

Community
  • 1
  • 1
franckysnow
  • 707
  • 2
  • 6
  • 13
  • Just to "play around" (I have no idea about this) what happens if you use UTF-8 as encoding for example? – User Jun 16 '12 at 23:26
  • UTF-8 certainly won't work. Not every byte sequence is a valid UTF-8 sequence, to start. So it's not even possible to get a string out of most inputs that way. – Sean Owen Jun 17 '12 at 10:27

3 Answers3

5

In one of my apps I needed to encode and decode a bytes array in a QRCode generated with the ZXing app. As the byte array contained compressed text data I wanted to avoid base64 encoding. It is possible to do this but as I have so far not seen a complete set of code snippets I will post them here.

Encoding:

public void showQRCode(Activity activity, byte[] data){
  Intent intent = new Intent("com.google.zxing.client.android.ENCODE");
  intent.putExtra("ENCODE_TYPE", "TEXT_TYPE");
  intent.putExtra("ENCODE_SHOW_CONTENTS", false);
  intent.putExtra("ENCODE_DATA", new String(data, "ISO-8859-1"));
  activity.startActivity(intent);
}

Start scanning:

public static void startQRCodeScan(Activity activity){
  Intent intent = new Intent(com.google.zxing.client.android.SCAN);
  intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
  intent.putExtra("CHARACTER_SET", "ISO-8859-1");
  activity.startActivityForResult(intent, 0);
}

Scan result handler:

public void onActivityResult(int requestCode, int resultCode, Intent intent) {
  byte[] result = intent.getStringExtra("SCAN_RESULT").getBytes("ISO-8859-1");
  ...
}

I think not setting CHARACTER_SET to ISO-8859-1 in the intent data for starting the scan is the point that made the code of the original question fail. It took me quite some time to dig this out as I have not seen this clearly posted anywhere and Latin 1 encoding is the standard encoding for QR code in Xzing. Especially tricky is the fact that the Xzing online decoder http://zxing.org/w/decode.jspx does not set CHARACTER_SET as well so that the generated QR code looks faulty when decoded on this site.

Nantoka
  • 4,174
  • 1
  • 33
  • 36
2

You are making the mistake of thinking that you can turn arbitrary binary data into a valid string without using some kind of armouring. It doesn't work. Binary -> text -> binary is lossy using any of the standard character sets / encoding. (Hint: using UTF-8 won't work either.)

You should use something like base64 encoding or hexadecimal encoding to ensure that the binary data doesn't get mangled.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • 1
    I agree with the overall sentiment; ISO-8559-1 does happen to work for this purpose in Java though. – Sean Owen Jun 17 '12 at 10:21
  • @StephenC Thank you a lot, using base64 encoding in my implementation solved the problem! – franckysnow Jun 17 '12 at 11:34
  • 1
    @SeanOwen I agree with you, encoding and decoding byte[] to String ISO-8859-1 and vice-versa works. The problem may be located on the ZXing layer. However, base64 solved the case. – franckysnow Jun 17 '12 at 11:36
  • 1
    It is my understanding ISO-8859-1 is 100% efficient when converting an arbitrary buffer to a string. base64 is 75%. Is my understanding wrong? – Luca Carlon Jan 25 '16 at 15:07
  • @LucaCarlon - No it isn't. But efficiency isn't the only issue. – Stephen C Jan 25 '16 at 23:05
  • @StephenC if by no you mean I'm not wrong, then do you consider binary -> ISO-8859-1 lossy? Do you see any reason for not doing it? – Luca Carlon Jan 26 '16 at 08:26
  • Yes, I do. At least in general, if not in this particular case. Binary data represented as an ISO-8859-1 "text" is fragile in a lot of contexts. – Stephen C Jan 26 '16 at 09:52
  • @SeanOwen I hit the same problem, wasted 2 days already. I have a random byte array, converting it to ISO-8859-1 string and passing to qr code writer didn't solve my problem. Converting to base64 does solve it but then it increases the total data size. What can I do next? – arvind.mohan Apr 23 '17 at 06:31
1

Conceptually, QR codes encode text, not bytes. Inside of course they translate input to a series of bytes, though that's opaque to the caller. You are right that, as it happens, choosing the right encoding would let you sneak the bytes through, and ISO-8859-1 is the right choice here. It does work, actually.

ASCII is not possible since it does not define chars for >= 128, and UTF-8 is definitely not going to work

The issue here is probably your code. I am not sure what you're attempting here... it looks like you're setting up to send an Intent somewhere (to Barcode Scanner?) but then you don't, you're just making an Intent and sending it to some code you copied from the project? I imagine something's gone wrong with how you are setting extras to the Intent.

This should be much simpler if you're doing it within your app. Just reuse QRCodeEncoder.encodeAsBitmap() directly and delete the rest of this.

Sean Owen
  • 66,182
  • 23
  • 141
  • 173
  • The only constructor of `QRCodeEncoder` takes for arguments `QRCodeEncoder(Activity activity, Intent intent, int dimension, boolean useVCard)`. And since there are no setter methods, it appread to me that it was the only way to go. If you could be a bit more specific about what could "go wrong" with the extras? It seems that base64 encoding and passing then an ISO8859-1 String to the `QRCodeEncoder` made the trick. Thanks! – franckysnow Jun 17 '12 at 12:14
  • I mean that you don't need the class at all -- just copy the method I indicated. The code in `android/` is not intended as a library; it's our app. The main problem is that you are doing something superfluous here -- listening for an `Intent` when you didn't send one to reply to. Read the javadoc for the keys -- `FORMAT` is nothing do with character encoding and you set it twice. – Sean Owen Jun 17 '12 at 12:49