Issue
I´m working on a webapp running on Apache Tomcat. This webapp creates an image from camera and sends it base64 encoded to a Servlet. The servlet then should save that image. Everything works fine, except the fact that the image shows up incomplete when I open it. I have compared the base64 String and noticed, that there are differences between the string sent and the string which is printed by Java. Do you have any idea where these differences might come from?
The differences are as follows: - The Java String is longer - The first ~7610 bytes are equal - After the ~7610th byte, the Strings differ
private Path saveAsFile(InputStream stream) throws IOException {
byte[] bytes = new byte[1024];
String base64String = "";
while (stream.read(bytes) != -1) {
String tmp = new String(bytes, StandardCharsets.UTF_8);
base64String += tmp;
}
//this prints out a longer base64 String than the Javascript part
System.out.println(base64String);
String replaced = base64String.replaceFirst("data:image/png;base64,", "");
byte[] replacedbytes = Base64.decodeBase64(replaced);
Path temp = Files.createTempFile("photo", ".png");
FileOutputStream fos = new FileOutputStream(temp.toFile());
fos.write(replacedbytes);
return temp;
}
Thanks in advance!
Solution
This :
byte[] bytes = new byte[1024];
String base64String = "";
while (stream.read(bytes) != -1) {
String tmp = new String(bytes, StandardCharsets.UTF_8);
base64String += tmp;
}
Is not a valid way to read a stream in java. (Basically, you must take into account the value returned by stream.read() as the number of bytes that are actually valid in the buffer, and use new String(bytes, 0, theAboveValue, charset)
as the string constructor.
On top of this, you do not properly close the FileOutputStream
.
But even so, there are other issues here (decoding the charset before concatenating stream is probably a bug waiting to happen here - but that will work because base64 is actually pure ASCII and no multi-bytes characters will cause a bug here. Pure luck).
Cleaning the IO part of the code, it should rather look like :
private Path saveAsFile(InputStream stream) throws IOException {
byte[] bytes = new byte[1024];
int readBytes = 0;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
while ((readBytes = stream.read(bytes)) != -1) {
buffer.write(bytes, 0, readBytes);
}
//this prints out a longer base64 String than the Javascript part
String base64String = new String(buffer.getBytes, "US-ASCII");
String replaced = base64String.replaceFirst("data:image/png;base64,", "");
byte[] replacedbytes = Base64.decodeBase64(replaced);
Path temp = Files.createTempFile("photo", ".png");
FileOutputStream fos = new FileOutputStream(temp.toFile());
fos.write(replacedbytes);
fos.close();// You should not miss that too! And put it in a "finally" step not to leak file descriptors.
return temp;
}
This probably works better, but is inneficient as far as memory consumption goes (transforming bytes to String back to bytes, copying at each step!).
There is a better way !
You'd probably be even better of using libraries to do such copying. Apache Commons IO and Commons Codec have nice Base64InputStream
and IOUtils.copy
classes that could be of help here.
In which case, it could read :
private Path saveAsFile(InputStream stream) throws IOException {
// Maybe you should advance the stream to skip the "data/image stuff"...
//stream.skip(theActualNumberOfBytesToSkip);
try (Base64InputStream decoded = new Base64InputStream(stream); FileOutputStream file = /*whatever*/) {
IOUtils.copy(decoded, file);
}
}
Answered By - GPI