en.javascript.info/6-binary/03-blob/article.md
2019-03-17 20:05:02 +03:00

8.4 KiB

Blob

ArrayBuffer and views are a part of ECMA standard, a part of Javascript.

In the browser, there are additional higher-level objects, described in File API.

Blob consists of an optional string type (a MIME-type usually), plus blobParts this a sequence of other Blob objects, strings and BufferSources.

Thanks to type, we can download/upload blobs, and it naturally becomes Content-Type in network requests.

The constructor syntax is:

new Blob(blobParts, options);
  • blobParts is an array of Blob/BufferSource/String values.
  • options optional object:
    • type -- blob type, usually MIME-type, e.g. image/png,
    • endings -- whether to transform end-of-line \r\n to \n end vise versa, to make the blob correspond to current OS newlines. By default "transparent" (do nothing), but also can be "native" (transform).

Then we can access blob as a whole, or extract slices with:

blob.slice([byteStart], [byteEnd], [contentType]);
  • byteStart -- the starting byte, by default 0.
  • byteEnd -- the last byte (exclusive, by default till the end).
  • contentType -- the type of the new blob, by default the same as the source.

The arguments are similar to array.slice, negative numbers are allowed too.

We can't change data directly in a blob, but we can copy parts of blobs, create new blobs from them, mix them and so on. This behavior is similar to Javascript strings: we can't change a character in a string, but we can make a new corrected string.

Blob as URL

A Blob can be easily used in an URL for <a>, <img> or other tags.

Let's start with a simple example. By clicking on a link you download a dynamically-generated blob with hello world contents as a file:

<!-- download attribute forces the browser to download instead of navigating -->
<a download="hello.txt" href='#' id="link">Download</a>

<script>
let binary = new Uint8Array([72, 101, 108, 108, 111]); // "hello" in binary form

// create blob of multiple parts
let blob = new Blob([binary, ' ', 'world'], {type: 'text/plain'});

link.href = URL.createObjectURL(blob);
</script>

We can also create a link in Javascript and simulate a click by link.click(), then download starts authomatically.

Here's the similar "on the fly" blob creation and download code, now without HTML:

let link = document.createElement('a');
link.download = 'hello.txt';

let blob = new Blob(['Hello, world!'], {type: 'text/plain'});

link.href = URL.createObjectURL(blob);

link.click();

URL.revokeObjectURL(link.href);

URL.createObjectURL takes a blob and creates an unique URL for it, in the form blob:<origin>/<uuid>.

That's what the generated url looks like:

blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273

The browser keeps an internal url -> blob mapping. For each url generated by URL.createObjectURL, it stores a reference to the corresponding blob.

A generated url is only valid while the current document is open. And it allows to reference the blob in <img>, <a>, any other object that expects an URL.

There's a side-effect though. While there's an mapping for a blob, the blob itself resides in the memory. The browser can't free it.

The mapping is automatically cleared on document unload, so blobs are freed then. But if an app is long-living, then that doesn't happen. So if we create an URL, that blob will hang in memory, even if not needed any more.

URL.revokeObjectURL(url) removes the reference from the internal mapping, thus allowing the blob to be deleted (if there are no other references), and the memory to be freed.

In the last example, we intend the blob to be used only once, for instant downloading, so we call URL.revokeObjectURL(link.href) immediately.

In the previous example though, with the clickable HTML-link, we dont't call URL.revokeObjectURL(link.href), because that would make the blob url invalid. After the revocation, as the mapping is removed, the url doesn't work any more.

Blob to base64

An alternative to URL.createObjectURL is to convert a blob into a base64-encoded string.

That encoding represents binary data as a string of ultra-safe "readable" characters with ASCII-codes from 0 to 64. And what's more important -- we can use this encoding in "data-urls".

A data url has the form data:[<mediatype>][;base64],<data>. We can use such urls everywhere, on par with "regular" urls.

For instance, here's a smiley:

<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">

The browser will decode the string and show the image:

To transform a blob into base64 and vise-versa, we'll use built-in FileReader/FileWriter objects. In the next chapter we'll cover those more in-detail. For now, it's important only that they can read a blob as data url, that we can assign it to link.href.

Here's the demo of downloading a blob, now via base-64:

let link = document.createElement('a');
link.download = 'hello.txt';

let blob = new Blob(['Hello, world!'], {type: 'text/plain'});

*!*
let reader = new FileReader();
*/!*

reader.onload = function() {
  link.href = reader.result; // data url
  link.click();
};

*!*
reader.readAsDataURL(blob); // converts the blob to base64 and calls onload
*/!*

Both ways of making an URL of a blob are usable. But usually URL.createObjectURL(blob) is simpler and faster.

+ We need to revoke them if care about memory.
+ Direct access to blob, no "encoding/decoding"
- No need to revoke anything.
- Performance and memory losses on big blobs for encoding.

Image or a page to blob

We can create a blob of an image, or even a page screenshot.

That's usually done via <canvas> element:

  1. We draw an existing image on canvas using canvas.drawImage.
  2. Canvas .toBlob(callback, format, quality) creates a blob and calls callback with it.

In the example below, an image is just copied, but we could cut from it, or transform it on canvas prior to making a blob:

let img = document.querySelector('img'); // just any image

let canvas = document.createElement('canvas');
canvas.width = img.clientWidth;
canvas.height = img.clientHeight;

let context = canvas.getContext('2d');

context.drawImage(img, 0, 0); // allows to cut image
// we can context.rotate() as well, and do many other things

canvas.toBlob(function(blob) {
  // download the blob
  let link = document.createElement('a');
  link.download = 'example.png';
  link.href = URL.createObjectURL(blob);
  link.click();

  URL.revokeObjectURL(link.href);
}, 'image/png');

For screenshotting a page, we can use a library such as https://github.com/niklasvh/html2canvas to draw the screenshot on <canvas>, and then download it the same way.

From Blob to ArrayBuffer

The Blob constructor allows to create a blob from almost anything.

But we can also revert back to lowest-level ArrayBuffer using FileReader:

// get arrayBuffer from blob
let fileReader = new FileReader();
fileReader.onload = function(event) {
    let arrayBuffer = fileReader.result;
};
*!*
fileReader.readAsArrayBuffer(blob);
*/!*

Summary

While ArrayBuffer, Uint8Array and other BufferSource are "binary data", a Blob represents "binary data with type".

That makes Blobs convenient for upload/download operations, that are so common in the browser.

Methods that perform web-requests, such as XMLHttpRequest, fetch and so on, can work with Blob natively, as well as with other binary types.

That said, it's still possible to convert betweeen Blob and low-level binary data types:

  • We can make a Blob from a typed array using new Blob(...) constructor.
  • We can get back ArrayBuffer from a Blob using FileReader, and then create a view over it for low-level binary processing.