【Python】重すぎる論文用PDF図を軽くする

PythonのTopに戻る

結論:重い点群やセルだけをrasterizedにする

大量の点やセルを含むPDF図が重いときは、該当する描画要素だけにrasterized=Trueを設定する。背景の密な要素を画像化し、軸や文字はベクトルとして残す方法である。図全体を低解像度のPNGへ変換するより、文字の読みやすさを保ちやすい。ただし必ず小さくなるわけではないので、実際のファイルサイズと最終掲載サイズで比較して決める。

PDFでは一つ一つの点やセルを図形として保存できる。その利点は拡大時に輪郭が保たれることだが、要素数が多いほどデータが増え、表示や編集が重くなる場合がある。一方、ラスタライズした部分の容量は描画領域と解像度にも左右される。少数の点まで画像化したり必要以上にdpiを上げたりすると、容量削減の効果がないこともある。

そのまま動かせる例

例は120×120セルの滑らかな分布に細かい変動を加えた図を作る。まず通常のPDFを保存し、同じmeshだけをラスタライズしてもう一つ保存する。元データ、図の大きさ、文字、色範囲は同じなので、保存方式だけを比較できる。NumPyとMatplotlibを用い、空の作業フォルダーで実行すると2つのPDFと確認用PNGを作る。

ファイル名はvector.pdf、mixed.pdf、rasterization-preview.pngで、同名ファイルは上書きされる。掲載したバイト数はこの環境で実際に生成したファイルの値であり、PDFの生成器やフォント、バージョンによって変わる。PNGは図の内容を示すためのプレビューで、PDF内部の保存方式の違いを画像だけから判別するものではない。

from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt

edges = np.linspace(-3, 3, 121)
centers = (edges[:-1] + edges[1:]) / 2
xx, yy = np.meshgrid(centers, centers)
z = np.exp(-(xx**2 + yy**2)) + 0.2 * np.cos(7 * xx) * np.sin(5 * yy)
fig, ax = plt.subplots(figsize=(5.2, 3.8), layout="constrained")
try:
    mesh = ax.pcolormesh(edges, edges, z, cmap="viridis", shading="flat")
    ax.set(xlabel="x (mm)", ylabel="y (mm)", title="120 x 120 cells")
    ax.text(0.02, 0.96, "Axes and text stay vector", transform=ax.transAxes,
            va="top", color="white")
    fig.colorbar(mesh, ax=ax, label="Signal (a.u.)")
    fig.savefig("vector.pdf")
    mesh.set_rasterized(True)
    assert mesh.get_rasterized()
    assert not ax.xaxis.label.get_rasterized()
    fig.savefig("mixed.pdf", dpi=180)
    fig.savefig("rasterization-preview.png", dpi=160)
finally:
    plt.close(fig)
for name in ["vector.pdf", "mixed.pdf"]:
    data = Path(name).read_bytes()
    assert data.startswith(b"%PDF-")
    print(name, len(data), "bytes")
print("mesh rasterized: True; x label rasterized: False")

実行結果(ファイル容量は環境によって変わる)

vector.pdf 232286 bytes
mixed.pdf 37209 bytes
mesh rasterized: True; x label rasterized: False
部分ラスタライズの対象となる120×120セルとベクトルで残す軸文字のプレビュー
掲載PNGは図の内容の確認用。保存方式と容量は同じコードで作る2つのPDFで比較する。

dpiが変える部分を区別する

mesh.set_rasterized(True)はそのmeshの保存方法を変える。ラベルまでまとめて画像になる指定ではない。例ではmeshの設定がTrue、x軸ラベルがFalseであることも確認している。PDFを開く際は、文字だけでなく目盛り、細い線、カラーバー、セル境界に読みにくい部分がないかを見よう。PDFビューアーによって補間やアンチエイリアスの見え方が違うこともある。

保存時のdpiはラスタライズされる部分の画素数に影響する。全てのベクトル線をそのdpiで粗くするという意味ではない。例えば幅3インチの描画部分を180 dpiで画像化すると、おおむね540画素幅が目安になる。実際にはレイアウトや余白の切り詰めによって対象領域が変わるため、図全体のインチ数だけで正確な埋め込み画像サイズを断定しない。

最終的な掲載幅で比較する

容量が小さくなっても、投稿先の規定や細部の可読性を満たさなければ採用できない。論文の1段幅・2段幅など実際の大きさで表示または印刷し、小さなマーカーや密な境界がつぶれていないか確認する。極端に拡大したときに画素が見えるのはラスタライズの性質であり、そのことだけで不適切とは限らない。必要な解像度は図の内容と提出条件で決まる。

同じ設定を散布図へ使うなら、scatterが返す描画要素へ指定する。複数の背景要素をまとめて制御したい場合は、zorderによるラスタライズ範囲の指定もできるが、文字まで含めないように対象を点検しよう。単純な折れ線、数個のマーカー、注釈などはベクトルの利点が大きく、すべてを一律に画像化する必要はない。

元データと編集用の図も残す

PDFを軽くするためにデータ点を間引く方法は、保存方式だけを変える今回の方法とは異なり、図に載る情報自体を減らす。細いピークや外れ値を落とす危険があるため、間引きを行うなら目的と規則を説明する。ラスタライズしても元の数値配列は変わらないが、PDFから元の値を完全に取り戻せるわけではない。再作図できるコードと入力を別に保存しよう。

透明度や画像圧縮、フォントの埋め込み、カラーバーの描画方式も出力へ影響する。最初は一要素だけを変更して比較する方が原因を追いやすい。この例のサイズ比を一般的な圧縮率として引用せず、自分の図で測定しよう。最終版を送る前には、生成されたPDFそのものを開き、図の欠けや文字化けがなく、意図したページサイズになっていることまで確認したい。

確認環境と参考資料

例はLinux・CPython 3.12.14・NumPy 2.3.5・Matplotlib 3.10.8(Agg)で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。

関連項目:大量の図を保存するときのメモリ増加を防ぐ

PythonのTopに戻る