💭

PDFを画像化する選択肢(pdf2image, PyMuPDF, Ghostscript)

に公開

一言でいうと

昨今、OCRなどを目的としてPDFを画像化するシーンが増えていますが、ライブラリによって速度・描画忠実度など挙動が異なります。
本記事では、Pythonから利用可能な代表的手法である pdf2image / PyMuPDF / Ghostscript の3種類を紹介の上、比較します。

概要

今回扱うライブラリはこちらです。

ライブラリ 内部エンジン 特徴 ライセンス
pdf2image Poppler Linux系で広く利用 MIT License (PopplerはGPL)
PyMuPDF MuPDF 高速・軽量 AGPL
Ghostscript Ghostscript 印刷互換性重視 AGPL

pdf2imageとPyMuPDFはPythonライブラリですが、正確にはGhostscriptはCLIツールなのでPythonから外部プロセスとして利用します。

インストール

前提

本検証は下記環境で実行しました。

対象 バージョン
OS Ubuntu 22.04
Python 3.10.12
pdf2image 1.17.0
poppler-utils 22.02.0
PyMyPDF 1.24.4
Ghostscript 9.55.0

pdf2image

apt install poppler-utils
pip install pdf2image

PyMuPDF

pip install PyMuPDF

Ghostscript

apt install ghostscript

使用方法

pdf2image

https://pdf2image.readthedocs.io/en/latest/index.html

def render_by_pdf2image(pdf_path: Path, out_path: Path, dpi: int, target_page: int):
    imgs = convert_from_path(
        pdf_path,
        dpi=dpi,
        first_page=target_page+1,
        last_page=target_page+1,
        fmt="png"
    )
    imgs[0].save(out_path, "PNG")

first_pageは変換の開始ページ、last_pageは最終ページで、どちらも1スタートで指定します。

PyMuPDF

https://pymupdf.readthedocs.io/ja/latest/

def render_by_pymupdf(pdf_path: Path, out_path: Path, dpi: int, target_page: int, aa_level: int):
    doc = pymupdf.open(pdf_path)
    pymupdf.TOOLS.set_aa_level(aa_level)
    page = doc.load_page(target_page)

    pix = page.get_pixmap(dpi=dpi, alpha=False)
    pix.save(str(out_path))
    doc.close()

PyMuPDFの場合は0スタートでページを指定します。また、以前はDPIを直接指定することができませんでしたが、v1.19.2以降はget_pixmapの引数として指定することができます。

Ghostscript

https://ghostscript.com/documentation

def render_by_gs(pdf_path: Path, out_path: Path, dpi: int, target_page: int, aa_level_text: int, aa_level_graphics: int):
    first = target_page + 1
    last = target_page + 1
    cmd = [
        "gs",
        "-dSAFER", "-dBATCH", "-dNOPAUSE",
        f"-r{dpi}",
        "-sDEVICE=png16m",
        f"-dFirstPage={first}",
        f"-dLastPage={last}",
        f"-dTextAlphaBits={aa_level_text}",
        f"-dGraphicsAlphaBits={aa_level_graphics}",
        f"-sOutputFile={str(out_path)}",
        pdf_path
    ]
    subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

ページの指定はpdf2imageと同じです。

比較

3種類のライブラリについて、画質と変換時間を評価します。

画質の評価指標として、今回はSSIM(structural similarity)を利用します。基準となる画像とどれだけ類似しているかを表現する指標の一つです。基準の画像としては、pdf2imageによるDPI800の画像としました。つまり恣意的ですが、pdf2imageは多少ゲタを履くことになります。

対象のPDFは、「Attention Is All You Need」の論文PDFとしました。

DPIを[72, 100, 150, 200, 300, 400, 600]と変動させたときのSSIMと処理時間をプロットした図です。縦軸がSSIM、横軸が処理時間を示すので、左上に位置するほど優秀と言えます。

処理時間は低DPIの場合は差は小さいですが、高DPIになるとpdf2imageの遅さが目立ちます。
比較結果をみると、PyMuPDF > Ghostscript > pdf2image に見えます。

実際の画像を見てみます。上段左がPyMuPDF、上段右がGhostscript、下段がpdf2imageです。

72DPI

150DPI

300DPI

300DPIにもなると人の目には違いが分かりにくいですが、私の主観では下記のように感じます。

  • pdf2image:最もパキっとしている
  • PyMuPDF:少しぼやっとしている
  • Ghostscript:低DPIでジャギジャギしている

アンチエイリアス

アンチエイリアスは、ピクセルの境界を中間色にすることで人の目に「滑らか」に見せる描画処理です。PDFのようなベクター形式をラスタライズする際には、特に文字や細線の見え方に大きく影響します。

アンチエイリアスがないと、斜め線や曲線のギザギザ(ジャギー)を目立ってしまいます。アンチエイリアスを掛けることで見た目が滑らかになり文字が読みやすくなりますが、掛けすぎると輪郭がぼやけたりもします。

PyMuPDFではset_aa_levelで独自の設定値を指定することができます。デフォルトではアンチエイリアスがかかっていますが、なくすと右図のようにギザギザしてみえます。

GhostscriptではTextAlphaBits / GraphicsAlphaBitsで制御できます。デフォルトでは1(最低レベル)のため、ジャギジャギして見えたわけです。

pdf2imageでは明示的に制御することはできませんが、画像を見るとある程度かかっているようには見えます。

先ほど各ライブラリの画質を私の主観で記載しましたが、これはあくまでデフォルトのアンチエイリアス設定の場合であり、本来は用途に合わせたチューニングが必要となります。

まとめ

PDFを画像化するライブラリを3つ紹介しました。

画質・処理時間の比較結果からは、ライセンスが許せばとりあえずPyMuPDFを試し、細かな設定が必要な場合はGhostscriptを検討するのが現実的だと思います。

ご参考になれば幸いです。

Discussion