components
|
@ -0,0 +1,18 @@
|
|||
function concat(arrays) {
|
||||
// sum of individual array lengths
|
||||
let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
|
||||
|
||||
if (!arrays.length) return null;
|
||||
|
||||
let result = new Uint8Array(totalLength);
|
||||
|
||||
// for each array - copy it over result
|
||||
// next array is copied right after the previous one
|
||||
let length = 0;
|
||||
for(let array of arrays) {
|
||||
result.set(array, length);
|
||||
length += array.length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
function concat(arrays) {
|
||||
// ...your code...
|
||||
}
|
||||
|
||||
let chunks = [
|
||||
new Uint8Array([0, 1, 2]),
|
||||
new Uint8Array([3, 4, 5]),
|
||||
new Uint8Array([6, 7, 8])
|
||||
];
|
||||
|
||||
console.log(Array.from(concat(chunks))); // 0, 1, 2, 3, 4, 5, 6, 7, 8
|
||||
|
||||
console.log(concat(chunks).constructor.name); // Uint8Array
|
|
@ -0,0 +1,31 @@
|
|||
describe("concat", function() {
|
||||
let chunks = [
|
||||
new Uint8Array([0, 1, 2]),
|
||||
new Uint8Array([3, 4, 5]),
|
||||
new Uint8Array([6, 7, 8])
|
||||
];
|
||||
|
||||
it("result has the same array type", function() {
|
||||
|
||||
let result = concat(chunks);
|
||||
|
||||
assert.equal(result.constructor, Uint8Array);
|
||||
});
|
||||
|
||||
it("concatenates arrays", function() {
|
||||
|
||||
let result = concat(chunks);
|
||||
|
||||
assert.deepEqual(result, new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8]));
|
||||
|
||||
});
|
||||
|
||||
it("returns empty array on empty input", function() {
|
||||
|
||||
let result = concat([]);
|
||||
|
||||
assert.equal(result.length, 0);
|
||||
|
||||
});
|
||||
|
||||
});
|
4
4-binary/01-arraybuffer-binary-arrays/01-concat/task.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
# Concatenate typed arrays
|
||||
|
||||
Given an array of `Uint8Array`, write a function `concat(arrays)` that returns a concatenation of them into a single array.
|
BIN
4-binary/01-arraybuffer-binary-arrays/8bit-integer-256.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
4-binary/01-arraybuffer-binary-arrays/8bit-integer-256@2x.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
4-binary/01-arraybuffer-binary-arrays/8bit-integer-257.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
4-binary/01-arraybuffer-binary-arrays/8bit-integer-257@2x.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 99 KiB |
BIN
4-binary/01-arraybuffer-binary-arrays/arraybuffer-views.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
4-binary/01-arraybuffer-binary-arrays/arraybuffer-views@2x.png
Normal file
After Width: | Height: | Size: 40 KiB |
269
4-binary/01-arraybuffer-binary-arrays/article.md
Normal file
|
@ -0,0 +1,269 @@
|
|||
# ArrayBuffer, binary arrays
|
||||
|
||||
In web-development we meet binary data mostly while dealing with files (create, upload, download). Another typical use case is image processing.
|
||||
|
||||
That's all possible in Javascript, and binary operations are high-performant.
|
||||
|
||||
Although, there's a bit of confusion, because there are many classes. To name a few:
|
||||
- `ArrayBuffer`, `Uint8Array`, `DataView`, `Blob`, `File`, etc.
|
||||
|
||||
Binary data in Javascript is implemented in a non-standard way, compared to other languages. But when we sort things out, everything becomes fairly simple.
|
||||
|
||||
**The basic binary object is `ArrayBuffer` -- a reference to a fixed-length contiguos memory area.**
|
||||
|
||||
We create it like this:
|
||||
```js run
|
||||
let buffer = new ArrayBuffer(16); // create a buffer of length 16
|
||||
alert(buffer.byteLength); // 16
|
||||
```
|
||||
|
||||
This allocates a contiguous memory area of 16 bytes and pre-fills it with zeroes.
|
||||
|
||||
```warn header="`ArrayBuffer` is not an array of something"
|
||||
Let's eliminate a possible source of confusion. `ArrayBuffer` has nothing in common with `Array`:
|
||||
- It has a fixed length, we can't increase or decrease it.
|
||||
- It takes exactly that much space in the memory.
|
||||
- To access individual bytes, another "view" object is needed, not `buffer[index]`.
|
||||
```
|
||||
|
||||
`ArrayBuffer` is a memory area. What's stored in it? It has no clue. Just a raw sequence of bytes.
|
||||
|
||||
**To manipulate an `ArrayBuffer`, we need to use a "view" object.**
|
||||
|
||||
A view object does not store anything on it's own. It's the "eyeglasses" that give an interpretation of the bytes stored in the `ArrayBuffer`.
|
||||
|
||||
For instance:
|
||||
|
||||
- **`Uint8Array`** -- treats each byte in `ArrayBuffer` as a separate number, with possible values are from 0 to 255 (a byte is 8-bit, so it can hold only that much). Such value is called a "8-bit unsigned integer".
|
||||
- **`Uint16Array`** -- treats every 2 bytes as an integer, with possible values from 0 to 65535. That's called a "16-bit unsigned integer".
|
||||
- **`Uint32Array`** -- treats every 4 bytes as an integer, with possible values from 0 to 4294967295. That's called a "32-bit unsigned integer".
|
||||
- **`Float64Array`** -- treats every 8 bytes as a floating point number with possible values from <code>5.0x10<sup>-324</sup></code> to <code>1.8x10<sup>308</sup></code>.
|
||||
|
||||
So, the binary data in an `ArrayBuffer` of 16 bytes can be interpreted as 16 "tiny numbers", or 8 bigger numbers (2 bytes each), or 4 even bigger (4 bytes each), or 2 floating-point values with high precision (8 bytes each).
|
||||
|
||||

|
||||
|
||||
`ArrayBuffer` is the core object, the root of everything, the raw binary data.
|
||||
|
||||
But if we're going to write into it, or iterate over it, basically for almost any operation – we must use a view, e.g:
|
||||
|
||||
```js run
|
||||
let buffer = new ArrayBuffer(16); // create a buffer of length 16
|
||||
|
||||
*!*
|
||||
let view = new Uint32Array(buffer); // treat buffer as a sequence of 32-bit integers
|
||||
|
||||
alert(Uint32Array.BYTES_PER_ELEMENT); // 4 bytes per integer
|
||||
*/!*
|
||||
|
||||
alert(view.length); // 4, it stores that many integers
|
||||
alert(view.byteLength); // 16, the size in bytes
|
||||
|
||||
// let's write a value
|
||||
view[0] = 123456;
|
||||
|
||||
// iterate over values
|
||||
for(let num of view) {
|
||||
alert(num); // 123456, then 0, 0, 0 (4 values total)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## TypedArray
|
||||
|
||||
The common term for all these views (`Uint8Array`, `Uint32Array`, etc) is [TypedArray](https://tc39.github.io/ecma262/#sec-typedarray-objects). They share the same set of methods and properities.
|
||||
|
||||
They are much more like regular arrays: have indexes and iterable.
|
||||
|
||||
|
||||
A typed array constructor (be it `Int8Array` or `Float64Array`, doesn't matter) behaves differently depending on argument types.
|
||||
|
||||
There are 5 variants of arguments:
|
||||
|
||||
```js
|
||||
new TypedArray(buffer, [byteOffset], [length]);
|
||||
new TypedArray(object);
|
||||
new TypedArray(typedArray);
|
||||
new TypedArray(length);
|
||||
new TypedArray();
|
||||
```
|
||||
|
||||
1. If an `ArrayBuffer` argument is supplied, the view is created over it. We used that syntax already.
|
||||
|
||||
Optionally we can provide `byteOffset` to start from (0 by default) and the `length` (till the end of the buffer by default), then the view will cover only a part of the `buffer`.
|
||||
|
||||
2. If an `Array`, or any array-like object is given, it creates a typed array of the same length and copies the content.
|
||||
|
||||
We can use it to pre-fill the array with the data:
|
||||
```js run
|
||||
*!*
|
||||
let arr = new Uint8Array([0, 1, 2, 3]);
|
||||
*/!*
|
||||
alert( arr.length ); // 4
|
||||
alert( arr[1] ); // 1
|
||||
```
|
||||
3. If another `TypedArray` is supplied, it does the same: creates a typed array of the same length and copies values. Values are converted to the new type in the process.
|
||||
```js run
|
||||
let arr16 = new Uint16Array([1, 1000]);
|
||||
*!*
|
||||
let arr8 = new Uint8Array(arr16);
|
||||
*/!*
|
||||
alert( arr8[0] ); // 1
|
||||
alert( arr8[1] ); // 232 (tried to copy 1000, but can't fit 1000 into 8 bits)
|
||||
```
|
||||
|
||||
4. For a numeric argument `length` -- creates the typed array to contain that many elements. Its byte length will be `length` multiplied by the number of bytes in a single item `TypedArray.BYTES_PER_ELEMENT`:
|
||||
```js run
|
||||
let arr = new Uint16Array(4); // create typed array for 4 integers
|
||||
alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per integer
|
||||
alert( arr.byteLength ); // 8 (size in bytes)
|
||||
```
|
||||
|
||||
5. Without arguments, creates an zero-length typed array.
|
||||
|
||||
We can create a `TypedArray` directly, without mentioning `ArrayBuffer`. But a view cannot exist without an underlying `ArrayBuffer`, so gets created automatically in all these cases except the first one (when provided).
|
||||
|
||||
To access the `ArrayBuffer`, there are properties:
|
||||
- `arr.buffer` -- references the `ArrayBuffer`.
|
||||
- `arr.byteLength` -- the length of the `ArrayBuffer`.
|
||||
|
||||
So, we can always move from one view to another:
|
||||
```js
|
||||
let arr8 = new Uint8Array([0, 1, 2, 3]);
|
||||
|
||||
// another view on the same data
|
||||
let arr16 = new Uint16Array(arr8.buffer);
|
||||
```
|
||||
|
||||
|
||||
Here's the list of typed arrays:
|
||||
|
||||
- `Uint8Array`, `Uint16Array`, `Uint32Array` -- for integer numbers of 8, 16 and 32 bits.
|
||||
- `Uint8ClampedArray` -- for 8-bit integers, "clamps" them on assignment (see below).
|
||||
- `Int8Array`, `Int16Array`, `Int32Array` -- for signed integer numbers (can be negative).
|
||||
- `Float32Array`, `Float64Array` -- for signed floating-point numbers of 32 and 64 bits.
|
||||
|
||||
```warn header="No `int8` or similar single-valued types"
|
||||
Please note, despite of the names like `Int8Array`, there's no single-value type like `int`, or `int8` in Javascript.
|
||||
|
||||
That's logical, as `Int8Array` is not an array of these individual values, but rather a view on `ArrayBuffer`.
|
||||
```
|
||||
|
||||
### Out-of-bounds behavior
|
||||
|
||||
What if we attempt to write an out-of-bounds value into a typed array? There will be no error. But extra bits are cut-off.
|
||||
|
||||
For instance, let's try to put 256 into `Uint8Array`. In binary form, 256 is `100000000` (9 bits), but `Uint8Array` only provides 8 bits per value, that makes the available range from 0 to 255.
|
||||
|
||||
For bigger numbers, only the rightmost (less significant) 8 bits are stored, and the rest is cut off:
|
||||
|
||||

|
||||
|
||||
So we'll get zero.
|
||||
|
||||
For 257, the binary form is `100000001` (9 bits), the rightmost 8 get stored, so we'll have `1` in the array:
|
||||
|
||||

|
||||
|
||||
In other words, the number modulo 2<sup>8</sup> is saved.
|
||||
|
||||
Here's the demo:
|
||||
|
||||
```js run
|
||||
let uint8array = new Uint8Array(16);
|
||||
|
||||
let num = 256;
|
||||
alert(num.toString(2)); // 100000000 (binary representation)
|
||||
|
||||
uint8array[0] = 256;
|
||||
uint8array[1] = 257;
|
||||
|
||||
alert(uint8array[0]); // 0
|
||||
alert(uint8array[1]); // 1
|
||||
```
|
||||
|
||||
`Uint8ClampedArray` is special in this aspect, its behavior is different. It saves 255 for any number that is greater than 255, and 0 for any negative number. That behavior is useful for image processing.
|
||||
|
||||
## TypedArray methods
|
||||
|
||||
`TypedArray` has regular `Array` methods, with notable exceptions.
|
||||
|
||||
We can iterate, `map`, `slice`, `find`, `reduce` etc.
|
||||
|
||||
There are few things we can't do though:
|
||||
|
||||
- No `splice` -- we can't "delete" a value, because typed arrays are views on a buffer, and these are fixed, contiguous areas of memory. All we can do is to assign a zero.
|
||||
- No `concat` method.
|
||||
|
||||
There are two additional methods:
|
||||
|
||||
- `arr.set(fromArr, [offset])` copies all elements from `fromArr` to the `arr`, starting at position `offset` (0 by default).
|
||||
- `arr.subarray([begin, end])` creates a new view of the same type from `begin` to `end` (exclusive). That's similar to `slice` method (that's also supported), but doesn't copy anything -- just creates a new view, to operate on the given piece of data.
|
||||
|
||||
These methods allow us to copy typed arrays, mix them, create new arrays from existing ones, and so on.
|
||||
|
||||
|
||||
|
||||
## DataView
|
||||
|
||||
[DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) is a special super-flexible "untyped" view over `ArrayBuffer`. It allows to access the data on any offset in any format.
|
||||
|
||||
- For typed arrays, the constructor dictates what the format is. The whole array is supposed to be uniform. The i-th number is `arr[i]`.
|
||||
- With `DataView` we access the data with methods like `.getUint8(i)` or `.gteUint16(i)`. We choose the format at method call time instead of the construction time.
|
||||
|
||||
The syntax:
|
||||
|
||||
```js
|
||||
new DataView(buffer, [byteOffset], [byteLength])
|
||||
```
|
||||
|
||||
- **`buffer`** -- the underlying `ArrayBuffer`. Unlike typed arrays, `DataView` doesn't create a buffer on its own. We need to have it ready.
|
||||
- **`byteOffset`** -- the starting byte position of the view (by default 0).
|
||||
- **`byteLength`** -- the byte length of the view (by default till the end of `buffer`).
|
||||
|
||||
For instance, here we extract numbers in different formats from the same buffer:
|
||||
|
||||
```js run
|
||||
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;
|
||||
|
||||
let dataView = new DataView(buffer);
|
||||
|
||||
// get 8-bit number at offset 0
|
||||
alert( dataView.getUint8(0) ); // 255
|
||||
|
||||
// now get 16-bit number at offset 0, that's 2 bytes, both with max value
|
||||
alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int)
|
||||
|
||||
// get 32-bit number at offset 0
|
||||
alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int)
|
||||
|
||||
dataView.setUint32(0, 0); // set 4-byte number to zero
|
||||
```
|
||||
|
||||
`DataView` is great when we store mixed-format data in the same buffer. E.g we store a sequence of pairs (16-bit integer, 32-bit float). Then `DataView` allows to access them easily.
|
||||
|
||||
## Summary
|
||||
|
||||
`ArrayBuffer` is the core object, a reference to the fixed-length contiguous memory area.
|
||||
|
||||
To do almost any operation on `ArrayBuffer`, we need a view.
|
||||
|
||||
- It can be a `TypedArray`:
|
||||
- `Uint8Array`, `Uint16Array`, `Uint32Array` -- for integer numbers of 8, 16 and 32 bits.
|
||||
- `Uint8ClampedArray` -- for 8-bit integers, "clamps" them on assignment.
|
||||
- `Int8Array`, `Int16Array`, `Int32Array` -- for signed integer numbers (can be negative).
|
||||
- `Float32Array`, `Float64Array` -- for signed floating-point numbers of 32 and 64 bits.
|
||||
- Or a `DataView` -- the view that uses methods to specify a format, e.g. `getUint8(offset)`.
|
||||
|
||||
In most cases we create and operate directly on typed arrays, leaving `ArrayBuffer` under cover, as a "common discriminator". We can access it as `.buffer` and make another view if needed.
|
||||
|
||||
There are also two additional terms:
|
||||
- `ArrayBufferView` is an umbrella term for all these kinds of views.
|
||||
- `BufferSource` is an umbrella term for `ArrayBuffer` or `ArrayBufferView`.
|
||||
|
||||
These are used in descriptions of methods that operate on binary data. `BufferSource` is one of the most common teerms, as it means "any kind of binary data" -- an `ArrayBuffer` or a view over it.
|
||||
|
||||
|
||||
Here's a cheatsheet:
|
||||
|
||||

|
76
4-binary/02-text-decoder/article.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# TextDecoder and TextEncoder
|
||||
|
||||
What if the binary data is actually a string? For instance, we received a file with textual data.
|
||||
|
||||
The build-in [TextDecoder](https://encoding.spec.whatwg.org/#interface-textdecoder) object allows to read the value into an an actual Javascript string, given the buffer and the encoding.
|
||||
|
||||
We first need to create it:
|
||||
```js
|
||||
let decoder = new TextDecoder([label], [options]);
|
||||
```
|
||||
|
||||
- **`label`** -- the encoding, `utf-8` by default, but `big5`, `windows-1251` and many other are also supported.
|
||||
- **`options`** -- optional object:
|
||||
- **`fatal`** -- boolean, if `true` then throw an exception for invalid (non-decodable) characters, otherwise (default) replace them with character `\uFFFD`.
|
||||
- **`ignoreBOM`** -- boolean, if `true` then ignore BOM (an optional byte-order unicode mark), rarely needed.
|
||||
|
||||
...And then decode:
|
||||
|
||||
```js
|
||||
let str = decoder.decode([input], [options]);
|
||||
```
|
||||
|
||||
- **`input`** -- `BufferSource` to decode.
|
||||
- **`options`** -- optional object:
|
||||
- **`stream`** -- true for decoding streams, when `decoder` is called repeatedly with incoming chunks of data. In that case a multi-byte character may occasionally split between chunks. This options tells `TextDecoder` to memorize "unfinished" characters and decode them when the next chunk comes.
|
||||
|
||||
For instance:
|
||||
|
||||
```js run
|
||||
let uint8Array = new Uint8Array([72, 101, 108, 108, 111]);
|
||||
|
||||
alert( new TextDecoder().decode(uint8Array) ); // Hello
|
||||
```
|
||||
|
||||
|
||||
```js run
|
||||
let uint8Array = new Uint8Array([228, 189, 160, 229, 165, 189]);
|
||||
|
||||
alert( new TextDecoder().decode(uint8Array) ); // 你好
|
||||
```
|
||||
|
||||
We can decode a part of the buffer by creating a subarray view for it:
|
||||
|
||||
|
||||
```js run
|
||||
let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]);
|
||||
|
||||
// the string is in the middle
|
||||
// create a new view over it, without copying anything
|
||||
let binaryString = uint8Array.subarray(1, -1);
|
||||
|
||||
alert( new TextDecoder().decode(binaryString) ); // Hello
|
||||
```
|
||||
|
||||
## TextEncoder
|
||||
|
||||
[TextEncoder](https://encoding.spec.whatwg.org/#interface-textencoder) does the reverse thing -- converts a string into bytes.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js run
|
||||
let encoder = new TextEncoder();
|
||||
```
|
||||
|
||||
The only encoding it supports is "utf-8".
|
||||
|
||||
It has two methods:
|
||||
- **`encode(str)`** -- returns `Uint8Array` from a string.
|
||||
- **`encodeInto(str, destination)`** -- encodes `str` into `destination` that must be `Uint8Array`.
|
||||
|
||||
```js run
|
||||
let encoder = new TextEncoder();
|
||||
|
||||
let uint8Array = encoder.encode("Hello");
|
||||
alert(uint8Array); // 72,101,108,108,111
|
||||
```
|
239
4-binary/03-blob/article.md
Normal file
|
@ -0,0 +1,239 @@
|
|||
# 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](https://www.w3.org/TR/FileAPI/).
|
||||
|
||||
`Blob` consists of an optional string `type` (a MIME-type usually), plus `blobParts` -- 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:
|
||||
|
||||
```js
|
||||
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 to make the blob correspond to current OS newlines (`\r\n` or `\n`). By default `"transparent"` (do nothing), but also can be `"native"` (transform).
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
// create Blob from a string
|
||||
let blob = new Blob(["<html>…</html>"], {type: 'text/html'});
|
||||
// please note: the first argument must be an array [...]
|
||||
```
|
||||
|
||||
```js
|
||||
// create Blob from a typed array and strings
|
||||
let hello = new Uint8Array([72, 101, 108, 108, 111]); // "hello" in binary form
|
||||
|
||||
let blob = new Blob([hello, ' ', 'world'], {type: 'text/plain'});
|
||||
```
|
||||
|
||||
|
||||
We can extract blob slices with:
|
||||
|
||||
```js
|
||||
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.
|
||||
|
||||
```smart header="Blobs are immutable"
|
||||
We can't change data directly in a blob, but we can slice parts of blobs, create new blobs from them, mix them into a new blob 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 as an URL for `<a>`, `<img>` or other tags, to show its contents.
|
||||
|
||||
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:
|
||||
|
||||
```html run
|
||||
<!-- download attribute forces the browser to download instead of navigating -->
|
||||
<a download="hello.txt" href='#' id="link">Download</a>
|
||||
|
||||
<script>
|
||||
let blob = new Blob(["Hello, world!"], {type: 'text/plain'});
|
||||
|
||||
link.href = URL.createObjectURL(blob);
|
||||
</script>
|
||||
```
|
||||
|
||||
We can also create a link dynamically 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, but without HTML:
|
||||
|
||||
```js run
|
||||
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 for each url generated by `URL.createObjectURL` stores an the url -> blob mapping internally. So such urls are short, but allow to access the 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 soon. 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 don'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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) has the form `data:[<mediatype>][;base64],<data>`. We can use such urls everywhere, on par with "regular" urls.
|
||||
|
||||
For instance, here's a smiley:
|
||||
|
||||
```html
|
||||
<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
|
||||
```
|
||||
|
||||
The browser will decode the string and show the image: <img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
|
||||
|
||||
|
||||
To transform a blob into base64, we'll use the built-in `FileReader` object. It can read data from Blobs in multiple formats. In the [next chapter](info:file) we'll cover it more in-depth.
|
||||
|
||||
Here's the demo of downloading a blob, now via base-64:
|
||||
|
||||
```js run
|
||||
let link = document.createElement('a');
|
||||
link.download = 'hello.txt';
|
||||
|
||||
let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
|
||||
|
||||
*!*
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(blob); // converts the blob to base64 and calls onload
|
||||
*/!*
|
||||
|
||||
reader.onload = function() {
|
||||
link.href = reader.result; // data url
|
||||
link.click();
|
||||
};
|
||||
```
|
||||
|
||||
Both ways of making an URL of a blob are usable. But usually `URL.createObjectURL(blob)` is simpler and faster.
|
||||
|
||||
```compare title-plus="URL.createObjectURL(blob)" title-minus="Blob to data url"
|
||||
+ 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 to blob
|
||||
|
||||
We can create a blob of an image, an image part, or even make a page screenshot. That's handy to upload it somewhere.
|
||||
|
||||
Image operations are done via `<canvas>` element:
|
||||
|
||||
1. Draw an image (or its part) on canvas using [canvas.drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage).
|
||||
2. Call canvas method [.toBlob(callback, format, quality)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) that creates a blob and runs `callback` with it when done.
|
||||
|
||||
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:
|
||||
|
||||
```js run
|
||||
// take any image
|
||||
let img = document.querySelector('img');
|
||||
|
||||
// make <canvas> of the same size
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = img.clientWidth;
|
||||
canvas.height = img.clientHeight;
|
||||
|
||||
let context = canvas.getContext('2d');
|
||||
|
||||
// copy image to it (this method allows to cut image)
|
||||
context.drawImage(img, 0, 0);
|
||||
// we can context.rotate(), and do many other things on canvas
|
||||
|
||||
// toBlob is async opereation, callback is called when done
|
||||
canvas.toBlob(function(blob) {
|
||||
// blob ready, download it
|
||||
let link = document.createElement('a');
|
||||
link.download = 'example.png';
|
||||
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.click();
|
||||
|
||||
// delete the internal blob reference, to let the browser clear memory from it
|
||||
URL.revokeObjectURL(link.href);
|
||||
}, 'image/png');
|
||||
```
|
||||
|
||||
If we prefer `async/await` instead of callbacks:
|
||||
```js
|
||||
let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
|
||||
```
|
||||
|
||||
For screenshotting a page, we can use a library such as <https://github.com/niklasvh/html2canvas>. What it does is just walks the page and draws it on `<canvas>`. Then we can get a blob of it the same way as above.
|
||||
|
||||
## From Blob to ArrayBuffer
|
||||
|
||||
The `Blob` constructor allows to create a blob from almost anything, including any `BufferSource`.
|
||||
|
||||
But if we need to perform low-level processing, we can get the lowest-level `ArrayBuffer` from it using `FileReader`:
|
||||
|
||||
```js
|
||||
// get arrayBuffer from blob
|
||||
let fileReader = new FileReader();
|
||||
|
||||
*!*
|
||||
fileReader.readAsArrayBuffer(blob);
|
||||
*/!*
|
||||
|
||||
fileReader.onload = function(event) {
|
||||
let arrayBuffer = fileReader.result;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
While `ArrayBuffer`, `Uint8Array` and other `BufferSource` are "binary data", a [Blob](https://www.w3.org/TR/FileAPI/#dfn-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](info:xmlhttprequest), [fetch](info:fetch-basics) and so on, can work with `Blob` natively, as well as with other binary types.
|
||||
|
||||
We can easily 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.
|
BIN
4-binary/03-blob/blob.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
4-binary/03-blob/blob@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
128
4-binary/04-file/article.md
Normal file
|
@ -0,0 +1,128 @@
|
|||
# File and FileReader
|
||||
|
||||
A [File](https://www.w3.org/TR/FileAPI/#dfn-file) object inhereits from `Blob`, but is extended with filesystem-related capabilities.
|
||||
|
||||
There are two ways to obtain it.
|
||||
|
||||
First, there's a constructor, similar to `Blob`:
|
||||
|
||||
```js
|
||||
new File(fileParts, fileName, [options])
|
||||
```
|
||||
|
||||
- **`fileParts`** -- is an array of Blob/BufferSource/String value, same as `Blob`.
|
||||
- **`fileName`** -- file name string.
|
||||
- **`options`** -- optional object:
|
||||
- **`lastModified`** -- a timestamp (integer date) of last modification.
|
||||
|
||||
Second, more often we get a file from `<input type="file">` or drag'n'drop or other browser interfaces. Then the file gets these from OS.
|
||||
|
||||
For instance:
|
||||
|
||||
```html run
|
||||
<input type="file" onchange="showFile(this)">
|
||||
|
||||
<script>
|
||||
function showFile(input) {
|
||||
let file = input.files[0];
|
||||
|
||||
alert(`File name: ${file.name}`); // e.g my.png
|
||||
alert(`Last modified: ${file.lastModified}`); // e.g 1552830408824
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
```smart
|
||||
The input may select multiple files, so `input.files` is an array-like object with them. Here we have only one file, so we just take `input.files[0]`.
|
||||
```
|
||||
|
||||
## FileReader
|
||||
|
||||
[FileReader](https://www.w3.org/TR/FileAPI/#dfn-filereader) is an object with the sole purpose of reading from `Blob` (and hence `File` too) objects.
|
||||
|
||||
It's event based, as reading from disk may take time.
|
||||
|
||||
The constructor:
|
||||
|
||||
```js
|
||||
let reader = new FileReader(); // no arguments
|
||||
```
|
||||
|
||||
The main methods:
|
||||
|
||||
- **`readAsArrayBuffer(blob)`** -- read the data as `ArrayBuffer`
|
||||
- **`readAsText(blob, [encoding])`** -- read the data as a string (encoding is `utf-8` by default)
|
||||
- **`readAsDataURL(blob)`** -- encode the data as base64 data url.
|
||||
- **`abort()`** -- cancel the operation.
|
||||
|
||||
As the reading proceeds, there are events:
|
||||
- `loadstart` -- loading started.
|
||||
- `progress` -- occurs during reading.
|
||||
- `load` -- no errors, reading complete.
|
||||
- `abort` -- `abort()` called.
|
||||
- `error` -- error has occured.
|
||||
- `loadend` -- reading finished with either success or failure.
|
||||
|
||||
When the reading is finished, we can access the result as:
|
||||
- `reader.result` is the result (if successful)
|
||||
- `reader.error` is the error (if failed).
|
||||
|
||||
The most widely used events are for sure `load` and `error`.
|
||||
|
||||
Here's an example of reading a file:
|
||||
|
||||
```html run
|
||||
<input type="file" onchange="readFile(this)">
|
||||
|
||||
<script>
|
||||
function readFile(input) {
|
||||
let file = input.files[0];
|
||||
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.readAsText(file);
|
||||
|
||||
reader.onload = function() {
|
||||
console.log(reader.result);
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
console.log(reader.error);
|
||||
};
|
||||
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
```smart header="`FileReader` for blobs"
|
||||
As mentioned in the chapter <info:blob>, `FileReader` works for any blobs, not just files.
|
||||
|
||||
So we can use it to convert a blob to another format:
|
||||
- `readAsArrayBuffer(blob)` -- to `ArrayBuffer`,
|
||||
- `readAsText(blob, [encoding])` -- to string (an alternative to `TextDecoder`),
|
||||
- `readAsDataURL(blob)` -- to base64 data url.
|
||||
```
|
||||
|
||||
|
||||
```smart header="`FileReaderSync` is available for workers only"
|
||||
For Web Workers, there also exists a synchronous variant of `FileReader`, called [FileReaderSync](https://www.w3.org/TR/FileAPI/#FileReaderSync).
|
||||
|
||||
Its reading methods `read*` do not generate events, but rather return a result, as regular functions do.
|
||||
|
||||
That's only inside a Web Worker though, because delays and hang-ups in Web Workers are less important, they do not affect the page.
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
`File` object inherit from `Blob`.
|
||||
|
||||
In addition to `Blob` methods and properties, `File` objects also have `fileName` and `lastModified` properties, plus the internal ability to read from filesystem. We usually get `File` objects from user input, like `<input>` or drag'n'drop.
|
||||
|
||||
`FileReader` objects can read from a file or a blob, in one of three formats:
|
||||
- String (`readAsText`).
|
||||
- `ArrayBuffer` (`readAsArrayBuffer`).
|
||||
- Data url, base-64 encoded (`readAsDataURL`).
|
||||
|
||||
In many cases though, we don't have to read the file contents. Just as we did with blobs, we can create a short url with `URL.createObjectURL(file)` and assign it to `<a>` or `<img>`. This way the file can be downloaded or shown up as an image, as a part of canvas etc.
|
||||
|
||||
And if we're going to send a `File` over a network, that's also easy, as network API like `XMLHttpRequest` or `fetch` natively accepts `File` objects.
|
3
4-binary/index.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Binary data, files
|
||||
|
||||
Working with binary data and files in Javascript.
|