続・PDF を Bitmap にラスタライズする

前回の続き。

前回のあらすじ

やりたかったこと

PDF ドキュメントを 1 ページずつバラして、ビットマップの連番ファイルに保存するプログラムJava で書きたい。

前回やったこと

  • Apache PDFBox + RxJava で適当に書いた。でも適当すぎた

今回やったこと

  • Subscriber 側のコードをすっきりさせたよ。

Main.java:

import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.exceptions.Exceptions;
import io.reactivex.schedulers.Schedulers;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.IntStream;

import javax.imageio.ImageIO;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        String path = "/Users/tercel/Documents/designPattern.pdf";

        PdfRasterizer.rasterize(path, ".")
            .subscribeOn(Schedulers.computation())       // 別スレッドで並列で動かす
            .doOnComplete(() -> System.out.println("BMP生成完了"))  // 完了時の処理
            .doOnError(e -> e.printStackTrace())                  // エラー時の処理
            .subscribe(System.out::println);    // BMPが生成されるたび呼ばれる処理
                                                //(引数は生成ファイルのフルパス)

        Thread.sleep(5_000L); // しばらく待つ
        System.out.println(Thread.currentThread().getName() + "メインスレッド終了");
    }
}

class PdfRasterizer {
    /**
     * 指定した PDF ファイルをビットマップに分解しつつ、
     * 分解後のビットマップファイルのフルパスをひたすら垂れ流す Flowable を返す。
     * 
     * @param pdfPath 入力 PDF ファイルのパス
     * @return Flowable
     */
    public static Flowable<String> rasterize(final String pdfPath){
        return rasterize(pdfPath, Paths.get(pdfPath).getParent().toString());
    }
    
    /**
     * 指定した PDF ファイルをビットマップに分解しつつ、
     * 分解後のビットマップファイルのフルパスをひたすら垂れ流す Flowable を返す。
     * 
     * @param pdfPath 入力 PDF ファイルのパス
     * @param outputDirectoryPath 出力 BMP ファイルのパス
     * @return Flowable
     */
    public static Flowable<String> rasterize(final String pdfPath, final String outputDirectoryPath) {
        final int defaultResolution = 300;
        return rasterize(pdfPath, outputDirectoryPath, defaultResolution);
    }

    /**
     * 指定した PDF ファイルをビットマップに分解しつつ、
     * 分解後のビットマップファイルのフルパスをひたすら垂れ流す Flowable を返す。
     * 
     * @param pdfPath 入力 PDF ファイルのパス
     * @param outputDirectoryPath 出力 BMP ファイルのパス
     * @param dpi 解像度
     * @return Flowable
     */
    public static Flowable<String> rasterize(final String pdfPath, final String outputDirectoryPath, final int dpi) {
        return Flowable.create(emitter -> {
            try {
                if(!checkArgs(pdfPath, outputDirectoryPath, dpi)) return;
                if(!tryMkDir(outputDirectoryPath)) throw new IOException("フォルダの作成に失敗したよ");

                try(final InputStream in = new FileInputStream(pdfPath);
                        final PDDocument doc = PDDocument.load(in)) {
                    final PDFRenderer pdfRenderer = new PDFRenderer(doc);

                    IntStream.range(0, doc.getNumberOfPages()).forEach(i -> {
                        if(emitter.isCancelled()) return;
                        final Path outPath = Paths.get(outputDirectoryPath, 
                                String.join("", getFileNameWithoutExtention(Paths.get(pdfPath)),
                                        "_", String.format("%05d", i), ".bmp"));

                        try(final OutputStream out = Files.newOutputStream(outPath)) {
                            ImageIO.write(pdfRenderer.renderImageWithDPI(i, dpi, ImageType.RGB), "bmp", out);
                            emitter.onNext(outPath.toRealPath().toAbsolutePath().toString());
                        } catch(IOException e) {
                            throw new RuntimeException(e);
                        };
                    });

                    emitter.onComplete();
                }
            } catch(Throwable t) {
                Exceptions.throwIfFatal(t);
                emitter.onError(t);
            }
        }, BackpressureStrategy.BUFFER);
    }

    /**
     * 引数チェック。妥当な引数の場合は true, それ以外は例外をスローし異常終了
     * @param pdfPath 入力 PDF のパス
     * @param outputDirectoryPath 出力ディレクトリパス
     * @param dpi 解像度
     * @return
     * @throws FileNotFoundException
     */
    private static boolean checkArgs(String pdfPath, String outputDirectoryPath, int dpi) throws FileNotFoundException {
        if(!isPDF(pdfPath)) { throw new IllegalArgumentException("PDFじゃないよ"); }
        if(!exists(pdfPath)) { throw new FileNotFoundException("そんなPDFないよ"); }
        if(!isDirectory(outputDirectoryPath)) { throw new IllegalArgumentException("フォルダが指定されてないよ"); }        
        if(dpi < 1) { throw new IllegalArgumentException("解像度の指定がおかしいよ"); }        

        return true;
    }

    /**
     * 指定されたパスが表すファイルが PDF かどうかを判定する。
     * @param path ファイルパス
     * @return ファイルが PDF の場合は true, それ以外は false
     */
    private static boolean isPDF(String path) {        
        return path != null && path.toLowerCase().endsWith(".pdf");
    }

    /**
     * 指定されたファイルまたはディレクトリが存在するかどうかを判定する。
     * @param path ファイルパス
     * @return ファイルまたはディレクトリが存在する場合は true, それ以外は false
     */
    private static boolean exists(String path) {
        return Files.exists(Paths.get(path));
    }

    /**
     * 指定されたパスがディレクトリかどうかを判定する。
     * @param path ファイルパス
     * @return ディレクトリの場合は true, それ以外は false
     */
    private static boolean isDirectory(String path) {
        return path != null && Files.isDirectory(Paths.get(path));
    }

    /**
     * 指定されたパスのうち、拡張子を除いたファイル名を返却する。
     * @param filePath ファイルパス
     * @return 拡張子を除いたファイル名
     */
    private static String getFileNameWithoutExtention(Path filePath) {
        final String fileName = filePath.getFileName().toString();
        final int point = fileName.lastIndexOf('.');
        return 0 < point ? fileName.substring(0, point) : fileName;
    }

    /**
     * ディレクトリの作成を試みる。同名のディレクトリが既に存在する場合は何もしない。
     * また、ディレクトリの作成に失敗した場合も例外をスローしない。
     * @param dirPath ディレクトリパス
     * @return ディレクトリの作成に成功した場合、または既に同名のディレクトリが存在する場合は true, それ以外は false
     */
    private static boolean tryMkDir(String dirPath) {
        if (dirPath == null) {
            return false;
        }

        final Path ret = Paths.get(dirPath);
        if(Files.notExists(ret)) {
            try {
                Files.createDirectories(ret);
            } catch(IOException e) {
                e.printStackTrace();
                return false;
            }
        } else if(!Files.isDirectory(ret)) {
            return false;
        }
        return true;
    }        
}