JavaでPDFのページサムネイルを作る

2018/02/08 富澤節由輝
Share on Facebook0Tweet about this on TwitterShare on Google+0Share on LinkedIn0Share on Tumblr0

pdfbox-logo

富澤です。

はじめに

JavaでPDFファイルから縦横比率4:3の各ページのサムネイル画像を作ろうとして試行錯誤したので、サムネイル化の方法を共有したいと思います。

目的

今回の目的は以下の条件を満たす画像をjavaで作成すること。

  • 出力する画像サイズは400px*300px
  • PDFのページの縦横比率は維持し、4:3でない場合は余白をつける

今回はテスト用ということでPowerPointのデフォルトテンプレートから作成した縦横比率16:9のPDFを利用します。
test.pdf

1.PDFBoxを使って画像化する

PDFを取り扱うためのライブラリPDFBoxを利用すれば縮小化した画像を作れるのでは、と思い以下のように実装してみました。
PDFBoxを使った読み込み等はすでに紹介されていますのでご参照ください。
http://javamemo.jpn.org/index.php?%5B%5BPDFBox%5D%5D

		try (PDDocument document = PDDocument.load(new File(pdfPath));) {

			PDFRenderer pdfRenderer = new PDFRenderer(document);

			for (int page = 0; page < document.getNumberOfPages(); ++page) {
				// ページごとの情報を取得する。
				BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
				// 画像をDPI指定してファイルにする
				ImageIOUtil.writeImage(bim, outputPath + (page+1) + ".png", DPI);
			}
		} catch (IOException e) {
			// エラー時処理
		}

ImageIOUtil.writeImageを使用して画像ファイルを作成することはできました。
また、DPIの値を小さくすればより小さい画像を作成できます。
しかし、これではDPI(dot per inch)で解像度を指定するため任意のサイズの画像を得られませんでした。

2.画像を任意のサイズにリサイズする

1.の処理でBufferedImageにPDFを画像のデータとして保持することはできているので出力方法を独自に変えてみました。

		try (PDDocument document = PDDocument.load(new File(pdfPath));) {

			PDFRenderer pdfRenderer = new PDFRenderer(document);

			for (int page = 0; page < document.getNumberOfPages(); ++page) {
				// ページごとの情報を取得する。
				BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
				// 取得した画像データをリサイズする
				ImageFilter filter = new ReplicateScaleFilter(400, 300);
				ImageProducer imageProducer = new FilteredImageSource(bim.getSource(), filter);
				java.awt.Image dstImage = Toolkit.getDefaultToolkit().createImage(imageProducer);

				// 画像ファイル出力時に使うにBufferを作成
				BufferedImage dst = new BufferedImage(400, 300, BufferedImage.TYPE_INT_RGB);

				// 背景色のみの画像データを作成する
				Graphics2D g = dst.createGraphics();
				// リサイズした画像を反映させる。
				g.drawImage(dstImage, 0, 0, null);
				g.dispose();

				// リサイズした画像をファイルに出力する
				FileOutputStream fos = new FileOutputStream(page + ".png");
				ImageOutputStream ios = ImageIO.createImageOutputStream(fos);
				ImageIO.write(dst, "PNG", ios);
			}
		} catch (IOException e) {
			// エラー時処理
		}

この実装で出力した結果がこちら。
test1
画像は確かに400px*300pxのサイズになっていますが、元画像が引き伸ばされてしまっています。
これでは今回の目的を達成できていません。

3.問題点

2.までで画像をリサイズすることはできていますが、リサイズするときのサイズが間違っていました。
また、余白をつける必要があるが、その処理がありません。

4.解決策

今回の肝の部分です。

  • 元画像と指定サイズを比較して拡大率を算出し、縦横比率を維持したままリサイズできるようにする
  • 	private double getScale(double width, double height) {
    		double imageRetio = height / width;
    		double scale = 0;
    
    		if (imageRetio <= RETIO) {
    			scale = IMAGE_WIDTH / width;
    		} else {
    			scale = IMAGE_WIDTH * RETIO / height;
    		}
    		return scale;
    	}
    
  • 領域平均化アルゴリズムを利用して元画像をリサイズする
  • 		ImageFilter filter = new AreaAveragingScaleFilter(scaledWidth, scaledHeight);
    		ImageProducer imageProducer = new FilteredImageSource(in.getSource(), filter);
    		java.awt.Image dstImage = Toolkit.getDefaultToolkit().createImage(imageProducer);
    

    領域平均化アルゴリズム(AreaAveragingScaleFilter)を使う事で画像の色の境界が滑らかになり、ジャギーが減り、画質が向上します。
    ただし、処理が重いので他のScaleFilterを使った時より速度が若干遅くなります。

  • サムネイル画像サイズのBufferedImageを新たに作成し、背景色を設定する
  • 		BufferedImage dst = new BufferedImage((int) IMAGE_WIDTH, (int) (IMAGE_WIDTH * RETIO),
    				BufferedImage.TYPE_INT_RGB);
    		Graphics2D g = dst.createGraphics();
    		g.setBackground(Color.WHITE);
    		g.clearRect(0, 0, (int) IMAGE_WIDTH, (int) (IMAGE_WIDTH * RETIO));
    
  • 作成したBufferedImageにリサイズした画像を上から重ねる
  • 		int x = ((int) IMAGE_WIDTH - scaledWidth) / 2;
    		int y = ((int) (IMAGE_WIDTH * RETIO) - scaledHeight) / 2;
    		g.drawImage(dstImage, x, y, null);
    

    この時に重ねる画像の位置のx座標、y座標を指定する事で上下左右に余白があるような画像が作れます。
    位置指定と拡大率を変えれば画像の一部だけ拡大表示したものも作成可能です。

  • 最後にサムネイル画像サイズのBufferedImageをファイルへ出力して完成
  • 				BufferedImage resizedBim = scaleImage(bim);
    
    				FileOutputStream fos = new FileOutputStream(page + ".png");
    
    				ImageOutputStream ios = ImageIO.createImageOutputStream(fos);
    				ImageIO.write(resizedBim, "PNG", ios);
    

5.最終形

以上を踏まえリサイズ時のサイズ指定、リサイズした画像を反映させる時の位置指定を変更し、たどり着いた最終形の実装が以下となります。

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.image.AreaAveragingScaleFilter;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.awt.image.ImageProducer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageOutputStream;

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

public class PdfToImage {

	// サムネイル画像の縦横比率
	private static final double RETIO = 0.75;
	// サムネイル画像の幅(400px)
	private static final double IMAGE_WIDTH = 400;

	/**
	 * PDFファイルを画像化する
	 * 
	 * @param pdfPath
	 *            PDFへのパス
	 * @return
	 */
	public void pdfToImage(String pdfPath) {

		// PDFドキュメントをロード
		try (PDDocument document = PDDocument.load(new File(pdfPath));) {

			PDFRenderer pdfRenderer = new PDFRenderer(document);

			for (int page = 0; page < document.getNumberOfPages(); ++page) {
				// ページごとの情報を取得する。
				BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);

				BufferedImage resizedBim = scaleImage(bim);

				FileOutputStream fos = new FileOutputStream(page + ".png");

				ImageOutputStream ios = ImageIO.createImageOutputStream(fos);
				ImageIO.write(resizedBim, "PNG", ios);

			}
		} catch (IOException e) {
			// エラー時処理
		}
	}

	/**
	 * 縦横比を維持したまま画像サイズを変更し、上下または左右に余白をつける
	 * 
	 * @param in
	 *            元画像データ
	 * @return リサイズした画像データ
	 */
	public BufferedImage scaleImage(BufferedImage in) {
		double scale = getScale((double) in.getWidth(), (double) in.getHeight());
		int scaledWidth = (int) (in.getWidth() * scale + 0.5);
		int scaledHeight = (int) (in.getHeight() * scale + 0.5);

		// リサイズ
		// 領域平均化アルゴリズムを利用
		// https://docs.oracle.com/javase/jp/1.3/api/java/awt/image/AreaAveragingScaleFilter.html
		ImageFilter filter = new AreaAveragingScaleFilter(scaledWidth, scaledHeight);
		ImageProducer imageProducer = new FilteredImageSource(in.getSource(), filter);
		java.awt.Image dstImage = Toolkit.getDefaultToolkit().createImage(imageProducer);

		// 画像ファイル出力時に使うにBufferを作成。画像は縦横比がRETIOと一致するようにする
		BufferedImage dst = new BufferedImage((int) IMAGE_WIDTH, (int) (IMAGE_WIDTH * RETIO),
				BufferedImage.TYPE_INT_RGB);

		// 背景色のみの画像データを作成する
		Graphics2D g = dst.createGraphics();
		// 背景色を白に塗り替える
		g.setBackground(Color.WHITE);
		g.clearRect(0, 0, (int) IMAGE_WIDTH, (int) (IMAGE_WIDTH * RETIO));

		// 画像の左上の位置を指定して中央に配置し、余白をつける。
		// x座標の位置(左右の位置)
		int x = ((int) IMAGE_WIDTH - scaledWidth) / 2;
		// y座標の位置(上下の位置)
		int y = ((int) (IMAGE_WIDTH * RETIO) - scaledHeight) / 2;
		// リサイズした画像を反映させる。
		g.drawImage(dstImage, x, y, null);
		g.dispose();
		return dst;
	}

	/**
	 * 画像の拡大率を取得する
	 * 
	 * @param width
	 *            元画像の横幅
	 * @param height
	 *            元画像の縦幅
	 * @return 拡大率
	 */
	private double getScale(double width, double height) {
		double imageRetio = height / width;
		double scale = 0;

		if (imageRetio <= RETIO) {
			// 指定比率よりも横幅が大きい時、拡大率は幅に合わせる
			scale = IMAGE_WIDTH / width;
		} else {
			// 指定比率よりも横幅が小さい時、拡大率は高さに合わせる
			scale = IMAGE_WIDTH * RETIO / height;
		}
		return scale;
	}
}

実行結果

16:9で作成したPDFを無事に4:3のサムネイル画像へ変換することに成功しました。
0

いかがでしたでしょうか。
縦横比や出力画像のサイズを変更する事で自由に画像を出力できるようになりました。
また、PDFから画像を取得しなくても、すでにある画像ファイルを読み込んでリサイズする事も可能です。
機会があればお試しください。

Share on Facebook0Tweet about this on TwitterShare on Google+0Share on LinkedIn0Share on Tumblr0