まえがき
以前の 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 なバイナリデータやファイルデータを上手く扱うための機能を提供します。
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 に変換したり、元に戻したりできます。
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 } を指定します。
// Create a empty Blob object.
var blob = new Blob();
// 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" } を指定します。
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つです。
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 } が拡張されています。
// 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 などにそのまま渡す事ができます。
var reader = new FileReader();
reader.readAsArrayBuffer(file);
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 のリストです。
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];
...
}
});
class FileList {
readonly length: UINT32 = 0,
item(index:UINT32):File { },
}
FileReader
FileReader クラスは、バイナリデータをメモリに読み込む機能を提供します。
readAsArrayBuffer を使うと Blob を ArrayBuffer に変換します。変換した結果は result プロパティに設定されています。
同様に readAsDataURL や readAsText もありますが、こちらを利用する機会はほとんどないでしょう。
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 で動的に生成する事ができます。
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)
var blobURL = URL.createObjectURL(file);
... 必要な処理 ...
URL.revokeObjectURL(blobURL);
img.src や XMLHttpRequest#open(method, URL) や Audio API が URL を要求しますが、そのような場合は Blob から BlobURL を生成して渡す事ができます。
BlobURL は DataURI を使った場合に比べて速度・メモリともに効率的です。Audio などのストリーミングデータを扱う場合はその差が顕著に現れます。
var img = document.createElement("img");
img.src = blobURL;
有効な BlobURL は以下の方法で確認する事ができます。
- Chrome chrome://blob-internals/
Blob, File, BlobURL の相互変換
BlobURL は XMLHttpRequest を使う事で、Blob に戻したり ArrayBuffer や他の型に変換することができます。
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 への変換メソッドは以下のように実装すると良いでしょう。
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" です。
var reader = new FileReader();
reader.onload = function() {
var buffer = reader.result; // String
};
reader.readAsText(file, "UTF-16");