タツノオトシゴのブログ

主にJavaに関するものです。

続・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;
        
    }
}