【Python】PNG を減色して圧縮するコード例(画像ファイル)【pngquant】

Python

PNG 形式の画像ファイルを減色して圧縮する Python コード例を書きました。

コマンドラインツールの pngquantピンクウォント を使用して減色しました。

Windows 用の pngquant.exe を、Python の subprocess.run() から呼び出して使用します。

PNG の画像ファイル (.png) を読み込んで、減色した PNG を別の画像ファイル (.png) に保存しました。

※ 標準入力に PNG データを渡して、結果を標準出力から受け取る方法は、以下の記事に書きました。

PNG を減色して圧縮するコード例(標準入力と標準出力)

日本語を含んだ画像ファイルパス

過去のバージョン pngquant (2.12.0 (January 2018)) では、日本語を含んだファイルパスを解釈できなかったようで(pngquant の内部で文字化けした)、英数字と記号のファイルパスしか使用できませんでした。

例えば、以下のようなエラーメッセージが表示されて、処理が止まりました。

"D:\project\data_plot\日本語.png"
error: cannot open D:\project\data_plot\譌・譛ャ隱・png for reading

ですが、新しいバージョン pngquant (2.17.0 (September 2021)) では、日本語を含んだファイルパスが使用できるようでした。

もしかしたら、pngquant.exe に何か変更があったのかもしれません。

例えば、以下のようなファイルパスでも減色して保存することができました(出力画像も正常でした)。

(入力画像)"F:\project\data\sample日本語.png"
(出力画像)"F:\project\data\sample_genshoku日本語.png"

pngquant マニュアル

pngquant の公式サイトです。

(pngquant.org) pngquant – lossy PNG compressor

『Command-line』の『Binary for Windows』から『pngquant-windows.zip』を取得します。

この中に pngquant.exe がありました。

これを好きな場所に解凍して使用します。

自分が取得した時のバージョンは『2.17.0 (September 2021)』でした。

pngquant.exe の引数ひきすうの説明は、zip に同梱されている『README.txt』に載っていました。

公式サイトにも説明がありました。

(pngquant.org) Manual

GUI で操作できる PNGooピングー というソフトも便利でした。

(pngquant.org) PNGoo – Windows GUI for batch conversion

Python マニュアル

コード例で使用した Python 機能のマニュアルの場所です。

(docs.python.org) class pathlib.Path(*pathsegments)

(docs.python.org) bytes.decode(encoding='utf-8', errors='strict')

(docs.python.org) 標準エンコーディング encoding='cp932', 'shift_jis', 'euc_jp', 'utf_8', 'utf_16', ...

(docs.python.org) エラーハンドラ errors='strict', 'ignore', 'replace', 'xmlcharrefreplace', 'backslashreplace', ...

(docs.python.org) subprocess.PIPE

(docs.python.org) exception UnicodeDecodeError

(docs.python.org) exception subprocess.TimeoutExpired

(docs.python.org) subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None, **other_popen_kwargs)

(docs.python.org) class subprocess.CompletedProcess

(docs.python.org) args

(docs.python.org) returncode

(docs.python.org) stderr

(docs.python.org) Path.stat(*, follow_symlinks=True)

(docs.python.org) class os.stat_result

(docs.python.org) st_size

(docs.python.org) sys.version

コード例

『PNG ファイル (.png)』を指定して減色する Python コード例です。

結果も『PNG ファイル (.png)』に保存します。

"""
PNG 画像を減色して圧縮する Python コード例です。
pngquant.exe を使用して圧縮します。
『PNG ファイル』を指定して減色します。
結果も『PNG ファイル』に保存します。
"""
from pathlib import Path
import subprocess

def main():
    print('start')

    # (1/7) PNG 画像を用意します。
    # 今回は試しに matplotlib で作図して、
    # PNG 画像として保存したものを用意しました。
    import matplotlib
    matplotlib.use('TkAgg')
    from matplotlib import pyplot as plt
    src_file = Path(r'F:\project\data\sample.png')
    # (src は source ソース の略です)
    (fig, ax) = plt.subplots(ncols=1, nrows=1)
    try:
        x  = [ 1,  2,  3,  4]
        y1 = [ 3,  9, 27, 81]
        y2 = [10, 15, 20, 25]
        y3 = [15, 30, 45, 60]
        y4 = [20, 50, 65, 70]
        ax.set_xlim(0, 5)
        ax.set_ylim(0, 100)
        ax.plot(x, y1, label='y1', lw=3, alpha=0.6, marker='o', ms=8)
        ax.plot(x, y2, label='y2', lw=3, alpha=0.6, marker='o', ms=8)
        ax.plot(x, y3, label='y3', lw=3, alpha=0.6, marker='o', ms=8)
        ax.plot(x, y4, label='y4', lw=3, alpha=0.6, marker='o', ms=8)
        ax.grid()
        ax.legend(loc=2, fancybox=True)
        fig.set_dpi(100)
        fig.set_figwidth(3.6)
        fig.set_figheight(3.6)
        fig.savefig(src_file)
    finally:
        plt.close()
    # これで sample.png という画像ファイルができました。

    # (2/7) 実行ファイル (.exe) の場所を決めます。
    exe_file = Path(r'F:\project\tools\pngquant\pngquant.exe')

    # (3/7) PNG ファイルの保存先を決めます。
    dst_file = Path(r'F:\project\data\sample_genshoku.png')
    # (dst は destination デスティネーション の略です)

    # (4/7) 画像の色数(いろすう,いろかず,しきすう)を決めます。
    # コマンドなので、文字列 (str 型) で指定します。
    ncolors = '256'
    # PNG は ncolors 以下の色数に減色されました。
    # '256' を指定したときに、
    # ぴったり 256 色使用された時もあれば、
    # 251 色しか使用されなかった時もありました。
    # (入力画像の内容や quality の max の設定によって変わりました)

    # (5/7) quality (品質)を決めます。
    # 通常は min-max ⇒ '0-100' で OK でした。
    quality = '0-100'
    # min 0 ⇒ (意訳) 結果的に出力画像の quality が低くなった場合でも、
    # スキップしたりせずに、減色した PNG ファイルを保存する。
    # max 100 ⇒ (意訳) ncolors の範囲内で、できるだけ
    # 多くの色を使用して PNG ファイルを保存する。

    # (6/7) コマンドを作ります。
    # タプル (tuple) でもリスト (list) でも OK です。
    # ファイルパスは『pathlib.WindowsPath 型』のままでも OK でした。
    # もちろん、str(src_file) で文字列に直したファイルパスでも OK でした。
    cmd = (
        exe_file,
        '--force', # dst_file が既に存在しているときに、上書き保存します。
        '--output', dst_file, # PNG ファイルの保存先です。
        '--quality', quality,
        '--strip', # PNG ファイルから Metadata を除去します。
        ncolors, # ncolors 以下の色数に減色します。
        '--', src_file, # 入力 PNG ファイル。
        )

    # (7/7) 実行します(減色された PNG ファイルができます)。
    cp = subprocess.run(cmd, stderr=subprocess.PIPE, timeout=30.0)
    # cp は CompletedProcess の略です。

    # stderr=subprocess.PIPE は、pngquant.exe の標準エラー出力 (stderr) を
    # cp.stderr から受け取るために設定しました。

    # timeout=30.0 は、一応指定しています。
    # たいていは一瞬で完了しました。なので、
    # 30 秒かかっても終わらないなら異常とみなすことにしました。
    # その時は、exception subprocess.TimeoutExpired が発生しました。

    # (デバッグ) Python のバージョンを表示します。
    import sys
    print(f'(Python) {sys.version}')

    # (デバッグ) コマンド (cmd) の内容を表示します。
    print(f'(cmd)\n{cp.args}')

    # (デバッグ) 入力画像のファイルサイズを表示します。
    src_file_size = src_file.stat().st_size
    print(f'(src_file_size) {src_file_size} bytes')

    # (デバッグ) 出力画像のファイルサイズを表示します。
    dst_file_size = dst_file.stat().st_size
    print(f'(dst_file_size) {dst_file_size} bytes')

    # (デバッグ) 標準エラー出力 (bytes 型) の内容を
    # 文字列にデコードして表示します (通常は空文字列のはず)。
    stderr = cp.stderr.decode('utf-8', errors='backslashreplace')
    # errors には 'strict', 'ignore', 'replace', 'backslashreplace'
    # などが設定できました。
    print(f'(cp.stderr) {stderr}')

    # (標準エラー出力の例)
    # error: 'F:\project\data\sample_genshoku.png' exists; not overwriting
    # このメッセージは、dst_file が存在しているのに
    # '--force' を付けなかった場合に表示されました。

    # (デバッグ) 戻り値を表示します(通常は 0 のはず)。
    print(f'(cp.returncode) {cp.returncode}')
    print('end')
    return


if __name__ == '__main__':
    main()

実行結果

コード例を実行したときの画面表示です。

src_file_size と dst_file_size の変化にある通り、画像データを削減することができました。

start
(Python) 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:52:53) [MSC v.1927 64 bit (AMD64)]
(cmd)
(WindowsPath('F:/project/tools/pngquant/pngquant.exe'),
'--force',
'--output', WindowsPath('F:/project/data/sample_genshoku.png'),
'--quality', '0-100',
'--strip',
'256',
'--', WindowsPath('F:/project/data/sample.png'))       
(src_file_size) 25332 bytes
(dst_file_size) 8117 bytes
(cp.stderr)
(cp.returncode) 0
end

256 色であれば、ファイルサイズを半分以下にしつつ、ほとんど同じ見た目の画像にすることができました。

Web サイトや各種画像ビューアでの表示も高速になりました。

自分は matplotlib で作成した大量のグラフ画像をサクサク見ていくために、pngquant を使用しています。

元の画像と減色した画像の比較

過去に実験したときの画像を紹介します。

コード例で使用したもの (2.17.0 (September 2021)) と比べて、pngquant (2.12.0 (January 2018)) とバージョンが古いのですが、参考にはなると思います。

(グラフ画像の作成に使用した matplotlib のバージョンも異なります)

元画像 (1284色)

元画像です。1284色ありました。(ファイルサイズ 29,048 バイト)

元画像 (1284色)

ncolors = 64色

64色です。(ファイルサイズ 7,088 バイト)ncolors=64色

ncolors = 8色

8色です。(ファイルサイズ 4,570 バイト)

ncolors=8色

少しギザギザした感じが出てきましたが、まだ大丈夫です。すばらしいです。

ncolors = 4色

4色です。(ファイルサイズ 3,845 バイト)ncolors = 4色

色がなくなってしまいました。このグラフだと8色までが限界のようでした。

色数の調べ方

PNG 画像に使用されている色数(いろすう、いろかず、しきすう)の調べ方です。

自分は ImageMagickイメージマジック (magick.exe) のコマンドで調べました。

(www.imagemagick.org) ImageMagick

やり方です。

『Single Letter Attribute Percent Escapes』の表にある『%k CALCULATED: number of unique colors』という機能を使用します。

(www.imagemagick.org) %k – Single Letter Attribute Percent Escapes

コマンドライン文字列の例です。

"F:\project\tools\ImageMagick\magick.exe" identify -format %k "F:\project\data\sample.png"

実行すると、『1403(色)』などの数字で、画像に使用されている色数が返ってきました。

以上です。

スポンサーリンク
シェアする(押すとSNS投稿用の『編集ページ』に移動します)
フォローする(RSSフィードに移動します)
スポンサーリンク
シラベルノート
タイトルとURLをコピーしました