Convert a JavaScript Blob to a Base64 String
APIs don't always give you data in the most useful format. I'll show you how to turn a binary JavaScript blob into a usable Base64 data URL.
Sometimes APIs return binary data. With the JavaScript Fetch API, this means that you're getting back a Blob
. A Blob
is a JavaScript object that encapsulates binary data along with some metadata like size and type. Here's an example using the back-end from one of my projects.
This blob contains the binary data for a PNG image. I'd like to use this image as an image.src
attribute, but the HTML image
element doesn't understand blobs. But it does take data URLs.
A Data URL is a string that contains type information and binary data encoded as Base64. Base64 is a URL-safe format for encoding arbitrary binary data. You can read more about this format here.
Our Blob
already has everything we need: binary data and type information. It's just not formatted correctly. Let's write a function to convert our blob into a Base64 Data URL so we can display our image.
Using the JavaScript FileReader
This JS interface lets us convert binary data to Base64
The FileReader
is an object that we can use to convert our blob into a Base64 string. There's a member function that looks like it does exactly what we want: FileReader.readAsDataURL()
. This method even takes a Blob
as an argument!
🤯 What's going on here? When we create a FileReader
and run the readAsDataURL
method on our blob, the it returns undefined
.
If we look at the documentation for FileReader, we see that this function doesn't return anything. The way to get the result is to listen to Events
that are dispatched from FileReader
. But what are events? The Events API is one of the main ways that JavaScript facilitates asynchronous operation. Instead of returning, the readAsDataURL
function dispatches and Event
object that we need to listen for.
So how do we listen to these events? The FileReader has a loadend
event that we can use. We provide a callback function, and that function is called whenever the loadend
event is dispatched. In our callback function, we get an Event
object that has an attribute target
from which we can get the result. The target
is the object that dispatched the event. In our case, it should be our original FileReader object.
Let's see test it out. We can register a simple callback to print the result.
Now we can see that our callback was called after we called readAsDataURL
once the result was ready. We can see that a Base64 string was printed.
This interface isn't ideal though. It would be nice to call a function and just get our Base64 result inline. Because of JavaScript's asynchronous nature, we're going to need to learn a little about async
functions before we can clean this interface up.
Asynchronous Functions and the Promise Interface
One way to write asynchronous code in JavaScript is to use a Promise
. A promise is just an object that promises to return a result later. It let's us write some code that uses promises.
let promise = new Promise((resolve, error) => {
try {
resolve("The Result");
} catch(e) {
error("Something bad happened: " + e);
}
})
promise.then((r) => console.log(r));
This code looks complicated, but it doesn't do much. It just prints "The Result"
. The key here is that the callback that we pass to then
won't get called until our promise resolves.
JavaScript has always been asynchronous by nature, but ES6 introduced new keywords async
and await
that allow us to be more explicit when we write asynchronous code.
The async
keyword allows us to create functions that return promises, but we don't have to explicitly create any promises. Let's rewrite the above example using the async
keyword.
async function returnResult() {
return "The Result";
}
let promise = returnResult();
promise.then((r) => console.log(r));
This code does the same thing as the promise example, but it's better. It's way less code and clearer to read.
That last line where we use then
is still a little ugly. The then
syntax gets even more complicated when you have multiple promises to wait on. Your code can become difficult to read with lots of nested then
statements. Let's clean it up with await
.
The await
keyword allows us to wait for the result of a promise inline without passing around callback functions. We can modify the above code to use await
like this.
let promise = returnResult();
console.log(await promise);
Or even just this.
console.log(await returnResult());
We don't need to use async
to make a function that is awaitable. We can also simply return a promise.
function returnResult() {
return new Promise((resolve, _) => {
resolve("The Result");
});
}
console.log(await returnResult());
Now we have all of the pieces we need write our blobToBase64
function.
Pulling It All Together
Let's use what we know about the FileReader, Events, and Promises to write one async function
With what we've learned about Promises, we can make a more usable interface where we don't need to listen to FileReader's events. We want to pass in our Blob
and get back a string encoded in Base64, and we don't want to deal with instantiating a FileReader object and listening to events.
Our goal is to write a function that returns a Promise
which resolves when FileReader's loadend
event fires. Let's start by scaffolding out our function and creating the FileReader
.
function blobToBase64(blob) {
const reader = new FileReader();
return new Promise((resolve, _) => {
// In here, we'll need to use the onloadend
// callback to resolve our Promise
});
// Pass the Blob to FileReader so onloadend can be triggered
}
Now that we have our scaffolding, let's focus on resolving that promise. We know we need to use the onloadend
callback. When we get the result from the loadend
event, that's the resolution of our promise.
function blobToBase64(blob) {
const reader = new FileReader();
return new Promise((resolve, _) => {
reader.onloadend = (event) => {
const result = event.target.result
resolve(result.replace(/^data:.+;base64,/, ''))
}
// Pass the Blob to FileReader so onloadend can be triggered
});
}
Now that we have the FileReader
set up and the Promise
set to resolve whenever onloadend
is dispatched, we just need to pass the blob to our reader.
function blobToBase64(blob) {
const reader = new FileReader();
return new Promise((resolve, _) => {
reader.onloadend = (event) => {
const result = event.target.result
resolve(result.replace(/^data:.+;base64,/, ''))
}
reader.readAsDataURL(blob);
});
}
That's it! We have our one function. We can call it like this.
blob = await response.blob();
base64String = await blobToBase64(blob);
We did it 🥳