今回は、既存のPDF文書にページ番号を追加するプログラムをPythonで作成します。
プログラムのポイントは、既存のPDFにページ番号を直接追加することは難しいので、まずページ番号だけのPDFをメモリ上に作成し、それを上から重ね合わせることで可能にするところです。
手順としては以下のようになります。
- ReportLabを用いてページ番号だけのPDFをメモリ上に作成する。
- PyPDF2を用いてページ番号だけのPDFをメモリから読み込む。
- PyPDF2を用いて既存のPDFの上にページ番号だけのPDFを重ね合わせる。
ここで、外部のライブラリには、重ね合わせなどページごとの操作に用いる「PyPDF2 」、PDF文書の新規作成に用いる「ReportLab 」の2つを組み合わせて利用します。
ページ番号の追加は、有料のAcrobat®やその他のフリーソフトで行う作業ですが、Pythonでも今回の方法で可能になります。しかも、プログラムは好きなようにカスタマイズできるので、ヘッダーやフッダーの挿入を自動化するプログラムも作成できます。
本記事の目次
プログラミングする機能
以下のようにページ番号のないPDF文書に、ページ番号を追加するプログラムを作成します。ページ番号は、最初のページを1ページ目として1ずつ増やして追加します。
なお、今回は「資料 – 1、資料 – 2…」のように「資料 -」というプレフィックスを付けます。このプレフィックスはコードで変更できるようにします(付けたくない場合は空文字を代入します)。
プログラムの実行イメージ
今回はGUIも作成して、以下の手順でプログラムを実行できるようにします。
1. プログラムを実行すると、以下のようなダイアログが表示されます。
2. 「参照」ボタンをクリックして、ページ番号を追加するPDF文書を選択します。
3. テキストボックスにPDF文書のパスが挿入されるので、「実行」ボタンをクリックします。
4. ページ番号を追加したPDFを保存する「ファイル名」を入力し、「保存」ボタンをクリックします。
5. 完了すると以下のダイアログが表示されます。
6. ページ番号が追加されたPDF文書が保存されます。
次のページにも同様にページ番号が追加されたのを確認できます。
※ 今回はページ番号に「資料-」のプレフィックスを付けます(コードで変更可能)。
プログラムファイルの構成
今回のプログラムは、「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の位置にページ番号を追加します。
# フォントの設定
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)
...
なお、デフォルト以外の日本語フォントを用いたい場合は、フォントファイルを読み込んで登録します。詳しくは以下の記事で詳しく説明しています。
# ページ番号だけの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)のプログラムは、以下の記事とほとんど同じです。内容についてはこちらで詳しく説明しています。
最後に
今回は「ReportLab」と「PyPDF2」を組みあわせて実用的なプログラムを作成しました。そのためにBytesIOでメモリ上でPDFを受け渡しました。これは、一時的なファイル(テンポラリーファイル)を用いても可能ですが、メモリを使う方が速度の面からもスムーズです。
今回のプログラムには、複数のライブラリの組み合わせ、メモリの使い方、画面(GUI)プログラミングと盛り沢山の内容が含まれています。ぜひ日頃のプログラミングの参考にしてください。