Pythonで既存PDFに文字(ページ番号)を追加する方法

今回は、既存のPDF文書にページ番号を追加するプログラムをPythonで作成します。

プログラムのポイントは、既存のPDFにページ番号を直接追加することは難しいので、まずページ番号だけのPDFをメモリ上に作成し、それを上から重ね合わせることで可能にするところです。

手順としては以下のようになります。

  1. ReportLabを用いてページ番号だけのPDFをメモリ上に作成する。
  2. PyPDF2を用いてページ番号だけのPDFをメモリから読み込む。
  3. PyPDF2を用いて既存のPDFの上にページ番号だけのPDF重ね合わせる

ここで、外部のライブラリには、重ね合わせなどページごとの操作に用いる「PyPDF2 」、PDF文書の新規作成に用いる「ReportLab 」の2つを組み合わせて利用します。

ページ番号の追加は、有料のAcrobat®やその他のフリーソフトで行う作業ですが、Pythonでも今回の方法で可能になります。しかも、プログラムは好きなようにカスタマイズできるので、ヘッダーやフッダーの挿入を自動化するプログラムも作成できます。

本記事の目次

プログラミングする機能

以下のようにページ番号のないPDF文書に、ページ番号を追加するプログラムを作成します。ページ番号は、最初のページを1ページ目として1ずつ増やして追加します。

pdf original

なお、今回は「資料 – 1、資料 – 2…」のように「資料 -」というプレフィックスを付けます。このプレフィックスはコードで変更できるようにします(付けたくない場合は空文字を代入します)。

プログラムの実行イメージ

今回はGUIも作成して、以下の手順でプログラムを実行できるようにします。

1. プログラムを実行すると、以下のようなダイアログが表示されます。

pdf pager run

2. 「参照」ボタンをクリックして、ページ番号を追加するPDF文書を選択します。

pdf pager select file

3. テキストボックスにPDF文書のパスが挿入されるので、「実行」ボタンをクリックします。

pdf pager exec

4. ページ番号を追加したPDFを保存する「ファイル名」を入力し、「保存」ボタンをクリックします。

pdf pager save

5. 完了すると以下のダイアログが表示されます。

pdf pager finish

6. ページ番号が追加されたPDF文書が保存されます。

pdf paged 1

次のページにも同様にページ番号が追加されたのを確認できます。
pdf paged 1

※ 今回はページ番号に「資料-」のプレフィックスを付けます(コードで変更可能)。

プログラムファイルの構成

今回のプログラムは、「pdf_pager.py」と「pdf_pager_app.py」の2つのpythonファイルで構成されます。2つは以下のように同じフォルダーに配置します。

  ├ sample/
  │  └ test.pdf
  │
  ├ pdf_pager.py  <- 本体のプログラム
  └ pdf_pager_app.py <- 画面(GUI)と本体プログラムの呼び出し

pdf_pager.py」は今回の処理を行う本体のプログラムです。

pdf_pager_app.py」では「画面(GUI)」と「本体プログラムの呼び出し」を実装します。今回のプログラムを使うときは、このファイルを起動することとします。

なお、sampleフォルダーは、テスト用のPDFファイルを保存しておくためにあると便利ですが、なくても構いません。

ライブラリのインストール

プログラムで利用する「PyPDF2 」と「ReportLab 」をpipでインストールしておきます。

Pythonを公式サイトからダウンロードしたインストーラーでWindowsにインストールした場合は、以下のようにpyコマンドを用いてインストールできます。

> py -m pip install PyPDF2
> py -m pip install reportlab

python本体にパスを通している場合は、以下のようにpythonコマンドでもインストールできます。

> python -m pip install PyPDF2
> python -m pip install reportlab

Macなどでは以下のように適宜pip3コマンドを用いてください。

> pip3 install PyPDF2
> pip3 install reportlab

本体のプログラム

本体プログラムのソースコードとプログラミングのポイントは以下の通りです。

pdf_pager.py
import io
import PyPDF2
from PyPDF2.pdf import PageObject
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.lib.units import mm

# ページ番号の下からの位置
PAGE_BOTTOM = 10 * mm
# ページ番号のプレフィックス
PAGE_PREFIX = "資料 - "
# フォント登録
pdfmetrics.registerFont(UnicodeCIDFont("HeiseiKakuGo-W5"))

def add_page_number(input_file: str, output_file: str, start_num: int = 1):
    """
    既存PDFにページ番号を追加する
    """
    # 既存PDF(ページを付けるPDF)
    fi = open(input_file, 'rb')
    pdf_reader = PyPDF2.PdfFileReader(fi)
    pages_num = pdf_reader.getNumPages()

    # ページ番号を付けたPDFの書き込み用
    pdf_writer = PyPDF2.PdfFileWriter()

    # ページ番号だけのPDFをメモリ(binary stream)に作成
    bs = io.BytesIO()
    c = canvas.Canvas(bs)
    for i in range(0, pages_num):
        # 既存PDF
        pdf_page = pdf_reader.getPage(i)
        # PDFページのサイズ
        page_size = get_page_size(pdf_page)
        # ページ番号のPDF作成
        create_page_number_pdf(c, page_size, i + start_num)
    c.save()

    # ページ番号だけのPDFをメモリから読み込み(seek操作はPyPDF2に実装されているので不要)
    pdf_num_reader = PyPDF2.PdfFileReader(bs)

    # 既存PDFに1ページずつページ番号を付ける
    for i in range(0, pages_num):
        # 既存PDF
        pdf_page = pdf_reader.getPage(i)
        # ページ番号だけのPDF
        pdf_num = pdf_num_reader.getPage(i)

        # 2つのPDFを重ねる
        pdf_page.mergePage(pdf_num)
        pdf_writer.addPage(pdf_page)

    # ページ番号を付けたPDFを保存
    fo = open(output_file, 'wb')
    pdf_writer.write(fo)

    bs.close()
    fi.close()
    fo.close()


def create_page_number_pdf(c: canvas.Canvas, page_size: tuple, page_num: int):
    """
    ページ番号だけのPDFを作成
    """
    c.setPageSize(page_size)
    c.setFont("HeiseiKakuGo-W5", 14)
    c.drawCentredString(page_size[0] / 2.0,
                        PAGE_BOTTOM,
                        PAGE_PREFIX + str(page_num))

    c.showPage()


def get_page_size(page: PageObject) -> tuple:
    """
    既存PDFからページサイズ(幅, 高さ)を取得する
    """
    page_box = page.mediaBox
    width = page_box.getUpperRight_x() - page_box.getLowerLeft_x()
    height = page_box.getUpperRight_y() - page_box.getLowerLeft_y()
    return float(width), float(height)


if __name__ == '__main__':
    # テスト用
    infile = './sample/test.pdf'
    outfile = './sample/test_paged.pdf'
    add_page_number(infile, outfile)

# 位置とプレフィックスの設定

ページ番号の下端からの位置(PAGE_BOTTOM)とページ番号の前に付けるプレフィックス(PAGE_PREFIX)を変更しやすいようにコードの始めに定数として指定してしておきます。どちらも変更する場合はここで変更します。

...
from reportlab.lib.units import mm

PAGE_BOTTOM = 10 * mm
PAGE_PREFIX = "資料 - "
...

ReportLabでは、寸法を指定するときにreportlab.lib.unitsにあるmm(ミリ)やcm(センチメートル)を単位に使えます。PAGE_BOTTOM = 10 * mmのようにインポートしたmmを数値のあとにアスタリスク*で付け加えます。

今回は、以下のように紙の下端から10mmの位置にページ番号を追加します。

pdf bottom

# フォントの設定

ReportLabではデフォルトで「HeiseiMin-W3」と「HeiseiKakuGo-W5」の2つの日本語フォントをサポートしています。今回はこのうちゴシック体の「HeiseiKakuGo-W5」の方を使用します。明朝体を用いたい場合は以下のコードの部分を「HeiseiMin-W3」に書き換えます。

日本語フォントを使うには、まず以下のようにpdfmetrics.registerFont()でフォント名を登録しておきます。

...
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
...

# フォント登録
pdfmetrics.registerFont(UnicodeCIDFont("HeiseiKakuGo-W5"))

登録しておけば使用するときにsetFont()メソッドでフォント名を指定するだけです。

...
def create_page_number_pdf(c: canvas.Canvas, page_size: tuple, page_num: int):
    """
    ページ番号だけのPDFを作成
    """
    c.setPageSize(page_size)
    c.setFont("HeiseiKakuGo-W5", 14)
    ...

なお、デフォルト以外の日本語フォントを用いたい場合は、フォントファイルを読み込んで登録します。詳しくは以下の記事で詳しく説明しています。

no image
今回はPythonで日本語フォントをPDFに出力します。ライブラリにはReportLabを利用します。 前回のC#の場合と同じく、真ん中に「こんにちは、世界!」と出力します。ReportLabは…

# ページ番号だけのPDFの作成

ページ番号だけのPDFはcreate_page_number_pdf()関数で作成しています。

ReportLabでは、Canvasオブジェクトにテキストや表を描画します。描画にはいくつかのメソッドが用意されていますが、今回はdrawCentredString()メソッドを用いて「文字列の中心」の座標で位置合わせして文字列を描画しています。

canvas.drawCentredString(文字列の中心のx座標, 文字列の中心のy座標, 描画する文字列)

描画を終えたらshowPage()メソッドを呼び出します。するとそれ以降の操作は次のページに描画されます。すべてのページの描画が完了したら、最後にsave()メソッドを呼び出してPDFファイルとして保存します。

# メモリ上でPDFを読み書きする方法

ReportLabで作成するPDFのファイル名は、Canvasオブジェクトを作成するときに指定します。

from reportlab.pdfgen import canvas
c = canvas.Canvas(作成するPDFファイル名)

ここで、ファイル名の代わりに「ファイルオブジェクト」を指定することもできます。ファイルオブジェクトはファイルをopen()で開いて作成できますが、今回は標準ライブラリのio にあるBytesIOを利用します。

BytesIOを利用すれば、ファイルで扱うのと同じようにPDFのようなバイナリーデータメモリ上で読み書きできます。方法は簡単です。以下のようにBytesIOオブジェクトbs)を作成してファイル名の代わりに指定するだけです。

...
bs = io.BytesIO()
c = canvas.Canvas(bs)
...

BytesIOに書き込んだPDFは、ファイルからと同じように読み込めます。今回はPyPDF2で既存PDFに重ね合わせるので、以下のようにPyPDF2.PdfFileReader()で読み込みます。

pdf_num_reader = PyPDF2.PdfFileReader(bs)

# ページ番号だけのPDFを上から重ね合わせる

既存PDFの上にページ番号だけのPDFを重ねるには、下になるPDFページでmargePage()メソッドを実行します。

下になるPDFページ.mergePage(上から重ねるPDFページ)

この処理を以下のように1ページごとループで実行します。ページ番号のPDFを重ねたPDFページ(pdf_page)は、ループごとにpdf_writerに追加してゆきます。

...
for i in range(0, pages_num):
    # 既存PDF
    pdf_page = pdf_reader.getPage(i)
    # ページ番号だけのPDF
    pdf_num = pdf_num_reader.getPage(i)

    # 2つのPDFを重ねる
    pdf_page.mergePage(pdf_num)
    pdf_writer.addPage(pdf_page)
...

ループを完了したら最後に出力用のファイルに保存します。

fo = open(output_file, 'wb')
pdf_writer.write(fo)

画面(GUI)のプログラム

画面(GUI)のプログラムのソースコードは以下の通りです。

今回のプログラムはこのファイルを実行して起動します。本体のプログラム(pdf_pager.py)は、このコードの中でインポートして画面の「実行」ボタンをクリックして呼び出します。

pdf_pager_app.py
import tkinter as tk
from tkinter import Tk
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
# 本体プログラムのインポート
from pdf_pager import add_page_number


def ask_file():
    """
    参照ボタンの動作
    """
    global input_path
    # ファイルを開くダイアログを開く
    filename = filedialog.askopenfilename(filetypes=[('PDF files', '*.pdf')])
    input_path.set(filename)


def app():
    """
    実行ボタンの動作
    """
    # ファイル保存ダイアログを開く
    output_path = filedialog.asksaveasfilename(
        filetypes=[('PDF files', '*.pdf')], defaultextension=".pdf")
    if output_path is None:
        return

    # 既存PDFにページ番号を追加(本体プログラムの呼び出し)
    add_page_number(input_path.get(), output_path, 1)
    messagebox.showinfo('完了', '完了しました。')


# メインウィンドウ
main_window = Tk()
main_window.title('PDFにページ番号をふる')
main_window.geometry('500x100')

# メインフレーム
main_frame = ttk.Frame(main_window, padding=(5,10))
main_frame.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.E, tk.W))

# パラメータ(ファイル名)
input_path = tk.StringVar()

# ウィジェット(ファイル名)
input_label = ttk.Label(main_frame, text='入力ファイル')  # ラベル
input_entry = ttk.Entry(main_frame, textvariable=input_path)  # テキストボックス
input_btn = ttk.Button(main_frame, text='参照', command=ask_file)  # 参照ボタン

# ウィジェット(実行ボタン)
exe_btn = ttk.Button(main_frame, text='実行', command=app)

# ウィジェットの配置
input_label.grid(column=0, row=0, sticky=tk.W, pady=10)
input_entry.grid(column=1, row=0, sticky=(tk.E, tk.W), padx=5)
input_btn.grid(column=2, row=0, sticky=tk.E)
exe_btn.grid(column=0, row=2, columnspan=3, pady=10)

# 配置設定
main_window.columnconfigure(0, weight=1)
main_window.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)

# ウィンドウ起動
main_window.mainloop()

上記の画面(GUI)のプログラムは、以下の記事とほとんど同じです。内容についてはこちらで詳しく説明しています。

no image
Pythonの処理部分のコードだけをあげるよりも、画面(GUI)のコードも一緒に付けてあげれば、もらった相手は使い勝手が良くなりなります。特に入出力ファイルをエクスプローラーのような画面で選択できると…

最後に

今回は「ReportLab」と「PyPDF2」を組みあわせて実用的なプログラムを作成しました。そのためにBytesIOでメモリ上でPDFを受け渡しました。これは、一時的なファイル(テンポラリーファイル)を用いても可能ですが、メモリを使う方が速度の面からもスムーズです。

今回のプログラムには、複数のライブラリの組み合わせ、メモリの使い方、画面(GUI)プログラミングと盛り沢山の内容が含まれています。ぜひ日頃のプログラミングの参考にしてください。