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

Python

PNG 形式の画像データ (bytes 型) を減色して圧縮する Python コード例を書きました。

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

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

標準入力 (stdin, input) から PNG データを読み込んで、減色した PNG を標準出力 (stdout) から受け取りました。

※ PNG ファイルを指定して、結果を別の PNG ファイルに保存する方法は、以下の記事に書きました。

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) Path.open(mode='r', buffering=- 1, encoding=None, errors=None, newline=None)

(docs.python.org) class io.BufferedReader(raw, buffer_size=DEFAULT_BUFFER_SIZE)

(docs.python.org) read([size])

(docs.python.org) class io.BufferedWriter(raw, buffer_size=DEFAULT_BUFFER_SIZE)

(docs.python.org) write(b)

(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) stdout

(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

コード例

『標準入力 (stdin, input)』に PNG データ (bytes 型) を渡して減色する Python コード例です。

結果も『標準出力 (stdout)』から受け取ります (bytes 型) 。

"""
PNG 画像を減色して圧縮する Python コード例です。
pngquant.exe を使用して圧縮します。
『標準入力 (stdin の input)』から PNG データを渡して減色します。
結果も『標準出力 (stdout)』から受け取ります。
"""
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 という画像ファイルができました。
    # これをバイナリモードで開いて、PNG データ (bytes 型) を取得します。
    with open(src_file, 'rb') as fr:
        src_data = fr.read()

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

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

    # (4/7) quality (品質)を決めます。
    # 通常は min-max ⇒ '0-100' で OK でした。
    quality = '0-100'
    # min 0 ⇒ (意訳) 結果的に出力画像の quality が低くなった時でも、
    # 元画像を返したりせずに、減色した PNG データを返す。
    # max 100 ⇒ (意訳) ncolors の範囲内で、できるだけ
    # 多くの色を使用して PNG データを作る。

    # (5/7) コマンドを作ります。
    # タプル (tuple) でもリスト (list) でも OK です。
    # ファイルパスは『pathlib.WindowsPath 型』のままでも OK でした。
    # もちろん、str(src_file) で文字列に直したファイルパスでも OK でした。
    cmd = (
        exe_file,
        '--quality', quality,
        '--strip', # PNG データから Metadata を除去します。
        ncolors, # ncolors 以下の色数に減色します。
        '-', # PNG データを標準入力から受け取って、標準出力に出します。
        )

    # (6/7) 実行します。
    # subprocess.run() で標準入力に bytes 型のデータを渡すときは、
    # 『引数 input=』の方を使用します。引数 stdin= は使用しません。
    cp = subprocess.run(
        cmd,
        input=src_data, # PNG データ (bytes 型)
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        timeout=30.0,
        )
    # cp は CompletedProcess の略です。

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

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

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

    # (7/7) 標準出力から PNG データ (bytes 型) を受け取ります。
    # (dst は destination デスティネーション の略です)
    dst_data = cp.stdout

    # (デバッグ) PNG データをファイルとして保存します。
    dst_file = Path(r'F:\project\data\sample_genshoku.png')
    with dst_file.open('wb') as fw:
        fw.write(dst_data)

    # (デバッグ) 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}')

    # (デバッグ) 戻り値を表示します(通常は 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'),
'--quality', '0-100',
'--strip',
'256',
'-')
(src_file_size) 25332 bytes
(dst_file_size) 8117 bytes
(cp.stderr)
(cp.returncode) 0
end

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

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

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

※ 画像の色数をどんどん減らしていった時の見た目の変化は、以下の記事に掲載しました。

PNG を減色して圧縮するコード例(画像ファイル)⇒ 元の画像と減色した画像の比較

pngquant のエラーメッセージの例

標準入力に渡した PNG データが壊れていたときに表示されたエラーの例です。

リターンコードは 25 でした。

25
error: IDAT: invalid distances set (libpng failed)
error: cannot decode image from stdin

以上です。

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