動機
Java 7 update 21以降、セキュリティ強化の目的でクライアントサイドでの制約が増え、
Java 7 update 45から未署名のアプレットが動かなくなった。
これに伴い、今までJavaアプレットで印刷していた帳票類が印刷できなくなった。
イントラネット内で使う仕組みだから自己署名証明書で署名すればいいかと思ってたけど、
Java 7 update 51からそれを禁止する予定ということだったので、アプレットを使わない方向に進んでみました。
そうして、実装が終わったところで自己署名証明書を使った署名の方法がアップされて涙目に。
Self-signed certificates for a known community
悔しいので今回の実装を晒してみようと思い、初投稿に至りました。
やりたいこと
Base64変換したPDFをクライアントサイドで処理して印刷
メリット
・アプレット特有のロード時のもっさり感がない。
デメリット
・FirefoxのJS Print Setupプラグインに頼り切りなので公共のサービスに使用できない。
用意するもの
・Firefox
・JS Print Setup
・pdf.js
・node.js <- pdf.js のコンパイルに使用
実装
/**
* PDFページをレンダリングします。
*
* @param viewer
* @param pdf
* @param pageNumber
* @param callback
*/
var renderPage = function(viewer, pdf, pageNumber, ratio, callback) {
pdf.getPage(pageNumber).then(function(page) {
var viewport = page.getViewport(ratio),
pageDisplayWidth = viewport.width,
pageDisplayHeight = viewport.height,
pagePrintWidth = pageDisplayWidth / ratio,
pagePrintHeight = pageDisplayHeight / ratio;
// ページごとに表示させるためにブロック要素で囲む
// 寸法は印刷時の値と等価となるため、PDFページの実寸で定義
var pageDivHolder = $("<div />")
.attr({ "class" : "pdfpage" })
.css({ "width" : pagePrintWidth + "px", "height" : pagePrintHeight + "px" });
viewer.append(pageDivHolder);
// PDFページの寸法を使用して canvas を定義
var canvas = $("<canvas />")
.attr({ "width" : pageDisplayWidth, "height" : pageDisplayHeight })
.css({ "width" : pageDisplayWidth + "px", "height" : pageDisplayHeight + "px" });
var context = canvas[0].getContext("2d");
pageDivHolder.append(canvas);
// PDFページを canvas の描画コンテキストにレンダリングする
var renderContext = {
canvasContext: context,
viewport: viewport
};
page.render(renderContext).promise().then(callback);
});
};
/**
* canvas へPDFページを描画します。<br />
* Base64形式のPDFイメージをページごとに切り出してレンダリングします。
*
* @param img
* @param idoc
*/
var drawCanvas = function(img, idoc, outputRatio) {
var defer = new $.Deferred();
var pdfAsArray = convertDataURIToBinary("data:application/pdf;base64," + img);
PDFJS.getDocument(pdfAsArray).then(function(pdf) {
var viewer = $("body", idoc).css({ "margin":"0px", "border":"0px", "padding":"0px" });
var pageNumber = 1;
renderPage(viewer, pdf, pageNumber++, outputRatio, function pageRenderingComplete() {
if (pageNumber > pdf.numPages) {
defer.resolve(pdf.numPages);
return;
}
renderPage(viewer, pdf, pageNumber++, outputRatio, pageRenderingComplete);
});
});
return defer.promise();
};
/**
* 印刷オプションを設定します。
*/
var setPrintOption = function() {
// set portrait orientation
jsPrintSetup.setOption("orientation", jsPrintSetup.kLandscapeOrientation);
// set top margins in millimeters
jsPrintSetup.setOption("marginTop", 0);
jsPrintSetup.setOption("marginBottom", 0);
jsPrintSetup.setOption("marginLeft", 0);
jsPrintSetup.setOption("marginRight", 0);
// set empty page header
jsPrintSetup.setOption('headerStrLeft', '');
jsPrintSetup.setOption('headerStrCenter', '');
jsPrintSetup.setOption('headerStrRight', '');
// set empty page footer
jsPrintSetup.setOption('footerStrLeft', '');
jsPrintSetup.setOption('footerStrCenter', '');
jsPrintSetup.setOption('footerStrRight', '');
};
/**
* 帳票(A4)を印刷します。
* @param img Base64変換したPDFのイメージ
*/
var printSheet = function(img) {
var defer = new $.Deferred();
var progressListener = {
onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) {
if (aStateFlags == STATE_PRINT_END)
defer.resolve();
return 0;
},
};
var ifm = $("body").append("<iframe style='visibility:hidden; width:0px; height:0px;' />").find("> :last-child"),
idoc = ifm.contents();
idoc[0].open();
idoc[0].close();
drawCanvas(img, idoc, 2.0).then(function(numPages) {
jsPrintSetup.setPrinter("プリンタ名");
setPrintOption();
jsPrintSetup.setPrintProgressListener(progressListener);
jsPrintSetup.setSilentPrint(true);
jsPrintSetup.printWindow(ifm[0].contentWindow);
jsPrintSetup.setSilentPrint(false);
});
return defer.promise();
};
解説
非同期処理の実装
印刷が終わったら画面遷移をさせたかったので、Deferredオブジェクトを定義して
リスナが印刷終了のステータスを拾ったタイミングでresolvedになるように仕込みました。
var progressListener = {
onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) {
if (aStateFlags == STATE_PRINT_END)
defer.resolve();
return 0;
},
};
jsPrintSetup.setPrintProgressListener(progressListener);
印刷対象のオブジェクトを動的に生成
pdf.jsで描画したPDFイメージを動的に生成するために、印刷処理の呼び出しごとにiframeを生成しています。
idoc[0].open(); と idoc[0].close(); の2行が必要な理由は実はイマイチ理解できてません。
ただ、idoc[0].close(); を入れないと上手く動かないのでこうしてます;;
var ifm = $("body").append("<iframe style='visibility:hidden; width:0px; height:0px;' />").find("> :last-child"),
idoc = ifm.contents();
idoc[0].open();
idoc[0].close();
印刷オプションの設定
用紙向きの指定
jsPrintSetup.setOption("orientation", jsPrintSetup.kLandscapeOrientation);
余白の指定
jsPrintSetup.setOption("marginTop", 0);
jsPrintSetup.setOption("marginBottom", 0);
jsPrintSetup.setOption("marginLeft", 0);
jsPrintSetup.setOption("marginRight", 0);
ヘッダーの設定
jsPrintSetup.setOption('headerStrLeft', '');
jsPrintSetup.setOption('headerStrCenter', '');
jsPrintSetup.setOption('headerStrRight', '');
フッターの設定
jsPrintSetup.setOption('footerStrLeft', '');
jsPrintSetup.setOption('footerStrCenter', '');
jsPrintSetup.setOption('footerStrRight', '');
印刷処理の呼び出し
setSilentPrintをtrueにすると、ダイアログなしで印刷が行われます。
// 印刷ダイアログを非表示にする
jsPrintSetup.setSilentPrint(true);
// 引数に指定したwindowを印刷
jsPrintSetup.printWindow(ifm[0].contentWindow);
// 印刷ダイアログを表示する
jsPrintSetup.setSilentPrint(false);
まだまだ書きかけですが、とりあえず更新しました。