愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

2014年にもなってWebページをまともに印刷する方法も知らない俺たちは(あるいは、とあるウェッブエンジニアの闘いの記録)

前略、window.printという関数をJavaScriptから呼び出せば、めでたく印刷ダイアログが表示されて、今目の前に表示されているウェッブページを印刷することが出来るわけだ。

が、とりあえずこの機能をそのまま使うとおかしい。何やら色々おかしい。media="print"なスタイルシートで指定したbackground-colorすら反映されない。「ブラウザの印刷オプションで出来る」という噂もあるが、僕の環境ではちゃんと色が出なかった。

これは一体どうなっているんだ?2014年にもなって、俺達はウェッブページを、自分がいま目の前にしているそのままの姿で印刷することすら出来ないのか?HTML5技術のカンファレンスを開いたり、WebSocketでチャットを作ったりしてる場合じゃない!事件はカンファレンスで起きているんじゃない、俺の家のプリンターの前で起きているんだ!

ゲンよ、画像じゃ。画像を使うんじゃ

というわけで僕はアッと驚くワークアラウンドを探すための旅に出たわけだが、何となくまともな印刷機能を提供していそうなGoogleという会社の、Googleカレンダーというサービスについてさらっと調べてみた。

どうやら、Googleカレンダーは「今見ているウェッブページ」をそのまま印刷するのではなく、一旦画像を生成して、その画像を表示したページを印刷することで回避しているようだ。コードをチラ見したところ、"Print"ボタンを押した際にiframeでアレコレしている節もある。

よし、これだ!画像は印刷でそのまま色が載るのだ。画像なら、スタイルシートがどうのこうのと悩むこともない。我が社もこれで行こう。

f:id:zentoo:20140211154855p:plain

画像をどうやって作るか?

で、だ。画像はどうやって作る?Googleはおそらくカレンダーのデータを画像としてレンダリングするような内部APIを持っているのだろう。これは間違いなくクロスブラウザなソリューションだ。が、自分でそんなものを用意するのは面倒くさくて仕方がない。

要は、今見ているページのDOMを、何らかの形でimgのsrcにぶっ込めるURLに変換できればいいわけだ。よし、ちょっと迂遠だがcanvasでいこう。

html2canvasというライブラリがある。試した所、ある程度まともに動きそうだ。これを使おう。

html2canvasを使って、DOM -> canvas -> Data URIという変換を行う

html2canvasを使うと、以下のような感じでいけるのではないか。

  1. html2canvasで、現在のページのDOM(またはその一部)をcanvas elementに変換する
  2. canvas elementのtoDataURL()でData URIを得る
  3. same originかつ不可視なiframeを開き(または予め開いておき)、その中のimage elementのsrcに親frameから先ほどのData URIをぶっ込む
  4. iframe.contentWindow.print()を呼び出す

完璧じゃないか、我が軍は。

実際のコード

なんとなく、Chrome 33.0.1750.70 betaで動くことは確認した。

    function printWithCanvas() {
        html2canvas(document.getElementsByTagName("main")[0], {
            onrendered: function(canvas) {
                var iframe = document.getElementById("print-iframe");
                var iframeWindow = iframe.contentWindow;
                var imageElement = iframeWindow.document.getElementById("canvas-image");

                if ( typeof canvas.toBlob === "function" ) { 
                    var blobURL = URL.createObjectURL(canvas.toBlob());
                    imageElement.src = blobURL;
                }   
                else {
                    var dataURL = canvas.toDataURL();
                    imageElement.src = dataURL;
                }   

                iframeWindow.print();
            }   
        }); 
    };  

iframeの中味は、以下のような感じのHTMLにしておく。

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
    <style type="text/css">
        #canvas-image {
            width: 100%;
            height: 100%;
        }   
    </style>
    <script type="text/javascript">
    </script>
</head>
<body>
    <img id="canvas-image" src="" alt="" />
</body>
</html>

HTMLCanvasElementのtoBlobがある場合はそれを使う。比較的良心的ブラウザーであるところのFirefoxには既に搭載されているようだ。canvasをimageとして扱う際にData URIにわざわざシリアライズしなくてはいけないのはとっても非効率なので、このAPIはとっととcanvasの載っている全てのブラウザーで実装されてほしい。

canvasがまともに動かないレガシーなブラウザでは、uuCanvasを使えばよいだろう。まだ、試していないが...

まとめ

  • window.printでそのままページを印刷するのは厳しい
  • html2canvasはそれなりに動く
  • ブラウザーはすべからくHTMLCanvasElementのtoBlobを実装すべし

今回は駆け足で考えかつ実装しましたが、もっといい方法があれば教えて頂きたいです。