愛と勇気と缶ビール

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

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を実装すべし

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

Martiniでtemplateのデリミタがclient sideのそれと被るのを回避する

最近、「個人プロジェクトだし、別にどの言語で書いてもいいや」というようなものについては試しにgolangで書いてみることが多くなった。単なる素振り。

で、それがWebアプリである時は codegangsta/martini · GitHub を使うわけだが、これに限らずgolangのtemplateではmustache的なアレ ( {{ }} のこと ) がデリミタであるのが基本である。

client sideのtemplateが同様のデリミタを採用していると爆死するので、このように回避する。

    m := martini.Classic()
    m.Use(render.Renderer(render.Options{
        Layout: "layout",
        Delims: render.Delims{"[[", "]]"},
    })) 

martiniを使ってない場合、特に組み込みのtemplateをそのまま使っている場合は、まあドキュメントでも自分で読んでくれい。

Google accountでAuthenticationかましつつAuthorizationもかましたい場合

ハーイ。今日のお兄さんは優しくないので、タイトルの2つを混同してるやつ死刑ね。

Google Accounts Authentication and Authorization — Google Developers

とか

Using OAuth 2.0 for Login (OpenID Connect) - Google Accounts Authentication and Authorization — Google Developers

とかを見てると、「例えばJavaScriptからAPI叩きつつ、裏側でGoogle+ Platform — Google Developers使ってユーザ認証もしておきたい場合はどげんしたらいいの?どれをどう使えばいいの?一体どっちなの?」という疑問に捉われる。

正解はgoogle plus loginの方を使いつつ、gapi.client相当も読み込んでおいて

    <script type="text/javascript">
      (function() {
        var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
        po.src = 'https://apis.google.com/js/client:plusone.js?onload=onLoadCallback';
        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
      })();
    </script>

API叩く用のscopeも指定しつつAuthorization Request送って(以下はGoogle+ sign-in buttonを使う場合)

    <span id="sign-in">
        <span
            class="g-signin"
            data-callback="signinCallback"
            data-clientid="[YOUR CLIENT ID]"
            data-cookiepolicy="single_host_origin"
            data-scope="openid https://www.googleapis.com/auth/calendar.readonly"
        >
        </span>
    </span>

後は普通にJSからAPIを叩けばオケー。これ以降のユーザ認証についてはドキュメントに書いてある通りなので省略。

        function signinCallback(authResult) {
            console.log(authResult);
            if ( authResult.status.signed_in ) {
                gapi.client.load("calendar", "v3", processCalendar);
            }
            else {
                document.getElementById("sign-in").style.cssText += "display:inline;";
            }
        }

        function processCalendar() {
            gapi.client.calendar.calendarList.list().execute(function(res) {
                console.log(res);
            });
        }

Linux Kernel UpdatesというKindle本がとても良い

Linux Kernel Updates Vol.2013.12

Linux Kernel Updates Vol.2013.12

Linux Kernel Updates Vol.2012.12

Linux Kernel Updates Vol.2012.12

Linux Kernel Updates Vol.2013.08

Linux Kernel Updates Vol.2013.08

このシリーズ、

  • 題名通りの、Linux Kernelの特定versionのupdate内容まとめ
  • 著者自身によるコラム

の2部構成になっているのだが、特に後半部の内容が濃い。

一番上に示したVol.2013.12ではTCP周りのチューニングについて書いてあるのだが、tcp_tw_recycleとtcp_tw_reuseの違いについて日本語で明確に書いてある資料は初めて見た。大抵の人は僕と同様に「えーそうだったんか、知らんかった!」という感想になると思う。

  • 中味ある程度知りたいけど、Kernelのネットワークスタックその他を自分で読み始めるのは色々厳しいな、という人
  • あるいは、自分で読むくらいのモチベーションはあるけど何らかのとっかかりが欲しい人

におすすめ。こういうKindle本が増えていって欲しい。

僕はシリーズ全部を普通に買ってしまったけど、AmazonのPrime会員な人は、オーナーライブラリを利用して読めばお得だろう。

そんじゃーね!