IE11 でもHTMLに動的描画のエリアを画像として保存

先日、html2canvas を使って表示HTMLを画像変換してダウンロード - Oboe吹きプログラマの黙示録
を書いて、IE11では不可能と書きましたが、方法があったのです。
html2canvas を実行した時に、Promise が認識できなくてダメだったのですが、
Polyfill を使えばいいのです。
https://polyfill.io/v2/

HTMLで、

<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>

を書いておくかこのJSを入手して自サーバに置くかします。
これで、html2canvas で画像イメージを取得はできますが、それでもIE11はダウンロードの壁があるので
次に、サーバに一旦、Base64エンコードデータを送信してデコードして返してもらう方法で
ダウンロードができます。
サーバ側アプリに、Wicket を使います。

html2canvas で変換対象のHTML部分は省略します。

<li><button id="outImage" type="button">Download</button>
<a id="download" href="#">Download</a></li>
<input wicket:id="imagedata" id="imagedata" type="hidden">
<button wicket:id="download2nd" id="download2nd" type="button" style="display: none">IE Download</button>

jQuery
Chrome or Edge なら、a tag の href にダウンロードを仕掛けます
そうでなければ、download2nd をtrigger でクリックします。

$("#outImage").on('click', function(){
   var userAgent = window.navigator.userAgent.toLowerCase().toLowerCase();
   console.log("useragent : " + userAgent);
   $('#ua').html(userAgent);
   html2canvas($("#tree"), {
      onrendered : function(canvas){
         if (userAgent.indexOf("chrome/", 0) > 0 || userAgent.indexOf("safari/", 0) > 0){
            var imgageData = canvas.toDataURL("image/png");
            // a id="download" に ダウンロード設定
            $("#download").attr("download", "テスト.png")
            .attr("href", imgageData.replace(/^data:image\/png/, "data:application/octet-stream"));
            // ダウンロード発火
            var evt = document.createEvent("MouseEvents");
            evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
            document.getElementById("download").dispatchEvent(evt);
         }else{
            var imgageData = canvas.toDataURL("image/png");
            $('#imagedata').val(imgageData.replace(/^data:image\/png/, "data:application/octet-stream"));
            $('#download2nd').trigger("click");
         }
      }
   });
});

Wicket Base64受信データを受信するフィールド

queue(new Form<Void>("form"));
final HiddenField<String> imagetext_Field = new HiddenField<>("imagedata", new Model<>());
queue(imagetext_Field);

グローバル変数Base64受信データを格納して、以下 AJAXDownload でデコードして
OutputStream に流します。
この AJAXDownload は、https://github.com/yipuran/yipuran-wicketcustom の中にあります。

String datastr;
AJAXDownload downloader = AJAXDownload.of(o->{
   try{
      byte[] b = Base64.getDecoder().decode(datastr);
      o.write(b, 0, b.length);
      o.flush();
      o.close();
   }catch(Exception e){
      throw new RuntimeException(e);
   }
}, ()->"image/png", ()->"テスト.png");

↑の AJAXDownload を submit で呼びます。
ここの SerialThrowableConsumer は、https://github.com/yipuran/yipuran-wicketcustom の中にあります。

queue(new Button("download2nd")
.add(AjaxFormSubmitBehavior.onSubmit("click", SerialThrowableConsumer.of(t->{
   datastr = imagetext_Field.getModelObject().replaceFirst("data:application/octet-stream;base64,", "");
   downloader.callBackDownload(t);
}, (t, x)->{
   t.appendJavaScript("alert('" + x.getMessage() + "');");
}))).add(downloader));

わざわざ、HTMLの input hidden を用意して送信して打ち返すという
方法で安易な方法ですが、確実です。

ブラウザの情報取得

Wicket でブラウザの情報取得は、WebApplication の init() で

getRequestCycleSettings().setGatherExtendedBrowserInfo(true);

として、
WebPage のコンストラクタで、Wicket 6 までは、、

ClientProperties properties
 = ((WebClientInfo)getRequestCycle().getClientInfo()).getProperties();

としていたが、もう RequestCycle からは取得できなくなってる

Wicket 8.x ~
WebSession から取得する。

ClientProperties cprop = WebSession.get().getClientInfo().getProperties();

System.out.println(" browser  width  = " + cprop.getBrowserWidth() );
System.out.println(" browser  height = " + cprop.getBrowserHeight() );
System.out.println(" screen   coloer depth = " + cprop.getScreenColorDepth() );
System.out.println(" screen   width  = " + cprop.getScreenWidth() );
System.out.println(" screen   height = " + cprop.getScreenHeight() );
System.out.println(" UserAgent       = " + cprop.getNavigatorUserAgent() );
System.out.println(" App  code Name  = " + cprop.getNavigatorAppCodeName() );
System.out.println(" App  Name       = " + cprop.getNavigatorAppName() );
System.out.println(" App  varsion    = " + cprop.getNavigatorAppVersion() );
System.out.println(" Language = " + cprop.getNavigatorLanguage() );
System.out.println(" Platform = " + cprop.getNavigatorPlatform() );
System.out.println(" remote Addressh = " + cprop.getRemoteAddress() );
System.out.println(" UserAgent       = " + ((WebRequest)getRequest()).getHeader("User-Agent") );

BootStrap で配布される bg-* の色見本

BootStrap 4.1.3 で配布されるcss の bg-* の色見本

どこかにあるのかも知れないけど、探すのが面倒だから。。。
( 流行り、廃りでもう BootStrap 使うというのは減少してるかな?所詮、CSS書けない人の為!のコンセプトみたいだし。。)

bg-* color default :hover or :focus
bg-primary #007bff    #0062cc   
bg-secondary #6c757d    #545b62   
bg-success #28a745    #1e7e34   
bg-info #17a2b8    #117a8b   
bg-warning #ffc107    #d39e00   
bg-danger #dc3545    #bd2130   
bg-light #f8f9fa    #dae0e5   
bg-dark #343a40    #1d2124   
bg-white #ffffff   
bg-transparent transparent

カスタマイズとして、任意の bg-* をCSSとして記述すれば、それが利用できる。
例)

<link href="css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
.bg-Cochineal-Red {
  background-color: #ae2b52 !important;
}
a.bg-Cochineal-Red:hover, a.bg-Cochineal-Red:focus,
button.bg-Cochineal-Red:hover,
button.bg-Cochineal-Red:focus {
  background-color: #e390a9 !important;
}
</style>
<nav class="navbar navbar-expand-lg navbar-dark bg-Cochineal-Red">
  <a class="navbar-brand" href="#">Navbar</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
 aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse" id="navbarNav">
    <ul class="navbar-nav">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">Features</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">Pricing</a>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" href="#">Disabled</a>
      </li>
    </ul>
  </div>
</nav>

jsTree で作成したツリー図をPDFにする

jsTree jsTree で作成したツリー図をPDFで出力するのは、jsTreeがHTML表示としたものを
画像データにして、PDF作成のツールで出力ということをしなければなりません。
PDF作成でテンプレートによる作成というのは無理があります。=できない。

【大きな流れ】
jsTree → HTML表示
→ ツリー表示エリアを canvas データに変換、html2canvas で変換します。
canvas データを jsPDF に渡します。
jsPDF で、canvas データをイメージデータとして作成するPDFオブジェクトに
  追加します。
→ jsPDF でダウンロード出力、プレビュー表示。

という流れになります。
jsTreeはツリー図を ul,li 要素で書いているので、jsPDF で HTML変換による
PDF作成だとjsTreeが指定する CSS を読み込まなくてはならず、
jsPDF のサンプルを見るととてもつらいです。
よって、html2canvas を介在する上に書いた流れになります。

ツリー図の表示があって、プレビューのボタンを押すと右横にPDFプレビューが表示される
ものを作成します。

サンプル
f:id:posturan:20181006210522j:plain


HTML は、ツリー表示の div 要素と button があります。

<div id="tree" class="demo"></div>

<button id="preview" type="button">preview PDF</button>

jsTree でのツリー表示の処理は、今回割愛します。
jsTree の他に必要なJS
html2canvas
jsPDF

日本語タイトルを入れたいので jsPDF 日本語の文字が使用できるものをダウンロードして
使います。

https://qiita.com/JunichiWatanuki/items/07bcb842e5532068fd62

https://github.com/JunichiWatanuki/jsPDFjp


ボタンを押して、プレビュー表示させる処理を以下のとおりです。
ツリー図を画像データとして保存する処理は、
http://oboe2uran.hatenablog.com/entry/2018/10/06/162209html2canvas を使って表示HTMLを画像変換してダウンロード - Oboe吹きプログラマの黙示録
のように、PNG画像でしたが、PDF出力で今回は、JPEGです。
そうしないと、作成したPDFを開いた時に
「ページの処理中にエラーが発生しました。
 このドキュメント(110)の読み込み中に問題が発生しました。」

ということになります。

html2canvas version 0.4.1 を使用する場合、

$("#preview").click(function(){
   html2canvas($("#tree"), {
      onrendered : function(canvas){
         var pdf = new jsPDF('p', 'pt', 'a4', false);
         pdf.setFontSize(20);
         pdf.text(10, 50, utf16_to_hexcode('ツリーサンプル'));
         // Tree図 → 横幅を取得して指定
         var width = pdf.internal.pageSize.width * 0.90;
         var x = (pdf.internal.pageSize.width - width) / 2;
         pdf.addImage(canvas, 'JPEG', x, 70, width, 0);

         var iframe = document.createElement('iframe');
         iframe.setAttribute('style','position:absolute;right:0; top:0; bottom:0; height:100%; width:500px');
         document.body.appendChild(iframe);
         iframe.src = pdf.output('datauristring');
      }
   });
});

html2canvas version 1.0.0 alpha 12 を使用する場合、

$("#preview").click(function(){
   html2canvas(document.querySelector("#tree")).then(function(canvas) {
      var pdf = new jsPDF('p', 'pt', 'a4', false);
      pdf.setFontSize(20);
      pdf.text(10, 50, utf16_to_hexcode('ツリーサンプル'));
      // Tree図 → 横幅を取得して指定
      var width = pdf.internal.pageSize.width * 0.90;
      var x = (pdf.internal.pageSize.width - width) / 2;
      pdf.addImage(canvas, 'JPEG', x, 70, width, 0);

      var iframe = document.createElement('iframe');
      iframe.setAttribute('style','position:absolute;right:0; top:0; bottom:0; height:100%; width:500px');
      document.body.appendChild(iframe);
      iframe.src = pdf.output('datauristring');
   });
});

プレビュー表示の為に、iframe で差し込むのです。
最近は、Window open させるということは、初回、ブラウザのポップアップブロックが
あるので iframe の方がよいかもしれません。

プレビューではなく、ボタンクリックでダウンロードの方法は、
jsPDF の save( ダウンロードファイル名 ) です。

html2canvas version 0.4.1 を使用する場合、

$(function(){
   $("#download").click(function(){
      html2canvas($("#tree"), {
         onrendered : function(canvas){
            var pdf = new jsPDF('p', 'pt', 'a4', false);
            pdf.setFontSize(20);
            pdf.text(10, 50, utf16_to_hexcode('ツリーサンプル'));
            // Tree図 → 横幅を取得して指定
            var width = pdf.internal.pageSize.width * 0.90;
            var x = (pdf.internal.pageSize.width - width) / 2;
            pdf.addImage(canvas, 'JPEG', x, 70, width, 0);
            pdf.save('テスト.pdf');
         }
      });
   });
});

html2canvas version 1.0.0 alpha 12 を使用する場合、

$(function(){
   $("#download").click(function(){
      html2canvas(document.querySelector("#tree")).then(function(canvas){
         var pdf = new jsPDF('p', 'pt', 'a4', false);
         pdf.setFontSize(20);
         pdf.text(10, 50, utf16_to_hexcode('ツリーサンプル'));
         // Tree図 → 横幅を取得して指定
         var width = pdf.internal.pageSize.width * 0.90;
         var x = (pdf.internal.pageSize.width - width) / 2;
         pdf.addImage(canvas, 'JPEG', x, 70, width, 0);
         pdf.save('テスト.pdf');
      });
   });
});

html2canvas を使って表示HTMLを画像変換してダウンロード

html2canvas - Screenshots with JavaScript を使います。
HTMLが以下の様に、画像抽出されるエリアと抽出実行制御するボタンがあります。

<div id="tree">この中が画像として抽出される</div>
<div>
   <ul>
      <li><button id="preview" type="button">prev Image</button></li>
      <li><button id="outImage" type="button">Download</button>
          <a id="download" href="#"></a>
      </li>
   </ul>
</div>
<div id="previewImage"></div>

ダウンロードリンクは見えないようにしておきます。この a タグ href にダウンロード用に抽出結果が入ります。

a#download{
   display: none;
}

html2canvas version 0.4.1 を使用する場合、

$(function(){
   /* プレビュー click !!  */
   $("#preview").on('click', function(){
      html2canvas($("#tree"), {
         onrendered : function(canvas){
            $("#previewImage").empty()
            $("#previewImage").append(canvas);
         }
      });
   });
   /* Image コンバート → Download */
   $("#outImage").on('click', function(){
      html2canvas($("#tree"), {
         onrendered : function(canvas){
            var imgageData = canvas.toDataURL("image/png");
            // a id="download" に ダウンロード設定
            $("#download").attr("download", "テスト.png")
            .attr("href", imgageData.replace(/^data:image\/png/, "data:application/octet-stream"));
            // ダウンロード発火
            var evt = document.createEvent("MouseEvents");
            evt.initMouseEvent("click", true, true
, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
            document.getElementById("download").dispatchEvent(evt);
         }
      });
   });
});

html2canvas version 1.0.0 alpha 12 を使用する場合、

$(function(){
   /* プレビュー click !!  */
   $("#preview").on('click', function(){
      html2canvas(document.querySelector("#tree")).then(function(canvas){
         $("#previewImage").empty();
         $("#previewImage").append(canvas);
      });
   });
   /* Image コンバート → Download */
   $("#outImage").on('click', function(){
      html2canvas(document.querySelector("#tree")).then(canvas=>{
         var imgageData = canvas.toDataURL("image/png");
         // a id="download" に ダウンロード設定
         $("#download").attr("download", "テスト.png")
         .attr("href", imgageData.replace(/^data:image\/png/, "data:application/octet-stream"));
         // ダウンロード発火
         var evt = document.createEvent("MouseEvents");
         evt.initMouseEvent("click", true, true
, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
         document.getElementById("download").dispatchEvent(evt);
      });
   });
});

ダウンロード発火を、trigger("click") と書いてしまうと動きません。
IE11 では動きません。
version 0.4.1 を使う場合注意が必要で生成対象のコンテキスト(tag の中の表示)
背景色を指定していないと、背景色が透明の PNG画像になります。

html2canvas onrendered option is deprecated

表示中HTMLのスクリーンショットcanvas tag にイメージ貼り付けしようと
html2canvas を使おうと思い、、
html2canvas - Screenshots with JavaScript
最新バージョンで試すと、
html2canvas: onrendered option is deprecated, html2canvas returns a Promise with the canvas as the value
onrendered オプション:関数定義で書く
バージョン0.4.1 では以下は動いていた。

// elment は、キャプチャ対象のHTMLをセレクタで取得したもの
html2canvas(element, {
     onrendered : function(canvas){
           // id=previewImage に、canvas を付与する
           $("#previewImage").append(canvas);
     }
});

最新のバージョン 1.0.0-alpha 12 では、以下のように書く。

html2canvas(document.querySelector("#tree")).then(function(canvas){
      // id=previewImage に、canvas を付与する
      $("#previewImage").append(canvas);
});

最新のバージョン、現在執筆中の時点ではアルファ版なので、ちょっと使うのをためらう。
html2canvas upgrade has deprecated "onrendered" causing incompatibility · Issue #1601 · MrRio/jsPDF · GitHub

https://github.com/MrRio/jsPDF/issues/1610

CDNサイトを使うならどこがいいんだろう?あまり使いたくないけど。

https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js

https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-alpha.12/dist/npm/index.min.js