ブラウザウィンドウのサムネイルを描画する

modest に投稿した記事と同内容です。

タブカタログ拡張機能のようにWebページのサムネイル画像を表示する拡張機能の多くは、 html:canvas 要素の二次元描画コンテクストの drawWindow メソッドへWebページの window オブジェクトなどを引数で渡してサムネイルの描画を行っています(canvas を使って図形を描く – MDC)。

この drawWindow メソッドの引数にWebページの window オブジェクトではなく、ブラウザウィンドウの ChromeWindow オブジェクトを渡すことで、ブラウザウィンドウのサムネイル画像を描画することも可能です。しかし、以下のようにブラウザタブ内に表示されたWebページまでは描画されず、背景色で塗りつぶされたようになってしまいます。

この問題を解決するには、ブラウザウィンドウのサムネイルの上に、Web ページのサムネイルを重ねて描画する必要があります。この記事では、その方式を紹介します。

XUL

ブラウザウィンドウのサムネイル描画用の html:canvas 要素を生成します。

<html:canvas id="testCanvas" />

JavaScript

最初にサムネイルの拡大/縮小率を定めておきます。ここでは50%に縮小することとします。

const scale = 0.5;

サムネイル描画の対象とするブラウザウィンドウとして、 nsIWindowMediator を使って直近のブラウザウィンドウの ChromeWindow オブジェクトを取得します。

var win = Components.classes["@mozilla.org/appshell/window-mediator;1"].
          getService(Components.interfaces.nsIWindowMediator).
          getMostRecentWindow("navigator:browser");

簡便のため、 html:canvas 要素への参照を取得しておきます。

var canvas = document.getElementById("testCanvas");

ブラウザウィンドウのサイズに合わせ、 html:canvas 要素のサイズを調整します。

var w = win.innerWidth;
var h = win.innerHeight;
canvas.width  = w * scale;
canvas.height = h * scale;

二次元描画コンテクストを取得し、ブラウザウィンドウのサムネイルを描画します。

var ctx = canvas.getContext("2d");
ctx.save();
ctx.scale(scale, scale);
ctx.drawWindow(win, 0, 0, w, h, "rgb(255,255,255)");

引き続き、Webページのサムネイルを描画しますが、その前に原点を左上(座標0, 0)から移動させる必要があります。原点を移動させるためには、二次元描画コンテクストの translate メソッドを使います。移動量は、ブラウザウィンドウに対する現在のブラウザ(xul:browser 要素)の位置から算出します。

var rect = win.gBrowser.mCurrentBrowser.getBoundingClientRect();
ctx.translate(rect.left, rect.top);

最後にWebページのサムネイルを重ねて描画します。なお、 win.contentwin に対応するブラウザウィンドウの現在のタブに表示されたWebページの window オブジェクトを表します。

w = win.content.innerWidth  || win.content.document.documentElement.clientWidth;
h = win.content.innerHeight || win.content.document.documentElement.clientHeight;
ctx.drawWindow(win.content, win.content.scrollX, win.content.scrollY, w, h, "rgb(255,255,255)");
ctx.restore();

以上のコードに適当に肉付けして実行してみると、以下のようにブラウザタブ内のWebページも含めてブラウザウィンドウのサムネイルが描画されます。

注意

今回はブラウザウィンドウ内の現在のブラウザタブのWebページだけが見えている前提となっていますが、分割ブラウザ (Split Browser)を使っている場合、この限りではありません。そのような場合にも対応させるには、さらなる工夫が必要となります。

補足

元々筆者はブラウザウィンドウのサムネイルを描画するために、ブラウザウィンドウ用とWebページ用の2つの html:canvas 要素を xul:stack 要素で重ねる方式を考え付きましたが、Piroさんのアドバイスにより、この記事で紹介したように原点を移動させつつ1個の html:canvas 要素に対して2回 drawWindow メソッドを使用する方式が可能なことがわかりました。

TOP

canvas要素によるWebページのスクリーンショット保存機能

今年4月に行なわれたMozillaParty7.0において、いくつか有用な情報を得ることができたが、中でもcanvas 要素の toDataURL メソッドで取得した data:URL をファイルへ保存するという Taken さんの情報は、 ScrapBook で保存したWebページのコレクションをサムネイル画像によって一望するというプランを一気に実現へと近づけることができるありがたいものであった。その具体的な方法はTaken SPC : Mozilla Party JP 7.0 に行ってきましたのポストでも説明されているが、これを利用して現在ブラウザに表示されているWebページのスクリーンショット(今回はサムネイルではなく、原寸大のスクリーンショット)をPNG画像として保存する機能を実装してみる。

(1) XUL

html:canvas要素を chrome://browser/content/browser.xul へオーバーレイする。オーバーレイする位置はどこでも構わないが、下記の例ではステータスバー内にオーバーレイしている。また、サイズの大きな canvas 要素を擬似的に隠すために、scrollbox 要素内に押し込んで表示させている。これは PearlCrescent PageSaver で用いられているテクニックである。

<overlay id="myOverlay"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">

    <statusbar id="status-bar">
        <scrollbox width="1" height="1">
            <html:canvas id="myCanvas" display="none" />
        </scrollbox>
    </statusbar>

</overlay>

(2) JavaScript

まずは対象となるWebページのWindowオブジェクトと、Webページのサイズを取得する。ただし、documentElement.clientHeightは、Quirks(後方互換)モードではWebページ全体の高さとなるが、 Standards Compliant(標準準拠)モードでは実際に見えている部分のみの高さとなる。PearlCrescent PageSaver では、どちらのモードにも対応した GetWindowWidth, GetWindowHeight 関数というのを自前で実装している。

var win = window._content;
var w = win.document.documentElement.clientWidth;
var h = win.document.documentElement.clientHeight;

var w = win.document.width;
var h = win.document.height;

「display: none;」となっていたcanvas要素を一時的に表示させ、そのサイズを先ほど取得したWebページのサイズと一致させる。canvas要素は scrollbox 内に押し込まれているので、見かけ上は1×1ピクセルとして表示される。

var canvas = document.getElementById("myCanvas");
canvas.style.display = "inline";
canvas.width = w;
canvas.height = h;

いよいよ、canvas要素へWebページを描画する。

var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(1.0, 1.0);    // 1.0なら原寸大
ctx.drawWindow(win, 0, 0, w, h, "rgb(255,255,255)");
ctx.restore();

続いて、PNG画像データをBASE64エンコードしたdata:URLを取得する。これは Firefox 2.0 以外では例外となるので、 try~catch する。さらに、取得した string型の data:URL から nsIURI オブジェクトを生成する。

try {
    var url = canvas.toDataURL("image/png");
} catch(ex) {
    return alert("This feature requires Firefox 2.0.
" + ex);
}
const IO_SERVICE = Components.classes['@mozilla.org/network/io-service;1']
                   .getService(Components.interfaces.nsIIOService);
url = IO_SERVICE.newURI(url, null, null);

ファイルピッカーを使って保存先ファイルを決定する。

var fp = Components.classes['@mozilla.org/filepicker;1']
          .createInstance(Components.interfaces.nsIFilePicker);
fp.init(window, "Save Screenshot As", fp.modeSave);
fp.appendFilters(fp.filterImages);
fp.defaultExtension = "png";
fp.defaultString = "screenshot.png";
if ( fp.show() == fp.returnCancel || !fp.file ) return;

nsIWebBrowserPersist を使って data:URL をファイルへ保存する。
nsIWebBrowserPersist はURIがhttp:プロトコルであるインターネット上のデータをダウンロードするためによく使われるが、data:プロトコルに対しても有効であるというのがミソ。

var wbp = Components.classes['@mozilla.org/embedding/browser/nsWebBrowserPersist;1']
          .createInstance(Components.interfaces.nsIWebBrowserPersist);
wbp.saveURI(url, null, null, null, null, fp.file);

最後に、一時的に表示されたキャンバスを非表示にして後始末する。

canvas.style.display = "none";
canvas.width = 1;
canvas.height = 1;

以上のような処理でおおよそスクリーンショットを保存する機能を実装することができた。あとは、適当なUIを加えれば、 PearlCrescent PageSaver Basic 相当の拡張機能がいとも簡単に実装できてしまう。

しかし、ひとつ厄介な問題がある。スクリーンショットを撮る対象のWebページのサイズが非常に大きい場合、一時的に膨大なメモリを食ってしまうということである。例えば ScrapBook の「リクエスト+バグ」のページのスクリーンショットを保存すると、一時的にメモリ使用量が50MBほど上昇する。PCに搭載されたメモリサイズに依存するが、最悪PCの動作が不安定になり、Firefox を強制終了するしかなくなることもある。原因はもちろん data:URL を取得して string型の変数へ格納した際に発生するのだが、対策は難しいだろう。

TOP