Search code examples
javastreambase64inputstreamfresco

Remove Base64 prefix from InputStream


I have a Base64 encoded Image String residing in a File Server. The encoded String has a prefix (ex: "data:image/png;base64,") for support in popular modern browsers (it's obtained via JavaScript's Canvas.toDataURL() method). The client sends a request for the image to my server which verifies them and returns a stream of the Base64 encoded String.

If the client is a web client, the image can be displayed as is within an <img> tag by setting the src to the Base64 encoded String. However, if the client is an Android client, the String needs to be decoded into a Bitmap without the prefix. Though, this can be done fairly easily.

The Problem: In order to simplify my code and not reinvent the wheel, I'm using an Image Library for the Android client to handle loading, displaying, and caching the images (Facebook's Fresco Library to be exact). However, no library seems to support Base64 decoding (I want my cake and to eat it too). A solution I came up with is to decode the Base64 String on the server as it is being streamed to the client.

The Attempt:

S3Object obj = s3Client.getObject(new GetObjectRequest(bucketName, keyName));
Base64.Decoder decoder = Base64.getDecoder();
//decodes the stream as it is being read
InputStream stream = decoder.wrap(obj.getObjectContent());
try{
    return new StreamingOutput(){
        @Override
        public void write(OutputStream output) throws IOException, WebApplicationException{
            int nextByte = 0;
            while((nextByte = stream.read()) != -1){
                output.write(nextByte);
            }
            output.flush();
            output.close();
            stream.close();
        }
    };
}catch(Exception e){
    e.printStackTrace();
} 

Unfortunately, the Fresco library still has a problem displaying the image (with no stack traces!). As there doesn't seem to be an issue on my server when decoding the stream (no stack traces either), it leads me to believe that it must be an issue with the prefix. Which leaves me with a dilemma.

The Question: How do I remove the Base64 prefix from a Stream being sent to the client without storing and editing the entire Stream on the server? Is this possible?


Solution

  • How do I remove the Base64 prefix from a Stream being sent to the client without storing and editing the entire Stream on the server?

    Removing the prefix while sending the stream to the client turns out to be a pretty complex task. If you don't mind storing the whole String on the server you could simply do:

    BufferedReader br = null;
    StringBuilder sb = new StringBuilder();
    String line;
    try {
        br = new BufferedReader(new InputStreamReader(stream));
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        String result = sb.toString();
        //comma is the charater which seperates the prefix and the Base64 String
        int i = result.indexOf(",");
        result = result.substring(i + 1);
        //Now, that we have just the Base64 encoded String, we can decode it
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] decoded = decoder.decode(result);
        //Now, just write each byte from the byte array to the output stream
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    But to be more efficient and not store the entire Stream on the server, creates a much more complicated task. We could use the Base64.Decoder.wrap() method but the problem with that is that it throws an IOException if it reaches a value that cannot be decoded (wouldn't it be nice if they provided a method that just left the bytes as is if they can't be decoded?). And unfortunately, the Base64 prefix can't be decoded because it's not Base64 encoded. So, it would throw an IOException.

    To get around this problem, we would have to use an InputStreamReader to read the InputStream with the specified appropriate Charset. Then we would have to cast the ints received from the InputStream's read() method call to chars. When we reach the appropriate amount of chars, we would have to compare it with the Base64 prefix's intro ("data"). If it's a match, we know the Stream contains the prefix, so continue reading until we reach the prefix end character (the comma: ","). Finally, we can begin streaming out the bytes after the prefix. Example:

    S3Object obj = s3Client.getObject(new GetObjectRequest(bucketName, keyName));
    Base64.Decoder decoder = Base64.getDecoder();
    InputStream stream = obj.getObjectContent();
    InputStreamReader reader = new InputStreamReader(stream);
    try{
        return new StreamingOutput(){
            @Override
            public void write(OutputStream output) throws IOException, WebApplicationException{
                //for checking if string has base64 prefix
                char[] pre = new char[4]; //"data" has at most four bytes on a UTF-8 encoding
                boolean containsPre = false;
                int count = 0;
                int nextByte = 0;
                while((nextByte = stream.read()) != -1){
                    if(count < pre.length){
                        pre[count] = (char) nextByte;
                        count++;
                    }else if(count == pre.length){
                        //determine whether has prefix or not and act accordingly
                        count++;
                        containsPre = (Arrays.toString(pre).toLowerCase().equals("data")) ? true : false;
                        if(!containsPre){
                            //doesn't have Base64 prefix so write all the bytes until this point
                            for(int i = 0; i < pre.length; i++){
                                output.write((int) pre[i]);
                            }
                            output.write(nextByte);
                        }
                    }else if(containsPre && count < 25){
                        //the comma character (,) is considered the end of the Base64 prefix
                        //so look for the comma, but be realistic, if we don't find it at about 25 characters
                        //we can assume the String is not encoded correctly
                        containsPre = (Character.toString((char) nextByte).equals(",")) ? false : true;
                        count++;
                    }else{
                        output.write(nextByte);
                    }
                }
                output.flush();
                output.close();
                stream.close();
            }
        };
    }catch(Exception e){
        e.printStackTrace();
        return null;
    }
    

    This seems a bit hefty of a task to do on the server so I think decoding on the client side is a better choice. Unfortunately, most Android client side libraries don't have support for Base64 decoding (especially with the prefix). However, as @tyronen pointed out Fresco does support it if the String is already obtained. Though, this removes one of the key reasons to use an image loading library.

    Android Client Side Decoding

    To decode on the client side application is pretty easy. First obtain the String from the InputStream:

    BufferedReader br = null;
    StringBuilder sb = new StringBuilder();
    String line;
    try {
        br = new BufferedReader(new InputStreamReader(stream));
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        return sb.toString();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    Then decode the String using Android's Base64 class:

    int i = result.indexOf(",");
    result = result.substring(i + 1);
    byte[] decodedString = Base64.decode(result, Base64.DEFAULT);
    Bitmap bitMap = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
    

    The Fresco library seems hard to update due to them using a lot of delegation. So, I moved on to using the Picasso image loading library and created my own fork of it with the Base64 decoding ability.