大量の画像を資料としてまとめたい時や、誰かに見せようと思ったとき、自動で1ページに綺麗に割り付けしたい場合がある。そんな時、PDFを利用するのが便利だ。ついでに、画像サイズも自動でリサイズしてくれたら配布しやすくなる。今回は、そんなツールを作ってみよう。
Pythonで画像からPDFを作る方法を考えよう
画像数枚をPDFに貼り付けたいだけなら、わざわざプログラムを作るまでもない。WordやPowerPointを開いて、画像をドラッグ&ドロップして、PDFで保存するだけだ。しかし、業務で頻繁に写真画像を使う場合など、とにかく短時間でPDFを作成したい場合には、専用のPDF作成ツールを作ると便利だ。
ここでは、使い勝手などを考えて、指定のフォルダにPDFに貼り付けたい画像を放り込んで、バッチファイルを実行するとPDFが自動的に作成されるようなプログラムを作ってみよう。日々の業務で使うことを想定して、とにかく短時間で作業が終わるという点に留意しよう。
- (1) 「pdf-in」というフォルダにPDFに差し込みたい画像をコピーしておく
- (2) 「image-to-pdf.bat」というバッチファイルをダブルクリック
- (3) 「images.pdf」というPDFが作成される
それでは、これを実現するために、どうしたら良いのかを考えよう。まずは、どんな処理をする必要があるのか、まとめてみよう。
- (1) 「pdf-in」というフォルダにあるJPEGファイルを列挙する
- (2) PDFのファイルサイズを小さくするために、画像サイズを幅800にリサイズする。また、画像が縦向きであれば、横向きに90度回転する
- (3) PDFの1ページに写真を最大6枚貼り付ける
- (4) 「images.pdf」という名前でファイルを保存する
こんな手順で良さそうだ。
大規模言語モデルにプログラムの骨組みを作ってもらおう
このように、詳しい手順が完成したら、ChatGPTやGoogle Bardなどの大規模言語モデル(LLM)を利用してプログラムの骨組みを作ってもらおう。そもそも筆者はプログラムを作るのが趣味なので、ゼロからプログラム作るのが好きだ。しかし、そんな筆者でも、最近ではプログラムの骨組みを作るのは、大規模言語モデルに任せることが増えた。
ここ1年で、大規模言語モデルのプログラム生成能力は格段に改善された。ChatGPTだけでなく、BardやClaudeなど、様々な安定して動作する大規模言語モデルが登場し、こちらの指示に沿って上手にプログラムを作ってくれるようになった。そのため、今ではプログラムを全くのゼロから作るのが非効率となった。業務で必要なプログラムを作るのであれば、なおさらだろう。
とは言え、「プログラムの骨組み」と表現したのは、まだまだ細かい些細な点まで完全に作ってくれるように頼んでも、なかなか思い通りにならない事が多いためだ。ありとあらゆる細い指示を与えても無視されることもある。
その辺りを考慮して、ここでは、次のようなプログラム生成用のプロンプトを用意した。
### 指示:
複数画像を元にしてPDFを作成するPythonプログラムを作ってください。
### 手順:
1.「pdf-in」というフォルダにあるJPEGファイルを列挙する
2. PDFのファイルサイズを小さくするために、画像サイズを幅800にリサイズする。
3. 画像が縦向きであれば、横向きに90度回転する
4. PDFの1ページに写真を最大6枚を貼り付ける
5.「images.pdf」という名前でファイルを保存する
今回は、無料でも利用できるChatGPT(モデルGPT3.5)を使ってみた。上記のプロンプトを与えると、次のような応答を返した。
なお、プログラムを実行するためには、Pythonのライブラリ「Pillow」と「ReportLab」が必要となる。ターミナル(WindowsならPowerShell、macOSならターミナル.app)を起動して、下記のコマンドを実行しよう。
# --- Windowsの場合 ---
python -m pip install Pillow ReportLab
# --- macOSの場合 ---
python3 -m pip install Pillow ReportLab
そして、ChatGPTが作成したプログラムを「images-to-pdf.py」というファイル名で保存しよう。そして、imagesというフォルダを作成して、そこにJPEGファイルを6枚以上コピーしよう。その後、ターミナルで次のコマンドを実行しよう。
# --- Windows ---
python images-to-pdf.py
# --- macOS ---
python3 images-to-pdf.py
すると次のようにエラーが表示された。読者の皆さんはどうだっただろうか。大規模言語モデルは、毎回異なる返答を返すため、作成されるプログラムも異なるのだ。
ChatGPTのプログラム生成の能力は日々高くなっているので、エラーが表示されず、正しく動くこともあるだろう。しかし、今回のようにエラーが表示されることもある。そんな時には、以下のように、エラーメッセージをコピーしてChatGPTの続く会話に与えると良い。
ここでは、ChatGPTの続く会話に下記のようにエラーメッセージを入力してみた。
下記のエラーが表示されました。
TypeError: expected str, bytes or os.PathLike object, not JpegImageFile
すると、次のようにすぐに謝罪し、改善案を示してくれた。今度は期待できそうだ。
先ほどと同じように、プログラムを「images-to-pdf.py」に保存し、ターミナルでPythonのプログラムを実行してみよう。すると、問題なく動かすことができた。
しかし、作成されたPDF「images.pdf」を見ると、次のように悲惨なものであった。
このように、ChatGPTを使う場合、その骨組みを作るのは非常に簡単なのだが、細かい点は、自分で調整した方が速い。写真を貼り付ける座標を間違っているようなので、その辺りを調整するだけで良さそうだった。
プログラムを完成させよう
それで、最終的に筆者が修正して完成させたプログラムを紹介しよう。少し長くなってしまったので、プログラム全体はこちらのGistにアップした。必要な部分を少しずつ解説しよう。
まずは、JPEG画像の一覧を列挙するプログラムを確認しよう。以下は、「pdf-in」というフォルダにあるJPEG画像を列挙する例だ。
import os
# フォルダ内のJPEGファイルを列挙 --- (*1)
image_folder = 'pdf-in'
image_files = [
f for f in os.listdir(image_folder) # os.listdirでファイルの一覧を取得
if f.endswith('.jpg') or f.endswith('.jpeg')] # 拡張子を判定
os.listdirメソッドと、リストの内包記法を利用して、ファイルの一覧を取得している。リストの内包表記を使う事で、簡潔にファイルの一覧を取得できる。
次にPDFのページサイズをA4(縦向き)に設定しているのが次の部分を確認してみよう。
from reportlab.lib.pagesizes import A4, landscape, portrait
from reportlab.pdfgen import canvas
# PDFファイルの作成と設定(A4縦向きにする) --- (*2)
c = canvas.Canvas(output_pdf, pagesize=portrait(A4))
width, height = portrait(A4)
print(f'PageSize={width:.1f},{height:.1f}')
# A4に縦向き6枚(2x3)で配置するように計算 --- (*3)
margin_x, margin_y = 10, 60
image_width = (width - 3 * margin_x) // 2
image_height = (height - 4 * margin_y) // 3
(*2)の部分で、reportlabには、ページサイズや向きを表す定数が用意されているので、これを利用してページサイズの指定が可能となっている。なお、ページサイズを指定すると、(*3)で幅(width)と高さ(height)が取得できるので、これを利用して画像のサイズを指定する。
そして、以下の関数が画像ファイルをサイズ(max_width, max_height)にリサイズする処理だ。リサイズに時間はかかるものの、もっとも画像の画質が良くなるLANCZOSアルゴリズムを利用してリサイズしている。
# 画像をリサイズする関数 --- (*4)
def resize_image(image, max_width, max_height):
width, height = image.size
# 新しいサイズを計算
resize_ratio = max_width / width
if width < height:
resize_ratio = max_height / height
new_width = int(width * resize_ratio)
new_height = int(height * resize_ratio)
# 画像をリサイズ(LANCZOSを利用)
image_r = image.resize((new_width, new_height), Image.LANCZOS)
次に、画像に埋め込まれたExif情報を利用して画像の向きを回転する処理を確認してみよう。
# Exifを利用して画像の向きを回転 --- (*5)
def change_image_rotation(img):
exif_data = img._getexif()
if exif_data:
for tag, value in exif_data.items():
if TAGS.get(tag) == 'Orientation':
if value == 3:
img = img.rotate(180, expand=True)
elif value == 6:
img = img.rotate(-90, expand=True)
elif value == 8:
img = img.rotate(90, expand=True)
return img
スマートフォンやデジタルカメラで撮影したJPEG画像には大抵Exifと呼ばれる撮影情報が記録されている。このExif情報には画像の向きを表す「Orientation」情報が入っている。これを利用して画像を回転するのが上記の処理となる。
最後に、画像をPDFに貼り付けるのが以下の処理だ。
# 画像をリサイズ、回転してPDFに貼り付け --- (*6)
for idx, image_file in enumerate(image_files):
if (idx % 6 == 0) and (idx > 0): # --- (*6a)
c.showPage() # 新しいページを開始
i = idx % 6
# 写真の座標を計算 --- (*7)
x = (i // 3) * (image_width + margin_x) + margin_x
y = (i % 3) * (image_height + margin_y) + margin_y
# 画像を読み込んでリサイズ -- (*7a)
img = Image.open(os.path.join(image_folder, image_file))
img = change_image_rotation(img)
img = resize_image(img, int(image_width * 4), int(image_height * 4))
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.jpg')
img.save(temp_file.name, format='JPEG')
# PDFのキャンバスに画像を貼り付け --- (*7b)
c.drawImage(temp_file.name, x, height - y - image_height, image_width, image_height)
temp_file.close()
# PDFファイルを保存
c.save()
上記の(*6)では画像を一枚ずつfor文を利用して一つずつ処理する。なお、1ページに6枚ずつの画像を貼るので、6枚に1度新しいページを作成するように、(*6a)の部分で指定している。
(*7)では写真を貼り付ける座標位置を計算している。整数の割り算を行う「//」演算子と、割り算の余りを求める「%」演算子を使うことで、とてもシンプルに座標が計算できる。
なお、(*7a)では画像を読み込んでリサイズしたり回転したりして、一時ファイルに保存する。 (*7b)では一時ファイルの内容を元にして、PDFに画像を埋め込んでいる。
最後に、キャンバスのsave()メソッドを呼ぶことでPDFファイルを作成できる。
プログラムを「images-to-pdf.py」という名前で保存して、ターミナルで「python images-to-pdf.py」コマンドを実行すれば、以下のようにプログラムを実行して、PDFを作成できる。