356

I need an efficient (read native) way to convert an ArrayBuffer to a base64 string which needs to be used on a multipart post.

1
  • 1
    if you need it in a multipart/form-data then create a FormData and a Blob from ArrayBuffer, append the blob to formdata and post binary data instead of base64. Commented Sep 27, 2023 at 15:42

21 Answers 21

376
function _arrayBufferToBase64( buffer ) {
    var binary = '';
    var bytes = new Uint8Array( buffer );
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode( bytes[ i ] );
    }
    return window.btoa( binary );
}

but, non-native implementations are faster e.g. https://gist.github.com/958841 see http://jsperf.com/encoding-xhr-image-data/6

jsPerf.com is jsPerf.app now: https://jsperf.app/encoding-xhr-image-data/51

Updated benchmarks: https://jsben.ch/wnaZC

Sign up to request clarification or add additional context in comments.

15 Comments

I tried the non-native implementation from the link and it took 1min and half to convert a 1M size buffer while the loop code above only took 1sec.
I like the simplicity of this approach, but all that string concatenation can be costly. It looks like building an array of the characters and join()ing them at the end is significantly faster on Firefox, IE, and Safari (but quite a lot slower on Chrome): jsperf.com/tobase64-implementations
I am trying 50mb pdf file upload using angualrjs and webapi2. I am using above line code , after upload file, the page got crashed and hanged . Below line of code ,I was used but getting null value in webapi method. "var base64String = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));" please suggest any idea ...
I'm wondering why everyone is avoiding the native buffer toString('base64') method.
@JoãoEduardoSoareseSilva because not everyone is using Node - Node's Buffer doesn't exist in the browser.
|
206

This works fine for me:

var base64String = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));

In ES6, the syntax is a little simpler:

const base64String = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));

As pointed out in the comments, this method may result in a runtime error in some browsers when the ArrayBuffer is large. The exact size limit is implementation dependent in any case.

11 Comments

I like this method better for conciseness, but get a "maximum call stack size exceeded error". The loop technique above gets around that.
I'm also getting a stack size error, so I used mobz's answer and it worked great.
It didn't work for large buffers. Slight modification to make it work: btoa([].reduce.call(new Uint8Array(bufferArray),function(p,c){return p+String.fromCharCode(c)},''))
I am trying 50mb pdf file upload using angualrjs and webapi2. I am using above line code , after upload file, the page got crashed and hanged . Below line of code ,I was used but getting null value in webapi method. "var base64String = btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));" please suggest any idea ...
@Kugel btoa is safe for characters in the code range 0-255, as this is here the case (Think about the 8 in Uint8Array).
|
63

For those who like it short, here's an other one using Array.reduce which will not cause stack overflow:

var base64 = btoa(
  new Uint8Array(arrayBuffer)
    .reduce((data, byte) => data + String.fromCharCode(byte), '')
);

5 Comments

Not sure if that's really sexy. After all, you're creating <amount of Bytes in the buffer> new strings.
How about btoa(new Uint8Array(arraybuffer).reduce((data,byte)=>(data.push(String.fromCharCode(byte)),data),[]).join(''))?
Another alternative: btoa(Array.from(new Uint8Array(arraybuffer)).map(b => String.fromCharCode(b)).join('')).
Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range
@RouR Same thing here. Not sure what's going on, but it seems fromCharCode(byte & 0xff) works.
60

The OP did not specify the Running Environment, but if you are using Node.JS there is a very simple way to do this.

According to the official Node.JS docs: https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings

// This step is only necessary if you don't already have a Buffer Object
const buffer = Buffer.from(yourArrayBuffer);

const base64String = buffer.toString('base64');

Also, if you are running under Angular for example, the Buffer class will also be made available in a browser environment.

3 Comments

Your answer only applies to NodeJS and will not work in the browser.
@jvatic I see, the OP did not clearly specify the Running Environment, so my answer is not incorrect, he only tagged Javascript. So I updated my answer to make it more concise. I think this is an important answer because I was searching how to do this and could not get to the best answer to the problem.
I came to realize recently that the Question date precedes NodeJS itself, which is more reason that the Mods should add an addendum to the question, because these days most people are looking for a solution on nodeJS and get mislead by the popularity of the old answers.
43

There is another asynchronous way use Blob and FileReader.

I didn't test the performance. But it is a different way of thinking.

function arrayBufferToBase64( buffer, callback ) {
    var blob = new Blob([buffer],{type:'application/octet-binary'});
    var reader = new FileReader();
    reader.onload = function(evt){
        var dataurl = evt.target.result;
        callback(dataurl.substr(dataurl.indexOf(',')+1));
    };
    reader.readAsDataURL(blob);
}

//example:
var buf = new Uint8Array([11,22,33]);
arrayBufferToBase64(buf, console.log.bind(console)); //"CxYh"

5 Comments

Use dataurl.split(',', 2)[1]instead of dataurl.substr(dataurl.indexOf(',')+1).
This doesn't seem to be guaranteed to work. According to w3c.github.io/FileAPI/#issue-f80bda5b readAsDataURL could theoretically return a percent encoded dataURI (And it seems it is actually the case in jsdom)
@CarterMedlin Why would split be better than substring?
split is shorter. but dataurl may contains one or more commas(,), split is not safe.
Additional note on the warning @TS gave: NodeJS is the engine which, for certain inputs, will return percent-encoded strings rather than base64. So as long as your code is running only in browsers, it appears the method above will always return a base64-encoded string, currently. (although the linked issue shows that the W3C spec does not guarantee this as of now)
41

This example uses the built-in FileReader readDataURL() to do the conversion to base64 encoding. Data URLs are structured data:[<mediatype>][;base64],<data>, so we split that url at the comma and return only the base64 encoded characters.

const blob = new Blob([array]);        
const reader = new FileReader();

reader.onload = (event) => {
  const dataUrl = event.target.result;
  const [_, base64] = dataUrl.split(','); 
  // do something with base64
};
   
reader.readAsDataURL(blob);

Or as a promisified utility:

async function encode(array) {
  return new Promise((resolve) => {
    const blob = new Blob([array]);
    const reader = new FileReader();
    
    reader.onload = (event) => {
      const dataUrl = event.target.result;
      const [_, base64] = dataUrl.split(',');
      
      resolve(base64);
    };
    
    reader.readAsDataURL(blob);
  });
}

const encoded = await encode(typedArray);

7 Comments

Add some explanation to your answer please. What does this code mean?
this is by far the fastest approach - tens of times faster than the other ones in my limited testing
i wish i'd found this solution like 8 hours again. my day would not have been wasted ;( thank you
I think you also need to remove the DataURL header (data:*/*;base64,) to obtain just the Base64 string. See MDN docs
|
28

I used this and works for me.

function arrayBufferToBase64( buffer ) {
    var binary = '';
    var bytes = new Uint8Array( buffer );
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode( bytes[ i ] );
    }
    return window.btoa( binary );
}



function base64ToArrayBuffer(base64) {
    var binary_string =  window.atob(base64);
    var len = binary_string.length;
    var bytes = new Uint8Array( len );
    for (var i = 0; i < len; i++)        {
        bytes[i] = binary_string.charCodeAt(i);
    }
    return bytes.buffer;
}

2 Comments

Not safe. See @chemoish answer
It is safe, because all characters of the string are in the range [0–255]. See this comment for a more detailed explanation.
17

My recommendation for this is to NOT use native btoa strategies—as they don't correctly encode all ArrayBuffer's…

rewrite the DOMs atob() and btoa()

Since DOMStrings are 16-bit-encoded strings, in most browsers calling window.btoa on a Unicode string will cause a Character Out Of Range exception if a character exceeds the range of a 8-bit ASCII-encoded character.

While I have never encountered this exact error, I have found that many of the ArrayBuffer's I have tried to encode have encoded incorrectly.

I would either use MDN recommendation or gist.

4 Comments

btoa not works on String, but OP is asking ArrayBuffer.
Very much this, so many snippets here that recommend the wrong thing! I've seen this error multiple times, where people blindly use atob and btoa.
All array buffers should be encoded fine using the strategies in other answers, atob/btoa is only a problem for text that contains characters greater than 0xFF (which byte arrays by definition do not). The MDN warning doesn't apply because when using the strategy in the other answers you are guaranteed to have a string that only consists of ASCII characters as any value from a Uint8Array is guaranteed to be between 0 and 255 which means String.fromCharCode is guaranteed to return a character that is not out of range.
This is the correct answer when btoa or Buffer are not available (react-native)
15

Below are 2 simple functions for converting Uint8Array to Base64 String and back again

arrayToBase64String(a) {
    return btoa(String.fromCharCode(...a));
}

base64StringToArray(s) {
    let asciiString = atob(s);
    return new Uint8Array([...asciiString].map(char => char.charCodeAt(0)));
}

4 Comments

This is a confusing answer. That does not look look like valid JavaScript and is a Uint8Array an ArrayBuffer?
@user1153660 Add the function keyword and it should work in a modern browser.
Awesome! btoa(String.fromCharCode(...a)); is shortest version I have seen so far to encode Uint8Array.
This looks good but if the array is too huge it will throw maximum call stack size exceeded error.
6

In Node.js (i.e. not browser):

Buffer.from(myArrayBuffer).toString("base64");

Comments

5

If you're okay with adding a library, base64-arraybuffer:

yarn add base64-arraybuffer

then:

  • encode(buffer) - Encodes ArrayBuffer into base64 string
  • decode(str) - Decodes base64 string to ArrayBuffer

1 Comment

best answer for me since includes the decoding
2

Native method Uint8Array.prototype.toBase64 is now available!

Uint8Array.prototype.toBase64 has been implemented on most major browsers (see caniuse) and can be used as follows:

const arr = new Uint8Array([ 73, 32, 108, 111, 118, 101, 32, 121, 111, 117 ]);
const str = arr.toBase64();

console.log(str); // SSBsb3ZlIHlvdQ==

It's worth noting that since .toBase64 is a natively implemented function, it's incredibly fast (in fact, much faster than any of the top-voted answers).


You can also use this core-js polyfill:

const arr = new Uint8Array([ 73, 32, 108, 111, 118, 101, 32, 121, 111, 117 ]);
const base64 = arr.toBase64();

console.log(base64);
<script src="https://unpkg.com/[email protected]/minified.js"></script>

Comments

1
ABtoB64(ab) {
    return new Promise(res => {
        const fr = new FileReader();
        fr.onload = ({target: {result: s}}) => res(s.slice(s.indexOf(';base64,') + 8));
        fr.readAsDataURL(new Blob([ab]));
    });
}

asynchronous method using file reader.

Comments

1

i use TextDecode api to convert it to normal text and then convert it to Base64

const uint =  new Uint8Array([ 73, 32, 108, 111, 118, 101, 32, 121, 111, 117 ]).buffer
const decoder = new TextDecoder()
const decodedText = decoder.decode(uint)
const base64Code = btoa(decodedText)

3 Comments

Works great! One-line version: btoa(new TextDecoder().decode(myUint8Array)) Don't know why this isn't rated higher; it's just like the answer that uses String.fromCharCode(...myUint8Array), except without the stack-overflow concern (afaik).
Ah, I perhaps see why some don't prefer this route: TextDecoder works for characters 0-127 (those in the Latin1 set), but errors for array values between 128-255. So the other answer does have wider compatibility, if you think you may encounter array-values outside the 0-127 (Latin1) range. (actually, this one is maybe better since avoids the stack-overflow issue that the other can hit; or perhaps this one, though has negative of being async)
FWIW: MDN is wrapping this up quite properly.
1

In the Browser suggested solutions with btoa seem fine. But in Node.js btoa is Legacy

It is recommended to use buffer.toString(encoding)

like

const myString = buffer.toString("base64")

Comments

1

Here is a ES6 solution that is 3 times faster than using the native btoa function and is 1.25 faster than the non-native solution proposed by mobz and Emmanuel. Its code also respect more recent good coding practices and uses way more clean binary masks for readability :

    /**
     * @param {ArrayBuffer} buffer
     * @return {string}
     */
    function toBase64(buffer) {
        const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

        const byteLength = buffer.byteLength;
        const bufferView = new Uint8Array(buffer);
        const remainingBytesCount = byteLength % 3;
        const mainLength = byteLength - remainingBytesCount;

        let string = "";
        let i = 0;

        for (; i < mainLength; i += 3) {
            const chunk = (bufferView[i] << 16) | (bufferView[i + 1] << 8) | bufferView[i + 2];
            string += base64Chars[(chunk & 0b111111000000000000000000) >> 18];
            string += base64Chars[(chunk & 0b000000111111000000000000) >> 12];
            string += base64Chars[(chunk & 0b000000000000111111000000) >> 6];
            string += base64Chars[(chunk & 0b000000000000000000111111)];
        }

        if (remainingBytesCount === 2) {
            const chunk = (bufferView[i] << 16) | (bufferView[i + 1] << 8);
            string += base64Chars[(chunk & 0b111111000000000000000000) >> 18];
            string += base64Chars[(chunk & 0b000000111111000000000000) >> 12];
            string += base64Chars[(chunk & 0b000000000000111111000000) >> 6];
            string += "=";

        } else if (remainingBytesCount === 1) {
            const chunk = (bufferView[i] << 16);
            string += base64Chars[(chunk & 0b111111000000000000000000) >> 18];
            string += base64Chars[(chunk & 0b000000111111000000000000) >> 12];
            string += "==";
        }

        return string;
    }

3 Comments

Could you explain why it's faster than other non-native solutions?
Unlike most other non-native solutions, this solution does not create any temporary array, string or objects (like FileReader). It is also as straight forward as possible. Thus, it can be highly optimised by the JS engine. But the main purpose of my contribution here is to provide a cleaner and up-to-date version of the retained solution and avid web site redirection.
Please remember that with javascript speed questions, testing is more important than making theories. JS engines have sophisticated optimisation process that can be very surprising. I you find a faster (during tests) solution, please tell me.
0

You can derive a normal array from the ArrayBuffer by using Array.prototype.slice. Use a function like Array.prototype.map to convert bytes in to characters and join them together to forma string.

function arrayBufferToBase64(ab){

    var dView = new Uint8Array(ab);   //Get a byte view        

    var arr = Array.prototype.slice.call(dView); //Create a normal array        

    var arr1 = arr.map(function(item){        
      return String.fromCharCode(item);    //Convert
    });

    return window.btoa(arr1.join(''));   //Form a string

}

This method is faster since there are no string concatenations running in it.

1 Comment

Not safe. See @chemoish answer
0
 var uint8Array = new Uint8Array(BytesArray);

 var base64Str = btoa(String.fromCharCode(...uint8Array));

 Or,

 var base64Str = btoa(uint8Array.reduce((x, y) => x + String.fromCharCode(y), ''));

Comments

-1

Use uint8-to-b64 package to do encoding/decoding in browser and Node.js

Comments

-4

By my side, using Chrome navigator, I had to use DataView() to read an arrayBuffer

function _arrayBufferToBase64( tabU8A ) {
var binary = '';
let lecteur_de_donnees = new DataView(tabU8A);
var len = lecteur_de_donnees.byteLength;
var chaine = '';
var pos1;
for (var i = 0; i < len; i++) {
    binary += String.fromCharCode( lecteur_de_donnees.getUint8( i ) );
}
chaine = window.btoa( binary )
return chaine;}

Comments

-6
function _arrayBufferToBase64(uarr) {
    var strings = [], chunksize = 0xffff;
    var len = uarr.length;

    for (var i = 0; i * chunksize < len; i++){
        strings.push(String.fromCharCode.apply(null, uarr.subarray(i * chunksize, (i + 1) * chunksize)));
    }

    return strings.join("");
}

This is better, if you use JSZip for unpack archive from string

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.