高速表示とサイズ削減のために、PNGを減色して圧縮する方法です。ピンクォワント(pngquant.exe)の標準入力(stdin)に画像データを渡して圧縮します。
普通にファイルパスを指定して圧縮する方法は「PNGを圧縮して高速化【Python, pngquant】」で紹介しています。pngquant.exe の入手方法や圧縮画像の品質比較も載っています。
それで標準入力を使用する理由なのですが、pngquant はどうも日本語を含むファイルパスを解釈できないようなんですね。以下のようなエラーメッセージが表示されます。
"D:\project\data_plot\日本語.png"
error: cannot open D:\project\data_plot\譌・譛ャ隱・png for reading
pngquant の中で文字化けしてしまって、存在しないファイルパスになっています。
そこで、ファイルの読み込みと保存はパイソンに任せて、pngquant には画像の圧縮だけをしてもらうことにします。pngquant には標準入力からデータを受け取る機能がありましたので、その機能を使用してパイソンコードを書きます。圧縮画像は、標準出力からバイナリデータとして受け取ることができます。
以下、標準入出力でPNG画像を圧縮するコード例です。
モジュール群のインポート
exeファイルを使用するので、subprocess関連をインポートします。
また、標準エラー出力のバイナリデータをデコードするために、ロケールモジュール(locale)もインポートします。標準入出力でバイナリデータを扱う関係上、標準エラー出力もバイナリデータになるので、これでシステムのエンコードを推定してデコードに利用します。
マットプロットリブ(matplotlib)は、テスト画像を作るためにインポートしています。
import os
from subprocess import Popen
from subprocess import PIPE
from subprocess import TimeoutExpired
import traceback
# テスト画像生成用のマットプロットリブ
from matplotlib import pyplot as plt
PNG圧縮関数 (pngquant.exe)
pngquant のコマンドライン文字列を生成して、実行する関数です。オプションの説明は pngquant に同梱されている「README.txt」に載っています。
入力ファイルパスを指定する部分にハイフン'-'
だけを指定すると、標準入力(stdin)からデータを読み込んで、結果を標準出力(stdout)に返してくれるようになります。この標準出力は、p.communicate() から標準エラー出力(stderr)と一緒に受け取ります。
標準入力にはバイナリデータを渡します。ここでテキストデータを渡してしまうと、 pngquant.exe が延々と入力待ちのような状態になってしまいます。p.communicate()のタイムアウトも発生しないようです。
ほかのオプションですが、基本的には色数(ncolors)の設定だけで良いと思います。これが一番見た目とサイズに影響するオプションです。画像にもよりますが、ncolors=256(色)なら、ほぼ同じ見た目で半分以下のサイズになりました。
その他の speed、quality、strip などは、指定しなくても大丈夫です。デフォルトの状態でも、特に問題なく圧縮してくれました。
def compress_image_stdio(
exe_file, image_data, ncolors,
speed=None, quality=None, strip=None,
timeout=30):
"""標準入力にデータを渡して、標準出力から結果を受け取る。"""
# スピードオプション
if speed is None:
speed_op = ''
else:
speed_op = ' --speed {speed}'.format(speed=speed)
# クオリティオプション
if quality is None:
quality_op = ''
else:
quality_op = ' --quality {min}-{max}'.format(
min=quality[0],
max=quality[1],
)
# ストリップオプション
if strip is True:
strip_op = ' --strip'
else:
strip_op = ''
# 色数オプション
ncolors_op = ' {ncolors}'.format(ncolors=ncolors)
# データを標準入力から受け取って標準出力で返すオプション
std_io_op = ' -'
# コマンドライン文字列生成
cmd = '"{exe}"{quality}{speed}{strip}{ncolors}{std_io}'.format(
exe=exe_file,
quality=quality_op,
speed=speed_op,
strip=strip_op,
ncolors=ncolors_op,
std_io=std_io_op,
)
# 子プロセス実行
with Popen(
cmd,
stdin=PIPE, stdout=PIPE, stderr=PIPE,
shell=False, universal_newlines=False) as p:
try:
# 標準入力にバイナリデータを渡して、子プロセスの終了を待つ。
(bin_outs, bin_errs) = p.communicate(
input=image_data, timeout=timeout)
except TimeoutExpired:
# 子プロセスを終了
p.kill()
# 通信を再試行
(bin_outs, bin_errs) = p.communicate(timeout=timeout)
return (cmd, p.returncode, bin_outs, bin_errs)
コマンドライン文字列の例です。入出力のファイルパスが無いので短いです。
"D:\\project\\tool\\pngquant\\pngquant.exe" --quality 0-100 --strip 64 -
メイン関数
テスト画像を保存して、それをパイソンで開き、pngquant で圧縮して、パイソンで保存するコード例です。
画像は開く時も保存する時もバイナリモードです。pngquant に渡すデータも受け取るデータもバイナリデータです。ファイルパスをパイソン側で処理しているので、パスに日本語を含む画像でも圧縮することができます。
def main():
"""テスト"""
# コマンドラインツールのファイルパス
pngquant_exe = r'D:\project\tool\pngquant\pngquant.exe'
# データフォルダ
data_dir = r'D:\project\data_plot'
# 入力・出力ファイル
filename = "000.png"
in_file = os.path.join(data_dir, filename)
out_file = os.path.join(
data_dir, '%s_compressed%s' % os.path.splitext(filename))
# 子プロセスのタイムアウト(秒)
timeout = 30
# 標準出力と標準エラー出力のデコードに使うエンコーディング
encoding = 'utf-8'
# 作図して保存
(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(in_file)
finally:
plt.close()
# Pythonで画像をバイナリモードで読み込む
with open(in_file, 'rb') as fr:
image_data = fr.read()
# pngquant.exe で圧縮
(cmd, returncode, bin_outs, bin_errs) = compress_image_stdio(
pngquant_exe,
image_data,
ncolors=64,
speed=None,
quality=(0, 100),
strip=True,
timeout=timeout,
)
# エラーがあれば表示
if returncode != 0:
print('%s\n%s\n%s' % (
cmd, returncode, bin_errs.decode(encoding)))
elif bin_errs:
print('%s\n%s\n%s' % (
cmd, returncode, bin_errs.decode(encoding)))
if bin_outs:
# Pythonで画像を保存
with open(out_file, 'wb') as fw:
fw.write(bin_outs)
return
何らかのエラーで圧縮に失敗した場合は、標準出力から空のバイナリデータ(b''
)が返ってきます。空のファイルを作成しても仕方ないので、if文でスキップしています。空のバイナリデータが False と判定されることを利用しています。
ところで、エラーが発生した時の pngquant の標準エラー出力ですが、バイナリデータになっているので、表示するにはデコードする必要があります。問題は何のエンコーディングを指定するかですね。
試した範囲では、utf-8, shift-jis, cp932, ascii のどれでもデコードできました。pngquant.exe にかぎらず、標準出力や標準エラー出力に、何のエンコーディングを使って返してくるかは、ソフトによります。
半角のアルファベットと記号だけなら、どのエンコーディングでもよさそうでした。とりあえず utf-8 を使ってみて、だめならほかのエンコーディングを試す感じでいいと思います。
使おうとしているソフトのヘルプを見ても、テキストを標準出力や標準エラー出力に出すときに、なにでエンコーディングして出しているか?までは書いてなかったです。なので、デコードに失敗したらほかのエンコーディングを試してみる、というアプローチでいいと思います。
pngquant のエラーメッセージの例
標準入力に渡したPNGデータが壊れていたときのエラーの例です。
リターンコードは25でした。
25
error: IDAT: invalid distances set (libpng failed)
error: cannot decode image from stdin