File APIs(Blob, BlobURL, ArrayBuffer, FileReader)


# まえがき

以前の JavaScript はバイナリデータの扱いがとても下手でした。  

バイナリデータをブラウザ上に一旦保存し再利用するには、サイズが1.3倍になることを覚悟した上でデータをDataURIに変換する必要がありましたし、XHRを使ってサーバからデータを取得する場合も、一度全てメモリに読み込んでから、ループでマスク処理を施し、Base64に変換し、DataURIに変換し… と、何重にも変換を繰り返す必要がありました。

2009年に出版された JavaScript Good Parts では、ビット演算子が「使うべきではない悪いパーツ」と評価されていた事を思い出す方もいるでしょう。


あれから5年、もはや時代が違います。
2015年の JavaScript においては、バイナリデータはもはや扱いづらい困った存在ではありません。  

ハードウェアと JIT コンパイラの進化に合わせ、大容量のバイナリデータを扱う事を前提としたHTML5 APIが多数実装されています。
バイナリデータはもはや敵ではないのです。

# File API and Objects

このエントリでは、WebWorkers はもとより Storage(IndexedDB, FileSystem), WebGL, WebRTC, WebAudio などで必要とされ、今後の JavaScript プログラミングシーンで大活躍するであろう File API と、そこで定義されている Blob, File, FileList オブジェクトについて説明します。

## Blob

[Blob][] は immutable なバイナリデータやファイルデータを上手く扱うための機能を提供します。  

```js
class Blob {
    readonly size: UINT64 = 0,
    readonly type: String = "",

    constructor(source:AnyArray, option:BlobTypeObject = null):Blob { },
    slice(start:INT64 = 0, end:INT64 = 0, contentType:String = ""):Blob { },
    close():void {}
}
class BlobTypeObject {
    readonly type: String = ""
}
```

Blob はプロパティ{ size:UINT64, type:String } とメソッド { slice, close } を持ちます。

Blob は ArrayBuffer や BlobURL に変換したり、元に戻したりできます。

```js
var source = new Uint8Array([ 0,1,2 ]);
var blob = new Blob([ source ]);
var reader = new FileReader();

reader.onload = function() {
    var result = new Uint8Array(reader.result); // reader.result is ArrayBuffer
};
reader.readAsArrayBuffer(blob);
```

Blob コンストラクタの第一引数(source)には配列を指定します。
この配列に指定可能な型は、Blob, ArrayBuffer, TypedArray, String またはオブジェクトです。
オブジェクトを指定した場合は、toString() メソッドで文字列に変換します。  
複数の要素がある場合は、先頭から順番に連結されたものが Blob オブジェクトの内容になります。

Blob コンストラクタの第二引数には、{ type: MimeTypeString } を指定します。

```js
// Create a empty Blob object.

var blob = new Blob();
```

```js
// Create a octet-binary Blob object.

var buffer1 = new Uint8Array(100);
var buffer2 = new Uint8Array(100);

var blob = new Blob([buffer1, buffer2], { type: "application/octet-binary" });
```

MimeTypeString にはBlobの種類を示す MimeType を指定します。  
汎用的なバイナリデータとして扱いたい場合は、{ type: "application/octet-binary" } を指定します。

```js
var blob = new Blob([file], { type: "image/png" });
var blob = new Blob([binary], { type: "application/octet-binary" });
var blob = new Blob(["文字列"], { type: "text/plain" });
var blob = new Blob(["<html>"], { type: "text/plain;charset=UTF-8" });
```


## File

[File][] は Blob を継承しています。Blob に対してプロパティ { name:String, lastModifiedDate:Date } が追加されています。

File は、JavaScript からローカルファイルを扱う数少ない方法の1つです。

```js
class File extends Blob {
    readonly name: String = "",
    readonly lastModifiedDate: Date = null,
    readonly webkitRelativePath: String = "",

    constructor(source:Blob, fileName:String):File { },
}
```

Chrome 39 では、File のコンストラクタには配列を指定する必要があります。 `new File([], ...)`  
プロパティ { lastModified: Integer, webkitRelativePath: String } が拡張されています。

```js

// Chrome の実装
class File extends Blob {
    readonly name: String = "",
    readonly lastModified: Integer = 0,
    readonly lastModifiedDate: Date = null,
    readonly webkitRelativePath: String = "",

    constructor(source:AnyArray, fileName:String):File { },
}
```

File は Blob を継承しているので、Blob を要求する FileReader#readAsArrayBuffer() API などにそのまま渡す事ができます。

```js
var reader = new FileReader();

reader.readAsArrayBuffer(file);
```


```js
function _toBlob(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.responseType = "blob";
    xhr.onload = function() {
        callback(xhr.response, url);
    };
    xhr.open("GET", url);
    xhr.send();
}

_toBlob(location.href, function(blob) {
    var blobURL = URL.createObjectURL(blob);
    var file = new File([blob], "foo.html"); // または new File(blob, ...)
});
```


## FileList

FileList は `<input type="file" multiple="multiple">` の onchange イベントから取得できる File のリストです。

```js
document.querySelector('input[type="file"]').addEventListener("change", function(event) {
    var fileList = event.target.files; // ArrayLikeObject

    for (var i = 0, iz = fileList.length; i < iz; ++i) {
        var file = fileList[0];
        ...
    }
});
```



```js
class FileList {
    readonly length: UINT32 = 0,

    item(index:UINT32):File { },
}
```


## FileReader

[FileReader][] クラスは、バイナリデータをメモリに読み込む機能を提供します。

readAsArrayBuffer を使うと Blob を ArrayBuffer に変換します。変換した結果は result プロパティに設定されています。  
同様に readAsDataURL や readAsText もありますが、こちらを利用する機会はほとんどないでしょう。

```js
class FileReader extends EventHandler {
    EMPTY:   UINT16 = 0,
    LOADING: UINT16 = 1,
    DONE:    UINT16 = 2,
    readonly error: Error = null,
    readonly result: ArrayBuffer|String = null,
    readonly readyState: UINT16 = EMPTY,
    onload:      EventHandler = null,
    onloadstart: EventHandler = null,
    onloadend:   EventHandler = null,
    onprogress:  EventHandler = null,
    onabort:     EventHandler = null,
    onerror:     EventHandler = null,

    // async read methods
    readAsArrayBuffer(blob:Blob): void {},
    readAsDataURL(blob:Blob):void {},
    readAsText(blob:Blob, label:String = ""):void {},

    abort():void {},
}
```

Chrome 39 にはこのほかに `readAsBinaryString(blob:Blob):void {}` が存在します。


## BlobURL の作成と解放

[BlobURL][] は "blob:..." から始まる仮想的なURLです。  
URL.createObjectURL(source:Blob):BlobURLString で動的に生成する事ができます。

```js
var uint8Array = new Uint8Array([1, 2, 3]);
var blob = new Blob([uint8Array], { type: "application/octet-binary" });
var blobURL = URL.createObjectURL(blob); // "blob:http%3A//dev.w3.org/207c851d-7486-42ec-97b1-d3cd26d08ed9"
```

URL.createObjectURL は常にユニークな BlobURL を生成します(同じリソースに対しても毎回異なる仮想URLを生成します)。
生成したBlobURLはブラウザか終了するまで有効です。

BlobURL を解放するには URL.revokeObjectURL(blobURL:BlobURLString):void を使用します。

リソースの解放を自動的に行う URL.createFor(Blob):BlobURLString もありますが、
まだ実装しているブラウザはありません(2014/11)

```js
var blobURL = URL.createObjectURL(file);

... 必要な処理 ...

URL.revokeObjectURL(blobURL);
```

img.src や XMLHttpRequest#open(method, URL) や Audio API が URL を要求しますが、そのような場合は Blob から BlobURL を生成して渡す事ができます。

BlobURL は DataURI を使った場合に比べて速度・メモリともに効率的です。Audio などのストリーミングデータを扱う場合はその差が顕著に現れます。

```js
var img = document.createElement("img");
img.src = blobURL;
```

有効な BlobURL は以下の方法で確認する事ができます。

- Chrome chrome://blob-internals/

## Blob, File, BlobURL の相互変換

BlobURL は XMLHttpRequest を使う事で、Blob に戻したり ArrayBuffer や他の型に変換することができます。

```js
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
        var result = xhr.response; // ArrayBuffer
    };
    xhr.responseType = "arraybuffer";
    //xhr.responseType = "blob";
    xhr.open("GET", blobURL);
    xhr.send();
```

Blob, BlobURLString, ArrayBuffer の相互変換は以下のメソッドで行います。

| from         | to                 | method                         |
|--------------|--------------------|--------------------------------|
| Blob/File    | BlobURLString      | URL.createObjectURL(...)       |
| BlobURL      | Blob               | XMLHttpRequest#responseType = "blob" |
| BlobURL      | ArrayBuffer        | XMLHttpRequest#responseType = "arraybuffer" |
| Blob/File    | ArrayBuffer        | FileReader#readAsArrayBuffer(...) |
| Blob/File    | BinaryString       | FileReader#readAsBinaryString(...) |
| Blob/File    | DataURLString      | FileReader#readAsDataURL(...) |
| Blob/File    | String             | FileReader#readAsText(..., 文字コード) |

ArrayBuffer への変換メソッドは以下のように実装すると良いでしょう。

```js
function _toArrayBuffer(source,     // @arg BlobURLString|URLString|Blob|File|TypedArray|ArrayBuffer
                        callback) { // @arg Function - callback(result:ArrayBuffer, source:Any):void
    if (source.buffer) {
        // TypedArray
        callback(source.buffer, source);
    } else if (source instanceof ArrayBuffer) {
        // ArrayBuffer
        callback(source, source);
    } else if (source instanceof Blob) {
        // Blob or File
        var reader = new FileReader();
        reader.onload = function() {
            callback(reader.result, source);
        };
        reader.readAsArrayBuffer(source);
    } else if (typeof source === "string") {
        // BlobURLString or URLString
        var xhr = new XMLHttpRequest();
        xhr.responseType = "arraybuffer";
        xhr.onload = function() {
            callback(xhr.response, source);
        };
        xhr.open("GET", source);
        xhr.send();
    } else {
        throw new TypeError("Unknown type");
    }
}
```

FileReader#readAsText(blob, "UTF-8") は、第二引数に文字コードを指定できます。デフォルトは "UTF-8" です。

```js
var reader = new FileReader();

reader.onload = function() {
    var buffer = reader.result; // String
};
reader.readAsText(file, "UTF-16");
```

[Blob]: http://dev.w3.org/2006/webapi/FileAPI/#blob
[File]: http://dev.w3.org/2006/webapi/FileAPI/#file
[FileList]: http://dev.w3.org/2006/webapi/FileAPI/#FileReader-interface
[FileReader]: http://dev.w3.org/2006/webapi/FileAPI/#APIASynch
[BlobURL]: http://dev.w3.org/2006/webapi/FileAPI/#DefinitionOfScheme