愛と勇気と缶ビール

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

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会員な人は、オーナーライブラリを利用して読めばお得だろう。

そんじゃーね!

Chrome extensionでclipboardに文字列をコピー (2014/01/26時点)

なんか、やろうと思ってググったらみんな色々なこと書いてて何が正しいのかよくわからん!ムキー!ってなったので同じような人を救うために2014年1月時点での方法をメモ。あまり大した内容ではない。

manifest.json

現時点で必要なのは "clipboardRead" というpermission。以下は例。

{
    "manifest_version": 2,
    "name": "Are",
    "version": "1.0",
    "description": "Are, aredesu",
    "permissions": [
        "clipboardRead"
    ]
}

pasteの仕方

background page (event page) にて、以下のような関数を書いておいてstringを渡せばOK。 ちなみに、textAreaはdisplay:none;とかだとダメです。これには由縁がありそうだが、あまり深追いするメリットなさそうなので「表示はさせた上で要素を飛ばせば大丈夫」くらいの認識で済ませた。

    function saveToClipboard(str) {
        var textArea = document.createElement("textarea");
        textArea.style.cssText = "position:absolute;left:-100%";

        document.body.appendChild(textArea);

        textArea.value = str;
        textArea.select();
        document.execCommand("copy");

        document.body.removeChild(textArea);
    }

じゃあの。

「あとで読む」な、今読め

f:id:zentoo:20140125135710p:plain

ネットで効率的な情報収集とか言われるようになって久しい昨今、百害あって一利なしなのが「あとで読む」系のサービスだと思う。これを真面目に使いはじめると、ヤバい。

経験上、「あとで読む」ことにしたものを後から読んで、メリットがあったことはほとんど一度もない。何らかの行動に繋がったこともない。

「あとで読む」ことにした時点で、それは今日この時を使って読む価値がない文章だったということだ。そんな文章が、後から参照して価値のある文章になることはほぼ無い。

一番よくないのは、「あとで読む」ことが可能であることによってその情報が今の自分にとって本当に必要なのかどうか、判断する必要がなくなってしまうことだ。

ひょっとしたらいつか必要になるかもしれない。そんな情報ばかりストックしていると、本当に今必要な情報への感度が鈍る。それが自分にとって本当に必要な情報かどうか分からないというのはつまり、自分が今なにをすべきかを判断できていないということ。これは恥ずべきことだ。

そういうわけで、あとで読むくらいなら、読まない。読むべきなら今読む。そういう割り切りを行った方が、生活がシンプルになると思う。

大量の積ん読も、止めた方がいい。積んでいるという時点で、結局その本には今すぐ参照したいような情報がないということだ。「いつか役に立つかもしれないから」「今すぐ勉強はしないけど、いつか必要になるかもしれないから」くらいのモチベーションしかないなら、初めからその本は買わない方がいい。

積んでいる技術書、これから先参照する可能性のない技術書は全部売ってしまえ。スペースの無駄である。もちろん、大量の技術本が詰まった本棚を眺めて悦に入りたい人は別だが…。そういうのって、やめた方がいいと思うけど。

この世には、どうでもいい情報が多すぎる。とりあえずみんな、ネットで情報収集なんて止めて、情報ダイエットした方がいいと思う。おそらくその方が幸せになれる。

(この記事に、「あとで読む」ってブコメつけてくださってもいいんですのよ!)