続・JavaFX8の印刷機能によるHTMLのPDF変換
以前の記事では失敗した、Java8の印刷機能を使ったHTMLのPDF変換を再度試してみた。
結果、上手くいきました。
【環境】
- OS:CentOS6.6 64bit
- kernel:2.6.32-504.el6.x86_64
- Java:build 1.8.0_25-b17
- 仮想プリンタ:cups-pdf(cups-pdf-2.6.1-4.el6.x86_64)
【はまったこと】
WebViewノードを印刷するときに、一般的な方法であるノードの印刷方法(PrinterJob#printPage)を使用すると、複数ページにまたがる場合や、縮小して印刷する際には自前で設定する必要がある。
専用のメソッドWebEngine#print()を使うと、自動的に縮小したりしてくれる。
さらに、CSSの印刷用メディアにも対応してくれる。
JavaFXの公式ページにも、このことは載っていました。
専用の「WebEngine#print」を使用した印刷
- cups-pdfを使用した場合は、ダイアログなしても印刷できる。
- 出力ファイル名は、プリンタのジョブ名になるため、ジョブ名を指定する。
- 今回は、デフォルトのプリンタをcups-pdfにしているが、通常は「Printer#getAllPrinters()」で全てのプリンタを取得して、目的のプリンタを取得する。
import java.io.Serializable; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker; import javafx.print.PageLayout; import javafx.print.PageOrientation; import javafx.print.Paper; import javafx.print.Printer; import javafx.print.PrinterJob; import javafx.scene.Scene; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; import javafx.stage.Window; public class Html2PdfApplication0 extends Application { public static void main(String[] args) { String url = "http://www.yahoo.co.jp/"; // url = "http://ja.wikipedia.org/wiki/JavaFX"; try { launch(new String[] {"--url=" + url}); } catch (Exception e) { e.printStackTrace(); } } @Override public void start(final Stage primaryStage) throws Exception { final Application.Parameters params = getParameters(); final String url = params.getNamed().get("url"); final WebView webView = new WebView(); final WebEngine engine = webView.getEngine(); webView.autosize(); final Scene scene = new Scene(webView); primaryStage.setScene(scene); engine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() { @Override public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) { if(newState == Worker.State.SUCCEEDED) { System.out.println("complete load."); try { saveNodeAsPdf(webView); // saveNodeAsPdfWithMultiple(webView, scene); } finally { // 印刷したらアプリケーションの終了 Platform.exit(); } } } }); engine.load(url); primaryStage.show(); } /** * 一般的な印刷用メディアで印刷する。 * @param webView */ private void saveNodeAsPdf(final WebView webView) { System.out.println("==== start print pdf"); // プリンタの取得 final Printer printer = Printer.getDefaultPrinter(); if(printer == null) { System.err.println("printe is null"); return; } else { System.out.println("print name:" + printer.getName()); } // レイアウトの設定 PageLayout pageLayout = printer.createPageLayout(Paper.A4, PageOrientation.PORTRAIT, Printer.MarginType.DEFAULT); // Printerジョブの取得。 // ジョブ名が出力するファイル名となるため変更する。 PrinterJob job = PrinterJob.createPrinterJob(printer); job.getJobSettings().setJobName("javafx2pdf"); System.out.printf("jobName [%s]\n", job.getJobSettings().getJobName()); if(job != null) { // CSSの印刷用メディア印刷するには、WebEngin#printを使用する。 webView.getEngine().print(job); job.endJob(); } System.out.println("==== end print pdf"); } }
一般的な「PrinterJob#printPage」を使用した印刷
非常に面倒です。
- PrinterJob#printPageメソッドは、表示されているそのままを印刷するメソッドのため、印刷使用とするページサイズに収まるように縮小表示する必要がある。
- さらにStage(Window)のウィンドウサイズを調整する必要がある。
- 現在のWebページのサイズやスクロール表示を考慮する必要がある。
- Webページのサイズは、WebEngine#executeScript()を使って、JavaScriptで取得する必要がある。
- 複数ページ印刷する場合、WebViewをスクロールする必要がある。
- WebViewは自動的にスクロールバーが表示されるが、これはJavaFXのScrollPaneではなく、WebKitの部品のものなので、JavaScriptでスクロールする必要がある。
非常に面倒ですが、WebView#snapshot()でWebページを画像として保存するも同様に、このような方法で色々と考慮する必要があります。
public class Html2PdfApplication0 extends Application { // 呼び出す部分は省略 /** * WebViewのノードに対して印刷する。 * 自分で縮小、スクロールする必要がある。 * @param webView * @param scene */ private void saveNodeAsPdfWithMultiple(final WebView webView, final Scene scene) { System.out.println("==== start print pdf"); /* * スクロールバーの非表示 * CSSは下記の内容 * body {overflow-x: hidden; overflow-y: hidden;} */ webView.getEngine().setUserStyleSheetLocation(getClass().getResource("no-scroll.css").toExternalForm()); // プリンタの取得 final Printer printer = Printer.getDefaultPrinter(); if(printer == null) { System.err.println("printe is null"); return; } else { System.out.println("print name:" + printer.getName()); } // 現在のウィンドウ情報を取得 final WebEngine engine = webView.getEngine(); WebViewSize viewSize = printWindowInfo(engine); // レイアウトの設定 PageLayout pageLayout = printer.createPageLayout( Paper.A4, PageOrientation.PORTRAIT, Printer.MarginType.DEFAULT ); // 用紙に合わせてWebViewを縮小表示する double scaleX = (pageLayout.getPrintableWidth() - pageLayout.getLeftMargin() - pageLayout.getRightMargin() ) / webView.getBoundsInParent().getWidth(); webView.setZoom(scaleX); System.out.printf("zoom size=%f\n", scaleX); // ウィンドウの横サイズを調整し、全て表示できるようにする Window stage = scene.getWindow(); int scrollBarWidth = 20; double windowWidth = viewSize.documentWidth * scaleX + scrollBarWidth; stage.setWidth(windowWidth); // Printerジョブの取得。 // ジョブ名が出力するファイル名となるため変更する。 final PrinterJob job = PrinterJob.createPrinterJob(printer); job.getJobSettings().setJobName("javafx2pdf"); System.out.printf("jobName [%s]\n", job.getJobSettings().getJobName()); if(job != null) { // 印刷するページ数のトータルページの計算 viewSize = printWindowInfo(engine); // 縮小表示した場合、現在のサイズを再取得する int pages = viewSize.documentHeight / viewSize.windowHeight; if(viewSize.documentHeight % viewSize.windowHeight != 0) { pages++; } // 複数ページの印刷 boolean success = false; for(int i=0; i < pages; i++) { if(i > 0) { // JavaScriptを使ってスクロール int scolled = i*viewSize.windowHeight; engine.executeScript(String.format("window.scrollTo(%d, %d);", 0, scolled)); } success = job.printPage(pageLayout, webView); System.out.printf("... page[%d/%d] - print result=%b\n", i+1, pages, success); if(!success) { job.endJob(); break; } } if(success) { job.endJob(); } } System.out.println("==== end print pdf"); } /** * JavaScriptで画面サイズ情報などを取得する * http://archiva.jp/web/javascript/get_page-size.html * */ WebViewSize printWindowInfo(WebEngine engine) { System.out.println("-------------------------"); // ウィンドウサイズの取得 int windowWidth = (Integer) engine.executeScript("document.documentElement.clientWidth || document.body.clientWidth || document.body.scrollWidth;"); int windowHeight = (Integer) engine.executeScript("document.documentElement.clientHeight || document.body.clientHeight || document.body.scrollHeight;"); System.out.printf("windowSize : width=%d, height=%d\n", windowWidth, windowHeight); // ドキュメントサイズの取得 int documentWidth = (Integer) engine.executeScript("document.documentElement.scrollWidth || document.body.scrollWidth;"); int documentHeight = (Integer) engine.executeScript("document.documentElement.scrollHeight || document.body.scrollHeight;"); System.out.printf("documentSize : width=%d, height=%d\n", documentWidth, documentHeight); // スクロール位置 int scrollWidth = (Integer) engine.executeScript("document.documentElement.scrollLeft || document.body.scrollLeft;"); int scrollHeight = (Integer) engine.executeScript("document.documentElement.scrollLeft || document.body.scrollLeft;"); System.out.printf("scrollPosition : width=%d, height=%d\n", scrollWidth, scrollHeight); System.out.println("-------------------------"); WebViewSize size = new WebViewSize(); size.windowWidth = windowWidth; size.windowHeight = windowHeight; size.documentWidth = documentWidth; size.documentHeight = documentHeight; size.scrollWidth = scrollWidth; size.scrollHeight = scrollHeight; return size; } public static class WebViewSize implements Serializable { public int windowWidth; public int windowHeight; public int documentWidth; public int documentHeight; public int scrollWidth; public int scrollHeight; } }