🖼️

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つでした。

  1. PNGをJPEGに変換(70%減)
  2. 横幅を1920pxにリサイズ(50%減)
  3. 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+ACmd+CCmd+V を全ページ分やる。面倒だけど、これで静止スライドになります。

あと、講義用に追加するスライド(タイムテーブルとか、質問の仕方とか)はCanva上で追加するようにしてます。元PDFは触らない。そうすると、同じPDFを別の講義で使い回せるので。

オンラインサービスは試したけど

最初はSmallpdfやILovePDFを試した。

でも、210MBはそもそもアップロードできなかった。ファイルサイズ制限に引っかかる。

アップロードできたやつ(PDF Compressor)も、35MBくらいにしかならなかった。今回の手法のほうが小さくなった。

結局、自分でスクリプト書いたほうが早いし、品質も調整できるし、何度でも使い回せるので、ブラウザでポチポチやるより楽でした。


半日振り回されて学んだこと:DPIはファイルサイズに関係ない。PNG→JPEG変換とリサイズが本質。

参考

Discussion