はじめに
うちの上司、NotebookLMのスライド機能にどハマりしている。
「AIが自動でスライドを作ってくれる!」と目を輝かせながら、次々と教材PDFを生成し、レンタルしている学習サイトにスライド画像をそのまま貼り付けていった。結果——サーバーのストレージが満杯になった。
原因は明白だった。NotebookLMが出力するPDFをそのまま画像化して貼り付けているため、1枚あたり数百KB〜数MBのPNGが大量に積み上がっていたのだ。「バイブコーディング(勢いで作るやつ)」の被害を、バックエンド側で受け止めなければならない典型例である。
そこで今回は2つのPythonスクリプトを作り、PDF → PNG → WebP という変換パイプラインで問題を解決した。
結果から先に言う
| フォーマット | ファイルサイズ(1枚あたり) |
|---|---|
| PNG(変換前) | 約 736 kB |
| WebP(変換後) | 約 110 kB |
約85%の容量削減。 13ページの教材なら 9.5 MB → 1.4 MB になる計算だ。ストレージ制限も、これなら怖くない。
環境・前提
- Python 3.x
- VSCode(ターミナルから順番に実行)
- ライブラリ:
PyMuPDF(fitz)、Pillow(PIL)
pip install PyMuPDF Pillow
フォルダ構成はシンプルにこう用意した。
プロジェクトフォルダ/
├──.venv
├── フォルダ/ ← PDFをここに入れる
├── step1_pdf_to_png.py
└── step2_png_to_webp.py
Step 1:PDF → PNG(PyMuPDF で1ページずつ画像化)
コード
import fitz # PyMuPDF
import os
import glob
def pdf_to_webp_pages(pdf_path, output_folder, dpi=300):
if not os.path.exists(output_folder):
os.makedirs(output_folder)
doc = fitz.open(pdf_path)
for i in range(len(doc)):
page = doc.load_page(i)
pix = page.get_pixmap(matrix=fitz.Matrix(dpi/72, dpi/72))
output_filename = f"vibe_coding_page_{i+1}.png"
output_path = os.path.join(output_folder, output_filename)
pix.save(output_path, "png")
print(f"保存完了: {output_filename}")
doc.close()
# 実行部分
pdf_folder = "フォルダ名"
output_base_dir = "extracted_images"
pdf_files = glob.glob(os.path.join(pdf_folder, "*.pdf"))
for pdf_path in pdf_files:
pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]
output_dir = os.path.join(output_base_dir, pdf_name)
pdf_to_webp_pages(pdf_path, output_dir, dpi=150)
何をやっているか
fitz.open() でPDFを解析する。 PyMuPDFはAdobeのようなGUIツールなしにPDFを操作できる強力なライブラリで、1ページずつオブジェクトとして取り出せる。
dpi=150 がミソ。 page.get_pixmap() に渡すDPI値で、出力画像の解像度が決まる。印刷用なら300以上欲しいが、ウェブ教材として画面で見るだけなら150で十分だ。解像度を半分にすれば、データ量は単純計算で4分の1になる。
glob でフォルダ内を全自動処理。 glob.glob("フォルダ名/*.pdf") で対象ファイルを一括取得し、PDFごとに出力フォルダを自動生成する。10本のPDFがあれば10フォルダが自動で生まれる。手作業ゼロ。
Step 2:PNG → WebP(Pillow で一括圧縮)
コード
import os
from PIL import Image
import glob
def compress_images(input_folder, output_folder, quality=80):
if not os.path.exists(output_folder):
os.makedirs(output_folder)
image_extensions = ('*.png', '*.jpg', '*.jpeg', '*.webp')
image_files = []
for ext in image_extensions:
image_files.extend(glob.glob(os.path.join(input_folder, '**', ext), recursive=True))
for image_path in image_files:
relative_path = os.path.relpath(image_path, input_folder)
base_name = os.path.splitext(relative_path)[0]
output_path = os.path.join(output_folder, f"{base_name}.webp")
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with Image.open(image_path) as img:
img = img.convert("RGB")
img.save(output_path, "WEBP", quality=quality)
print(f"変換完了: {relative_path} -> {base_name}.webp")
# 実行
compress_images("extracted_images", "compressed_webp")
何をやっているか
WebP はウェブのための次世代フォーマットだ。 Googleが開発した画像フォーマットで、JPEGやPNGと同等の画質を保ちながら、ファイルサイズを大幅に削減できる。現代のブラウザはほぼすべてWebPに対応している。
quality=80 の非可逆圧縮。 WebPは「lossy(非可逆)」圧縮が使える。quality=80 は、人間の目にはほぼ判別できないレベルでデータを間引く設定だ。これが 736kB → 110kB という数字の正体である。
img.convert("RGB") で透過を処理する。 PNGには透過(アルファチャンネル)が含まれることがある。WebP(非可逆モード)は透過を持てないため、RGBに変換してから保存することでエラーを防ぐ。
recursive=True でサブフォルダまで全探索。 Step 1でPDF名ごとにフォルダが分かれているため、再帰的に探索しなければ見つからないファイルが出てくる。glob の recursive=True と ** パターンを組み合わせることで、ネストしたフォルダ構造にも対応した。
実行手順(VSCodeで2ステップ)
ターミナルを開いて順番に実行するだけ。
# Step 1:PDF を PNG に変換
python step1_pdf_to_png.py
# Step 2:PNG を WebP に圧縮変換
python step2_png_to_webp.py
compressed_webp/ フォルダに変換後のファイルが生成されるので、これをレンタルサーバーにアップロードすれば完了だ。
まとめ:物理制約はコードで解く
今回の問題は「ストレージが足りない」というインフラ的な制約だった。解決策は「サーバーを増強する(=お金をかける)」ではなく、「画像を軽くする(=技術で解決する)」だ。
2つのスクリプトで実現したことをまとめると、
- PyMuPDF でAdobeなしにPDFをページ単位で画像化し、DPIを調整してウェブ最適化
- Pillow + WebP で次世代フォーマットへの変換と非可逆圧縮を自動化
- glob の再帰探索 でフォルダ構造が複雑でも一括処理
バイブコーディングで次々と生み出される教材を、バックエンドの自動化で受け止める。これが現代のAI開発チームにおける「守りの技術」の一形態だと思っている。
上司のバイブなコーディングは今日も止まらない。こちらも自動化で対抗するしかない。