Chromium で Canvas.toBlob が試験的に利用可能になっています

5年前から多くの注目を集めていた Chromium の [Issue 67587](https://code.google.com/p/chromium/issues/detail?id=67587#c113) に進捗があり、 [Canvas.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/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

```idl
interface HTMLCanvasElement : HTMLElement {
   [RaisesException, RuntimeEnabled=ExperimentalCanvasFeatures] void toBlob(FileCallback? _callback, optional DOMString type = null, optional any arguments);
}
```

Canvas.toBlob の実装は以下のようになっています。

```cpp
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)は可能だったのですが、それはただ単に可能というだけであり、とてもとてもつらい状態でした。

```js
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 にあったような苦役から開放されます。

1. Base64 変換が不要に ( `atob` )
2. JavaScript 側でのバッファの確保が不要に ( `new Uint8Array` )
3. charCodeAt のヘビーループが不要に ( `for 〜 charCodeAt` )
4. これら一連の同期処理が非同期化される

特に非同期化です。Canvas のキャッシュを作成する際に UI Thread が固まらなくなります。 
モバイル端末の画素数が激増する昨今において、これは大きな進歩です。 

## Sample Code

Canvas.toBlob と、toBlob の代わりに toDataURL を使ったコードはこのようなります。  
`chrome://flags/#enable-experimental-canvas-features` を有効にすると試用できます。


```html
<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
![](https://gyazo.com/66354c6947ce7abf14180cd34934782d.png)


## Conclusion

- Canvas.toBlob 良い。とても良い
- Firefox や IE 10+ では既に利用可能な状態です
- [~~残りは Safari だけ~~](https://bugs.webkit.org/show_bug.cgi?id=71270)。WebKit も追従してほしい
    - [Safari 11 でも実装されました](https://bugs.webkit.org/show_bug.cgi?id=148878)