17

Using Base64 from Apache commons

public byte[] encode(File file) throws FileNotFoundException, IOException {
        byte[] encoded;
        try (FileInputStream fin = new FileInputStream(file)) {
            byte fileContent[] = new byte[(int) file.length()];
            fin.read(fileContent);
            encoded = Base64.encodeBase64(fileContent);
        }
        return encoded;   
}


Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: Java heap space
    at org.apache.commons.codec.binary.BaseNCodec.encode(BaseNCodec.java:342)
    at org.apache.commons.codec.binary.Base64.encodeBase64(Base64.java:657)
    at org.apache.commons.codec.binary.Base64.encodeBase64(Base64.java:622)
    at org.apache.commons.codec.binary.Base64.encodeBase64(Base64.java:604)

I'm making small app for mobile device.

Ivan Ivanovich
  • 227
  • 1
  • 3
  • 7

8 Answers8

35

You cannot just load the whole file into memory, like here:

byte fileContent[] = new byte[(int) file.length()];
fin.read(fileContent);

Instead load the file chunk by chunk and encode it in parts. Base64 is a simple encoding, it is enough to load 3 bytes and encode them at a time (this will produce 4 bytes after encoding). For performance reasons consider loading multiples of 3 bytes, e.g. 3000 bytes - should be just fine. Also consider buffering input file.

An example:

byte fileContent[] = new byte[3000];
try (FileInputStream fin = new FileInputStream(file)) {
    while(fin.read(fileContent) >= 0) {
         Base64.encodeBase64(fileContent);
    }
}

Note that you cannot simply append results of Base64.encodeBase64() to encoded bbyte array. Actually, it is not loading the file but encoding it to Base64 causing the out-of-memory problem. This is understandable because Base64 version is bigger (and you already have a file occupying a lot of memory).

Consider changing your method to:

public void encode(File file, OutputStream base64OutputStream)

and sending Base64-encoded data directly to the base64OutputStream rather than returning it.

UPDATE: Thanks to @StephenC I developed much easier version:

public void encode(File file, OutputStream base64OutputStream) {
  InputStream is = new FileInputStream(file);
  OutputStream out = new Base64OutputStream(base64OutputStream)
  IOUtils.copy(is, out);
  is.close();
  out.close();
}

It uses Base64OutputStream that translates input to Base64 on-the-fly and IOUtils class from Apache Commons IO.

Note: you must close the FileInputStream and Base64OutputStream explicitly to print = if required but buffering is handled by IOUtils.copy().

Tomasz Nurkiewicz
  • 334,321
  • 69
  • 703
  • 674
  • Most flavors of Base64 are more complicated than this ... to deal with the fixed line-length requirement. See my answer for alternatives. – Stephen C Mar 06 '12 at 08:22
  • how do i get byte[] from all of this? – Ivan Ivanovich Mar 06 '12 at 09:21
  • @IvanIvanovich: that's what I am trying to say! If you have a pretty big file (say, 100 MiB), you are first loading it to memory and then placing its Base64 into `byte[]` which totals to ~233 MiB - where probably few KiB is enough if you use streaming. If you **really** need a `byte[]`, consider using `ByteArrayOutputStream`, but you will again run into OOM problems - which we are trying to avoid. – Tomasz Nurkiewicz Mar 06 '12 at 09:24
  • Please bear with me. I need byte[] to save decoded file into the db. Is its possible to do this with OutputStream? – Ivan Ivanovich Mar 06 '12 at 09:34
  • @IvanIvanovich: first of all you can use BLOB instead if CLOB to store binary data directly, no need for Base64. Secondly, `PreparedStatement` and `CallableStatement` have `setBlob`/`setClob` methods accepting `InputStream`/`Reader`. Believe, it is much safer to stream data rather than keeping it all in memory, especially when multiple threads are trying to do the same thing. – Tomasz Nurkiewicz Mar 06 '12 at 09:44
  • How do i link OutputStream to InputStream to create blob? I'm sorry for bothering you, i am a beginner in programming. – Ivan Ivanovich Mar 06 '12 at 09:50
  • @IvanIvanovich: no problem, we are here trying to help. Have a look at: http://www.coderanch.com/t/275464/Streams/java/OutputStream-InputStream and http://stackoverflow.com/questions/1225909 – Tomasz Nurkiewicz Mar 06 '12 at 10:10
  • Can i change your method to `public void encode(File file, InputStream base64inputStream)` ?? – Ivan Ivanovich Mar 06 '12 at 10:25
  • @IvanIvanovich: it won't make much sense - you need an `OutputStream` to **write** Base64 encoded data to. Then you need to pipe this `OutputStream` to `InputStream`, that is further read by `Blob`. BTW try using `ByteArrayOutputStream` for `base64OutputStream` argument and then simply call `getBytes()`. Yoou will still keep whole Base64 stream in memory, but at least the original file is streamed :-(. – Tomasz Nurkiewicz Mar 06 '12 at 10:34
  • One more question: Hibernate requires inputstream as well as length to create Blob. How do i find this length? Or i just multiply a length of original file by 2? – Ivan Ivanovich Mar 06 '12 at 11:13
  • @IvanIvanovich The size of Base64 encoded value will be original size * 4/3 rounded up to be divisible by 4. – Tomasz Nurkiewicz Mar 06 '12 at 11:51
  • Oh i'm so confused. When do i close piped streams? – Ivan Ivanovich Mar 06 '12 at 12:27
  • 1
    I tried this in first solution sb.append(Base64.encodeToString(fileContent, Base64.DEFAULT)); & it worked for me. sb is object of StringBuilder. – Arun Badole Jul 09 '14 at 10:27
  • FileInputStream does not guarantee to read 3000 bytes. It seems we need to use DataInputStream.readFully() method. Otherwise Base64 encoder will provide different output. – Oleksandr Albul Jan 18 '19 at 09:12
6

Well, do not do it for the whole file at once.

Base64 works on 3 bytes at a time, so you can read your file in batches of "multiple of 3" bytes, encode them and repeat until you finish the file:

// the base64 encoding - acceptable estimation of encoded size
StringBuilder sb = new StringBuilder(file.length() / 3 * 4);

FileInputStream fin = null;
try {
    fin = new FileInputStream("some.file");
    // Max size of buffer
    int bSize = 3 * 512;
    // Buffer
    byte[] buf = new byte[bSize];
    // Actual size of buffer
    int len = 0;

    while((len = fin.read(buf)) != -1) {
        byte[] encoded = Base64.encodeBase64(buf);

        // Although you might want to write the encoded bytes to another 
        // stream, otherwise you'll run into the same problem again.
        sb.append(new String(buf, 0, len));
    }
} catch(IOException e) {
    if(null != fin) {
        fin.close();
    }
}

String base64EncodedFile = sb.toString();
bluish
  • 26,356
  • 27
  • 122
  • 180
Sorin
  • 1,965
  • 2
  • 12
  • 18
  • How to reverse this I mean for getting original file from that base64? – Vishal Senjaliya Dec 22 '18 at 05:18
  • 1
    @VishalSenjaliya In Java 8, you could [wrap](https://docs.oracle.com/javase/8/docs/api/java/util/Base64.Decoder.html#wrap-java.io.InputStream-) a base64 encoded InputStream to read from it decoded data. – Sorin Jan 03 '19 at 09:46
6

Either the file is too big, or your heap is too small, or you've got a memory leak.

  • If this only happens with really big files, put something into your code to check the file size and reject files that are unreasonably big.

  • If this happens with small files, increase your heap size by using the -Xmx command line option when you launch the JVM. (If this is in a web container or some other framework, check the documentation on how to do it.)

  • If the file recurs, especially with small files, the chances are that you've got a memory leak.


The other point that should be made is that your current approach entails holding two complete copies of the file in memory. You should be able to reduce the memory usage, though you'll typically need a stream-based Base64 encoder to do this. (It depends on which flavor of the base64 encoding you are using ...)

This page describes a stream-based Base64 encoder / decoder library, and includes lnks to some alternatives.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
1

This is best code to upload image of more size

bitmap=Bitmap.createScaledBitmap(bitmap, 100, 100, true);

ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); //compress to which format you want.
byte [] byte_arr = stream.toByteArray();  
String image_str = Base64.encodeBytes(byte_arr);
zkanoca
  • 9,664
  • 9
  • 50
  • 94
rajlaxmi_jagdale
  • 1,370
  • 15
  • 16
1
  1. You are not reading the whole file, just the first few kb. The read method returns how many bytes were actually read. You should call read in a loop until it returns -1 to be sure that you have read everything.

  2. The file is too big for both it and its base64 encoding to fit in memory. Either

    • process the file in smaller pieces or
    • increase the memory available to the JVM with the -Xmx switch, e.g.

      java -Xmx1024M YourProgram
      
Joni
  • 108,737
  • 14
  • 143
  • 193
0

In Manifest in applcation tag write following android:largeHeap="true"

It worked for me

rajlaxmi_jagdale
  • 1,370
  • 15
  • 16
0

Java 8 added Base64 methods, so Apache Commons is no longer needed to encode large files.

public static void encodeFileToBase64(String inputFile, String outputFile) {
    try (OutputStream out = Base64.getEncoder().wrap(new FileOutputStream(outputFile))) {
        Files.copy(Paths.get(inputFile), out);
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}
jaco0646
  • 15,303
  • 7
  • 59
  • 83
0

Well, looks like your file is too large to keep the multiple copies necessary for an in-memory Base64 encoding in the available heap memory at the same time. Given that this is for a mobile device, it's probably not possible to increase the heap, so you have two options:

  • make the file smaller (much smaller)
  • Do it in a stram-based way so that you're reading from an InputStream one small part of the file at a time, encode it and write it to an OutputStream, without ever keeping the enitre file in memory.
Michael Borgwardt
  • 342,105
  • 78
  • 482
  • 720