5年前から多くの注目を集めていた Chromium の Issue 67587 に進捗があり、 Canvas.toBlob が Chrome Canary 47.0.2498.0 で試験的に利用可能になりました。
追記: Safari 11 にも Canvas.toBlob が実装されました。
本題
★ Issue 67587: Add toBlob to Canvas element.
174 people starred this issue.Currently, the html5 canvas element has a toDataURL method, which outputs a DOM string representing the backing bitmap. With the File API now widely implemented, it is appropriate to add a similar getBlob method, one which would return a Blob object. Passing Blob pointers around is quite a bit more efficient than passing large base64 encoded strings.
https://code.google.com/p/chromium/issues/detail?id=67587#c113
interface HTMLCanvasElement : HTMLElement {
[RaisesException, RuntimeEnabled=ExperimentalCanvasFeatures] void toBlob(FileCallback? _callback, optional DOMString type = null, optional any arguments);
}
Canvas.toBlob の実装は以下のようになっています。
void HTMLCanvasElement::toBlob(FileCallback* callback, const String& mimeType, const ScriptValue& qualityArgument, ExceptionState& exceptionState) const
{
if (!originClean()) {
exceptionState.throwSecurityError("Tainted canvases may not be exported.");
return;
}
File* resultBlob = nullptr;
if (!isPaintable()) {
// If the canvas element's bitmap has no pixels
return;
}
double quality;
if (!qualityArgument.isEmpty()) {
v8::Local<v8::Value> v8Value = qualityArgument.v8Value();
if (v8Value->IsNumber()) {
quality = v8Value.As<v8::Number>()->Value();
}
}
String encodingMimeType = toEncodingMimeType(mimeType);
ImageData* imageData = toImageData(BackBuffer);
ScopedDisposal<ImageData> disposer(imageData);
// Perform image encoding
Vector<char> encodedImage;
ImageDataBuffer(imageData->size(), imageData->data()->data()).encodeImage(encodingMimeType, &quality, &encodedImage);
resultBlob = File::create(encodedImage.data(), encodedImage.size(), encodingMimeType);
Platform::current()->mainThread()->postTask(FROM_HERE, bind(&FileCallback::handleEvent, callback, resultBlob));
}
Canvas.toDataURL を駆使して Canvas.toBlob 相当の結果を得るコーディング(polyfill_Canvas_toBlob)は可能だったのですが、それはただ単に可能というだけであり、とてもとてもつらい状態でした。
function polyfill_Canvas_toBlob(canvas, callback, type, encoderOptions) {
type = type || "image/png";
var url = canvas.toDataURL(type, encoderOptions);
var bin = atob(url.split(",")[1]);
var u8a = new Uint8Array(bin.length);
for (var i = 0, iz = bin.length; i < iz; ++i) {
u8a[i] = bin.charCodeAt(i);
}
callback( new Blob([u8a.buffer], { "type": type }) );
}
Canvas.toBlob では、polyfill_Canvas_toBlob にあったような苦役から開放されます。
- Base64 変換が不要に (
atob
) - JavaScript 側でのバッファの確保が不要に (
new Uint8Array
) - charCodeAt のヘビーループが不要に (
for 〜 charCodeAt
) - これら一連の同期処理が非同期化される
特に非同期化です。Canvas のキャッシュを作成する際に UI Thread が固まらなくなります。
モバイル端末の画素数が激増する昨今において、これは大きな進歩です。
Sample Code
Canvas.toBlob と、toBlob の代わりに toDataURL を使ったコードはこのようなります。
chrome://flags/#enable-experimental-canvas-features
を有効にすると試用できます。
<canvas width="150"></canvas>
<script>
window.onload = function() {
var canvas = document.querySelector("canvas");
var ctx = canvas.getContext("2d");
draw(ctx);
if (canvas.toBlob) {
canvas.toBlob(_copyCanvas); // native
}
polyfill_Canvas_toBlob(canvas, _copyCanvas);
};
function _copyCanvas(blob) {
var img = document.createElement("img");
var blobURL = URL.createObjectURL(blob);
img.onload = function() {
URL.revokeObjectURL(blobURL); // GC
};
img.src = blobURL;
document.body.appendChild(img);
}
function polyfill_Canvas_toBlob(canvas, callback, type, encoderOptions) {
type = type || "image/png";
var url = canvas.toDataURL(type, encoderOptions);
var bin = atob(url.split(",")[1]);
var u8a = new Uint8Array(bin.length);
for (var i = 0, iz = bin.length; i < iz; ++i) {
u8a[i] = bin.charCodeAt(i);
}
callback( new Blob([u8a.buffer], { "type": type }) );
}
function draw(ctx) {
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 100, 100);
ctx.fillStyle = "blue";
ctx.fillRect(10, 10, 100, 100);
}
</script>
Result
Conclusion
- Canvas.toBlob 良い。とても良い
- Firefox や IE 10+ では既に利用可能な状態です
-
残りは Safari だけ。WebKit も追従してほしい