210MBのPDFに半日振り回されて、DPIの設定が気休めだと知った話
勉強会のスクリーンショットを50枚くらいPDFにまとめたら、210MBになりました。
「重すぎてGoogle Driveに上がらない」「相手が開けない」という、よくあるやつです。
ネットで調べると「DPIを下げればいい」と書いてある。150から72に下げてみました。
205MB。
2%しか減っていない。
DPIを下げても効果がない理由と、実際に効果があった方法を調べたので、メモしておきます。
DPIを下げても意味がなかった理由
これ、ちゃんと調べるまで誤解してました。
DPI(Dots Per Inch)は、画像を「どのサイズで印刷するか」の指示でしかない。画像そのもののデータ量は変わらない。
たとえば、3840×2160ピクセルのスクショがあるとします。
- DPI 150 だと「25.6インチで印刷してね」という意味
- DPI 72 だと「53.3インチで印刷してね」という意味
どっちも画像データは3840×2160ピクセルのまま。ファイルサイズが変わるわけがない。
DPIとリサイズを混同している情報が多いのかもしれない。
本当に効いた3つのこと
結局、効果があったのはこの3つでした。
- PNGをJPEGに変換(70%減)
- 横幅を1920pxにリサイズ(50%減)
- pikepdfで再圧縮(10%減)
順番にやっていきます。
PNG→JPEG変換で、一気に70%減
スクリーンショットは通常PNG形式で保存される。これがファイルサイズ肥大の主な原因。
PNGは劣化なし、JPEGは劣化あり。劣化ありってことは、データを捨ててるってこと。捨てればファイルサイズは減る。
50枚のPNG(合計210MB)を、JPEG品質85%で変換したら62MBになりました。
ただ、そのまま変換しようとしたらエラー出ました。
OSError: cannot write mode RGBA as JPEG
透過PNGだとJPEGに変換できないらしい。背景を白で塗りつぶしてから変換する必要がありました。
from PIL import Image
def png_to_jpeg(png_path, jpeg_path, quality=85):
img = Image.open(png_path)
# 透過PNGは背景を白に
if img.mode == 'RGBA':
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
elif img.mode != 'RGB':
img = img.convert('RGB')
img.save(jpeg_path, 'JPEG', quality=quality)
品質85%にしたのは、これ以上下げるとコードが読みにくくなったから。スクショにはドキュメントとか含まれてるので、あんまり荒くはできなかった。
リサイズで、さらに50%減
4Kモニタでスクショ撮ってたので、画像が3840×2160ピクセルもありました。
講義スライドとして表示するだけなら、横幅1920pxで十分。
def resize_image(img, max_width=1920):
if img.width > max_width:
ratio = max_width / img.width
new_height = int(img.height * ratio)
img = img.resize((max_width, new_height), Image.Resampling.LANCZOS)
return img
62MBが31MBになりました。ピクセル数が半分になればファイルサイズも半分、という単純な話。
pikepdfで仕上げ
最後に、PDFの内部構造を最適化してくれる pikepdf というライブラリを通しました。
import pikepdf
def optimize_pdf(input_pdf, output_pdf):
with pikepdf.open(input_pdf) as pdf:
pdf.save(output_pdf, compress_streams=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate)
31MBが28MBになりました。10%減。劇的じゃないけど、一応やっておく価値はある。
結果
| やったこと | サイズ | 減り具合 |
|---|---|---|
| 最初 | 210MB | - |
| DPI変更(無駄だった) | 205MB | 2% |
| JPEG変換 | 62MB | 70%減 |
| リサイズ | 31MB | 50%減 |
| pikepdf | 28MB | 10%減 |
最終的に87%減。210MBが28MBになりました。
DPIを調整していた時間は無駄だった。
まとめてスクリプト化した
最終的に、こういうスクリプトにまとめました。
from PIL import Image
import pikepdf
import hashlib
from pathlib import Path
import os
def remove_duplicates(input_dir):
"""重複画像を除去"""
seen_hashes = set()
unique_images = []
for img_path in sorted(Path(input_dir).glob("*.png")):
if img_path.name.startswith('.'):
continue
with open(img_path, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()
if file_hash not in seen_hashes:
seen_hashes.add(file_hash)
unique_images.append(str(img_path))
return unique_images
def convert_screenshots_to_pdf(input_dir, output_pdf, max_width=1920, jpeg_quality=85):
image_paths = remove_duplicates(input_dir)
print(f"ユニーク画像: {len(image_paths)} 枚")
images = []
for img_path in image_paths:
img = Image.open(img_path)
# 透過PNG対応
if img.mode == 'RGBA':
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
elif img.mode != 'RGB':
img = img.convert('RGB')
# リサイズ
if img.width > max_width:
ratio = max_width / img.width
new_height = int(img.height * ratio)
img = img.resize((max_width, new_height), Image.Resampling.LANCZOS)
images.append(img)
# PDF作成
temp_pdf = output_pdf.replace('.pdf', '_temp.pdf')
images[0].save(temp_pdf, save_all=True, append_images=images[1:], quality=jpeg_quality)
# pikepdfで最適化
with pikepdf.open(temp_pdf) as pdf:
pdf.save(output_pdf, compress_streams=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate)
os.remove(temp_pdf)
print(f"完了: {output_pdf}")
# 使い方
convert_screenshots_to_pdf('screenshots/', 'output.pdf')
重複画像の除去も入れた。スクショを撮っていると同じ画面を2回撮ることがあるので。
ちなみにCanvaに入れるとき
PDFを最適化したあと、Canvaにインポートして講義スライドにする場合、注意点がある。
PDFをアップロードすると、Canvaが勝手に「動画」として認識することがある。再生ボタンが出てきて困る。
回避方法は、新規で「プレゼンテーション(16:9)」を作って、PDFインポート側から各ページをコピペすること。Cmd+A → Cmd+C → Cmd+V を全ページ分やる。面倒だけど、これで静止スライドになります。
あと、講義用に追加するスライド(タイムテーブルとか、質問の仕方とか)はCanva上で追加するようにしてます。元PDFは触らない。そうすると、同じPDFを別の講義で使い回せるので。
オンラインサービスは試したけど
最初はSmallpdfやILovePDFを試した。
でも、210MBはそもそもアップロードできなかった。ファイルサイズ制限に引っかかる。
アップロードできたやつ(PDF Compressor)も、35MBくらいにしかならなかった。今回の手法のほうが小さくなった。
結局、自分でスクリプト書いたほうが早いし、品質も調整できるし、何度でも使い回せるので、ブラウザでポチポチやるより楽でした。
半日振り回されて学んだこと:DPIはファイルサイズに関係ない。PNG→JPEG変換とリサイズが本質。
Discussion