Search code examples
androidandroid-volleyuniversal-image-loader

Load image from binary base64


EDIT: This is a bug in Android version <4.3 Kitkat. It relates to the libjpeg library in Android, which can't handle JPEGs with missing EOF/EOI bits, or apparently with metadata/EXIF data that it doesn't like. https://code.google.com/p/android/issues/detail?id=9064

ORIGINAL QUESTION:

I have an issue when loading an image in my app.

My endpoint sends JSON which contains a BASE64 encoded image. Depending on the REST call, these images can be PNG or JPG. Some of the JPG files suffer from an issue where they are missing an EOF bit at the end. The PNG files work, and some JPG files work, but unfortunately a lot of these JPG files with the issue are present in the Oracle DB (stored as BLOB). I don't have control of the DB.

I have been looking through Google bugs here: https://code.google.com/p/android/issues/detail?id=9064 and here: https://code.google.com/p/android/issues/detail?id=57502

The issue is also seen where the encoding is CYMK using a custom ICC profile.

Decoding the image the standard way returns false:

byte[] imageAsBytes = Base64.decode(base64ImageString, Base64.DEFAULT);
return BitmapFactory.decodeByteArray(imageAsBytes, 0, imageAsBytes.length);

According to the bug reports above, the built in JPG parser in Android is to blame.

I'm trying to figure out a workaround for my device, which is stuck on 4.2.2. I have no other option on this OS version.

I thought it might be a good idea to try and use an image loader library like Universal Image Loader, but it requires I either have the image stored locally, or stored on a URL. As I get the data in BASE64 from the REST server, I can't use this. An option is to support decodeByteArray in a custom class that extends BaseImageDecoder, as stated by the dev at the bottom here: https://github.com/nostra13/Android-Universal-Image-Loader/issues/209

Here's where I get stuck. I already have a custom image decoder to try handle the issue of the missing EOF marker in the JPG file, but I don't know how to edit it to add support for decodeByteArray.

Here is my CustomImageDecoder:

public class CustomImageDecoder extends BaseImageDecoder {

    public CustomImageDecoder(boolean loggingEnabled) {
        super(loggingEnabled);
    }

    @Override
    protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
        InputStream stream = decodingInfo.getDownloader()
                .getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
        return stream == null ? null : new JpegClosedInputStream(stream);
    }

    private class JpegClosedInputStream extends InputStream {

        private static final int JPEG_EOI_1 = 0xFF;
        private static final int JPEG_EOI_2 = 0xD9;
        private final InputStream inputStream;
        private int bytesPastEnd;

        private JpegClosedInputStream(final InputStream iInputStream) {
            inputStream = iInputStream;
            bytesPastEnd = 0;
        }

        @Override
        public int read() throws IOException {
            int buffer = inputStream.read();
            if (buffer == -1) {
                if (bytesPastEnd > 0) {
                    buffer = JPEG_EOI_2;
                } else {
                    ++bytesPastEnd;
                    buffer = JPEG_EOI_1;
                }
            }

            return buffer;
        }
    }
}

By the way, using the above custom class, I am trying to load my byte array like this:

byte[] bytes = Base64.decode(formattedB64String, Base64.NO_WRAP);
ByteArrayInputStream is = new ByteArrayInputStream(bytes);
String imageId = "stream://" + is.hashCode();
...
ImageLoader imageLoader = ImageLoader.getInstance();
imageLoader.displayImage(imageId, userImage, options);

and I get this error: ImageLoader: Image can't be decoded [stream://1097215584_656x383]

Universal Image loader does not allow the stream:// schema, so I created a custom BaseImageDownloader class that allows it:

public class StreamImageDownloader extends BaseImageDownloader {

    private static final String SCHEME_STREAM = "stream";
    private static final String STREAM_URI_PREFIX = SCHEME_STREAM + "://";

    public StreamImageDownloader(Context context) {
        super(context);
    }

    @Override
    protected InputStream getStreamFromOtherSource(String imageUri, Object extra) throws IOException {
        if (imageUri.startsWith(STREAM_URI_PREFIX)) {
            return (InputStream) extra;
        } else {
            return super.getStreamFromOtherSource(imageUri, extra);
        }
    }
}

So if anyone can help me create a better CustomImageDecoder that handles a BASE64 encoded string, or a byte[] containing an image so I can use decodeByteArray, I would be grateful!

Thank you.


Solution

  • Just to close this off: The issue here is actually a bug in Android <4.3 where Android can't display images that either aren't closed properly (missing end bytes) or contain certain metadata that, for some reason, it doesn't like. I'm not sure what metadata this is, however. My issue was with JPEGs not being terminated properly.

    The bug is fixed in Android 4.3 anyway.