7839

雑草魂エンジニアブログ

【Python】既存の PDF に画像を挿入する(PyPDF2 / ReportLab)

あるアプリケーション内で、Python を用いて Excel ファイルを作成し、その Excel ファイルを LibreOffice で PDF に変換していた。Excel のヘッダー/フッターには、文字・ロゴ画像を挿入していたが、LibreOffice で PDF 変換時に、なぜかヘッダーの画像だけが変換されないエラーが発生した。そこで、今回既存のPDFファイルに画像を挿入する機能を実装したので、備忘録として残しておく。

バージョン:Python-3.8

PyPDF2 / ReportLabとは

今回は、PyPDF2 と ReportLab の二つのライブラリを使って、実装を行うことにした。

Python の PDF 生成には様々な方法が存在するが、日本語の使用を前提とした場合、実績のあるライブラリは ReportLab であるように思えたので、今回選定に至った。

そして、PyPDF2 は日本語を含む PDF 生成の場合は文字化けなどが発生しやすいようであるが、PDFページの結合、回転など他のライブラリではできない処理を行うことができる。

今回の実装では、既存のPDFファイルの各ページのヘッダーの位置にロゴ画像を挿入することが目的のため、合成などせずに、各ページに直接ロゴ画像を挿入することもできた。しかしながら、同じロゴ画像を全てのページに挿入するのであれば、一度 ReportLab で PDF ファイルを作成しておけば、合成する処理のみで済むので、今回は合成する手段を選んだ。

インストール

pip で簡単にインストールすることができる。

pip install pypdf2
pip install reportlab 

画像を含んだ PDF 生成

ReportLab を用いて、PDFの生成を行う。メソッドの詳細は、ユーザーガイド を参照してほしい。

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm

def create_header_logo_PDF():
    # 新規PDF作成
    pdf_canvas = canvas.Canvas('pdf_template/header_logo.pdf', pagesize=A4)
    # 画像を挿入する
    target_x, target_y = 10*mm, 277*mm
    pdf_canvas.drawImage('report-img/_logo_header.jpg', target_x, target_y)
    # 文字を挿入したい場合は、以下。
    # pdf_canvas.drawString(target_x, target_y,"Hello World")

    # PDF保存
    pdf_canvas.save()

canvas のデフォルトは、A4の設定になっているが、公式ドキュメントにもあるように、明示的に示すことがオススメされている。(A4サイズは、 210mm × 297mm となっている。)また、ReportLab のデフォルトの座標系は、紙の左下が原点となっているので、画像の位置を設定する場合は注意が必要である。

PDF 合成

PyPDF2 を用いて、PDFの合成を行い、新規 PDF ファイルを作成する。上記で作成した PDF ファイルを読み込んで、input_file_path で指定したPDFファイルに合成して、output_file_path に出力する。

from PyPDF2 import PdfFileWriter, PdfFileReader
def merge_pdf(input_file_path, output_file_path):
    # ヘッダーにロゴアイコンがあるPDF全体の読み込み
    header_logo_pdf = PdfFileReader(open("pdf_template/header_logo.pdf", "rb"))
 # ロゴアイコンがあるページ読み込み
    header_logo_page = header_logo_pdf.getPage(0)
    # 既存のファイル読み込み
    input_file = PdfFileReader(open(input_file_path, 'rb'), strict=False)
    # 既存のファイルのページ数を取得する
    page_count = input_file.getNumPages()
    # 新規の出力ファイル作成
    output_file = PdfFileWriter()
    # 既存の全体ページをループで回す
    for page_number in range(page_count):
        input_page = input_file.getPage(page_number)
        # トップページ以外
        if page_number:
            # 既存のページとヘッダーロゴをmergeする
            input_page.mergePage(header_logo_page)
            # 圧縮する
            input_page.compressContentStreams()
        # 出力ファイルにページを追加する
        output_file.addPage(input_page)
    # 出力ファイル保存
    with open(output_file_path, "wb") as outputStream:
        output_file.write(outputStream)

また、今回 merge した際に、かなり容量が増えたので、compressContentStreams() で圧縮処理を行うようにした。容量を確認した結果を以下に示す。

  • 元のPDFファイル:2.3M
  • PyPDF2で一度読み込み、再出力:2.3M
    • 変化なしで、処理として問題ないことが確認できた。
  • PyPDF2で12KBのPDFファイルと各ページをmergeしていく:26M
    • mergeページ数313ページなので、単純計算で 313 × 12K = 約3.7M 増加するはずなので、2.3M + 3.7M = 6M になる予定であった。
    • mergePageではあまりに大きくなりすぎる問題が発生。
  • page.mergePage()をしてから、page.compressContentStreams()することで圧縮する:2.5M
    • きちんと圧縮できて、ひと安心。

(おまけ)Doker環境

今回、私は Dcoker環境で開発を行っていたので、ReportLab を追加した。

FROM python:3.9-alpine3.12
# 省略
RUN apk --update add py3-reportlab

Pillowインストール時に以下のエラーが発生した。

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/tmp/pip-install-_ufmrkz3/pillow_ab01fc49d3214bf78846d96525b3b440/setup.py", line 922, in <module>
    raise RequiredDependencyException(msg)
    __main__.RequiredDependencyException:
    The headers or library files could not be found for jpeg,
    a required dependency when compiling Pillow from source.

調べてみると、jpeg-dev zlib-dev のパッケージを追加する必要があるようであった。また、py3-reportlabでインストールされる Pillow のバージョンが 7.1 だったので、Python3.8までしか対応していないことがわかったので、Pythonのバージョンを下げることにした。

FROM python:3.8-alpine3.12
# 省略
RUN apk --update add py3-reportlab jpeg-dev zlib-dev

これで無事に、ReportLab が使えるようになった。

まとめ

無事に、既存の PDF ファイルの各ページのヘッダーにロゴ画像を追加することができた。

PyPDF2 を用いることで、簡単に PDF を合成することができたので、PDF にウォーターマークを入れたい場合などにも応用できると思えた。

それでは、ステキな開発ライフを。

関連記事

serip39.hatenablog.com

serip39.hatenablog.com