我有一个Base64编码的图像字符串驻留在文件服务器。编码后的字符串有一个前缀(例如:"data:image/png;base64,"),以支持流行的现代浏览器(它通过JavaScript的Canvas.toDataURL()方法获得)。客户端向我的服务器发送图像请求,服务器验证它们并返回Base64编码字符串的流。
如果客户端是web客户端,通过将src
设置为Base64编码的String,图像可以在<img>
标记中显示。但是,如果客户端是Android客户端,则需要将String解码为不带前缀的位图。不过,这很容易做到。
问题:为了简化我的代码,而不是重新发明轮子,我使用Android客户端的图像库来处理加载、显示和缓存图像(确切地说,是Facebook的Fresco库)。然而,似乎没有库支持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客户端一样。
如何从发送到客户端的流中删除Base64前缀,而无需在服务器上存储和编辑整个流?
在向客户端发送流时删除前缀是一项相当复杂的任务。如果您不介意将整个字符串存储在服务器上,您可以简单地这样做:
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();
}
}
}
但是为了更有效率,而不是将整个流存储在服务器上,创建了一个更复杂的任务。我们可以使用Base64.Decoder.wrap()方法,但问题是,如果它达到一个无法解码的值,它会抛出IOException(如果它们提供了一个方法,只留下字节,如果它们无法解码,那不是很好吗?)。不幸的是,Base64前缀不能被解码因为它不是Base64编码的。它会抛出IOException。
要解决这个问题,我们必须使用InputStreamReader
来读取具有指定的适当Charset
的InputStream
。然后,我们必须将从InputStream的read()方法调用接收到的int
转换为char
。当达到适当的字符数时,我们必须将其与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确实支持它。但是,这消除了使用图像加载库的一个关键原因。
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类解码字符串:
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库似乎很难更新,因为它们使用了大量的委托。所以,我继续使用毕加索图像加载库,并创建了我自己的带有Base64解码能力的分支。