从 InputStream 中移除 Base64 前缀

Remove Base64 prefix from InputStream

我有一个驻留在文件服务器中的 Base64 编码图像字符串。编码的字符串有一个 prefix (ex: "data:image/png;base64,") for support in popular modern browsers (it's obtained via JavaScript's Canvas.toDataURL() method)。客户端向我的服务器发送图像请求,服务器验证它们和 returns Base64 编码字符串流。

如果客户端是网络客户端,通过将 src 设置为 Base64 编码的字符串,图像可以在 <img> 标签内按原样显示。但是,如果客户端是Android客户端,则需要将String解码成没有前缀的Bitmap。不过,这可以很容易地完成。

问题: 为了简化我的代码而不是重新发明轮子,我使用 Android 客户端的图像库来处理加载、显示和缓存图像(确切地说是 Facebook 的 Fresco Library)。但是,似乎没有库支持 Base64 解码(我想要我的蛋糕也吃它)。我想出的一个解决方案是在服务器上解码流式传输到客户端的 Base64 字符串。

尝试:

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();
} 

不幸的是,Fresco 库在显示图像时仍然存在问题(没有堆栈跟踪!)。由于在解码流时我的服务器上似乎没有问题(也没有堆栈跟踪),这让我相信这一定是前缀的问题。这让我进退两难。

问题:如何在不在服务器上存储和编辑整个流的情况下从发送到客户端的流中删除 Base64 前缀?这可能吗?

Fresco 确实支持解码数据 URI,就像 Web 客户端一样。

演示应用有 example 个。

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?

事实证明,在将流发送到客户端时删除前缀是一项相当复杂的任务。如果您不介意将整个字符串存储在服务器上,您可以简单地执行以下操作:

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();
        }
    }
}

但为了提高效率而不是将整个 Stream 存储在服务器上,任务要复杂得多。我们可以使用 Base64.Decoder.wrap() method 但问题是,如果它达到无法解码的值,它会抛出 IOException(如果他们提供一种方法,让字节保持原样,那不是很好吗?无法解码?)。不幸的是,无法解码 Base64 前缀,因为它不是 Base64 编码的。所以,它会抛出 IOException。

要解决这个问题,我们必须使用 InputStreamReader 来读取具有指定适当 CharsetInputStream。然后我们必须将从 InputStream 的 read() 方法调用接收到的 ints 转换为 chars。当我们达到适当数量的字符时,我们必须将它与 Base64 前缀的介绍 ("data") 进行比较。如果匹配,我们就知道 Stream 包含前缀,所以继续阅读直到我们到达前缀结束字符(逗号:“,”)。最后,我们可以开始流出前缀后的字节。示例:

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;
}

这似乎是在服务器端完成的一项繁重任务,所以我认为在客户端解码是更好的选择。不幸的是,大多数 Android 客户端库不支持 Base64 解码(尤其是前缀)。但是,正如@tyronen 指出的那样 Fresco does support it 如果已经获得了字符串。不过,这消除了使用 图像加载 库的关键原因之一。

Android客户端解码

在客户端应用程序解码非常简单。首先从InputStream中获取String:

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();
        }
    }
}

然后使用 Android 的 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);

Fresco 库似乎很难更新,因为它们使用了很多委托。因此,我继续使用 Picasso 图像加载库并创建了具有 Base64 解码能力的 own fork of it