PNG 形式の画像データ (bytes 型) を減色して圧縮する Python コード例を書きました。
コマンドラインツールの pngquant を使用して減色しました。
Windows 用の pngquant.exe を、Python の subprocess.run() から呼び出して使用します。
標準入力 (stdin, input) から PNG データを読み込んで、減色した PNG を標準出力 (stdout) から受け取りました。
※ 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』に載っていました。
公式サイトにも説明がありました。
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) bytes.decode(encoding='utf-8', errors='strict')
(docs.python.org) 標準エンコーディング encoding='cp932', 'shift_jis', 'euc_jp', 'utf_8', 'utf_16', ...
(docs.python.org) subprocess.PIPE
(docs.python.org) exception UnicodeDecodeError
(docs.python.org) exception subprocess.TimeoutExpired
(docs.python.org) class subprocess.CompletedProcess
(docs.python.org) Path.stat(*, follow_symlinks=True)
(docs.python.org) class os.stat_result
コード例
『標準入力 (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
以上です。