2015/04/06

コマンドでPNG形式とJPEG形式の画像ファイルを最適化(無劣化圧縮)する

PNG形式とJPEG形式の画像ファイルは画像としての情報を完全に保持しながらファイルサイズをある程度減らすことができる場合がある。

画像のファイルサイズを減らすと、容量が節約できる他、Webページに貼り付けてある場合は表示速度を高速化でき、データ転送量やサーバ負荷を減らすことにもつながる。

コマンドは作業を自動化するのに便利なので、大量の画像を一括で処理するのも楽にできる。記事の最後では、扱ったツールを用いて指定ディレクトリ以下の画像をまとめて最適化する、複数CPU(コア含む)対応スクリプトも貼り付けている。

  1. PNGファイルの最適化
    1. PNGファイルの最適化例
  2. JPEGファイルの最適化
    1. JPEGライブラリとツール
    2. mozjpegの導入例
    3. mozjpegを用いたJPEGファイルの最適化例
  3. 指定ディレクトリ以下のPNG/JPEG画像を全て最適化するスクリプト

PNGファイルの最適化

PNG形式のファイルはOptiPNGと呼ばれるツールを用いて簡単に最適化を行うことができる。[1]Debian/Ubuntuでは “optipng” という名前のパッケージを選択してインストールすることで導入できる。

このツールは処理対象のファイルを直接変更(上書き)して最適化し、ファイルサイズを削減する。

PNGファイルの最適化例

(高レベルの最適化)
$ optipng -o7 /path/to/input.png

(最大限の最適化・非常に時間がかかる)
$ optipng -o7 -zm1-9 /path/to/input.png

どの程度ファイルサイズが削減できるかは処理対象のファイルが最適化されていない度合いによるため、数値で表現することは難しいが、出力内に削減率が表示されるので、実行後に確認することはできる。

下の例ではGIMPで適当にスクリーンショットを撮って圧縮レベルを最高の “9” にしてエクスポートしたファイルを長時間かけて最大限に最適化しているが、元々1.6MiB強のファイルサイズが7.3KiB程度しか減っていない。

$ optipng -o7 -zm1-9 test.png
** Processing: test.png
1920x1080 pixels, 3x8 bits/pixel, RGB
Input IDAT size = 1682354 bytes
Input file size = 1684871 bytes

Trying:
  zc = 9  zm = 9  zs = 1  f = 4         IDAT size = 1677293

Selecting parameters:
  zc = 9  zm = 9  zs = 1  f = 4         IDAT size = 1677293

Output IDAT size = 1677293 bytes (5061 bytes decrease)
Output file size = 1677350 bytes (7521 bytes = 0.45% decrease)

下の例では同じスクリーンショットをGIMPで圧縮レベルを “1” [2]にして保存したものをOptiPNGの既定(省略可)の最適化レベルである-o2指定で最適化した場合(処理時間は短い)。

$ optipng -o2 test2.png
** Processing: test2.png
1920x1080 pixels, 3x8 bits/pixel, RGB
Input IDAT size = 1898435 bytes
Input file size = 1901264 bytes

Trying:
  zc = 9  zm = 8  zs = 0  f = 5         IDAT size = 1704003
  zc = 9  zm = 8  zs = 1  f = 5         IDAT size = 1682354

Selecting parameters:
  zc = 9  zm = 8  zs = 1  f = 5         IDAT size = 1682354

Output IDAT size = 1682354 bytes (216081 bytes decrease)
Output file size = 1682411 bytes (218853 bytes = 11.51% decrease)

JPEGファイルの最適化

JPEGライブラリとツール

JPEG画像を扱うライブラリには “libjpeg” と呼ばれるものがあるが、x86,x86_64,ARMのアーキテクチャ上でCPUのSIMD命令を活用して効率良く(高速に)処理を行うための “libjpeg-turbo” というものがあり、本家版と互換性があるためにGNU/Linuxでも広く使われている。

この “libjpeg-turbo” をMozillaプロジェクトが独自に改善し、既存のJPEGデコードプログラムで正しくデコードできる形で圧縮率を向上させるための “Mozilla JPEG Encoder Project (mozjpeg)” プロジェクトが存在し、バージョン3.0時点ではlibjpeg-turboとのABI互換性もある。[3]

libjpeg系のライブラリ(libjpeg-turboやmozjpegも含む)にはライブラリの他に幾つかのツール(コマンド)群が付属し、JPEG形式と他形式との変換や、既存のJPEGファイルの最適化などが行える。

mozjpegの導入例

mozjpegは2015年春時点ではディストリのパッケージとしては利用できないことが多いが、libjpeg-turboとのABI互換性などから、今後ディストリのパッケージがこちらに置き換えられていくことは考えられる。

以下はソースの取得から(手動)インストールまでの例で、バージョン3.1時点では標準で/opt/mozjpeg/以下にインストールされる。SIMD命令を用いる場合はyasmもしくはnasmも必要。[4]これらのソフトウェアはGNU/Linuxでは同名のディストリのパッケージとして利用可能なことが多い(Debian/Ubuntuでは両方とも利用できる)。

(2015/6/6)mozjpeg-[バージョン]-release-source.tar.gzをダウンロードする形に修正し、バージョン部分も3.1に更新

$ wget https://github.com/mozilla/mozjpeg/releases/download/v3.1/mozjpeg-3.1-release-source.tar.gz -O - | tar zxf -
$ cd mozjpeg/
[mozjpeg]$ ./configure
[mozjpeg]$ make
[mozjpeg]$ sudo make install-strip

(2015/6/6)“Source code” と書かれたファイルをダウンロードする場合はconfigureスクリプトはautoreconfコマンドで生成する必要があるのでautoconfautomakeに加えlibtool(パッケージ名も同じ)もインストールされている必要があり、libtoolが未インストールだと以下のエラーで失敗する。

[mozjpeg]$ autoreconf --install
configure.ac:22: error: possibly undefined macro: AC_PROG_LIBTOOL
      If this token and others are legitimate, please use m4_pattern_allow.
      See the Autoconf documentation.
      autoreconf: /usr/bin/autoconf failed with exit status: 1

mozjpegを用いたJPEGファイルの最適化例

付属ツールに含まれるjpegtranというコマンドを用いることで既存のJPEGファイルを無劣化[5]で最適化し、ファイルサイズを小さくできる。こちらもどの程度縮むかは処理対象のファイルが最適化されていない度合いによる[6]が、例えばデジタルカメラで撮影した写真などが沢山ある場合などにはmozjpegのjpegtranを用いて全て最適化していくことで合計でそれなりの効果は得られる。

(2015/5/10)脚注の圧縮率に関する記述を修正

オリジナルのlibjpeg-turboでも同様の操作が可能だが、最適化に-optimizeオプションが必要で、効果はmozjpegより弱い。

下はjpegtranで最適化を行う書式。将来mozjpegがディストリのパッケージとしてlibjpeg-turboを置き換えるようになった場合は “/opt/mozjpeg/bin/” の部分は不要になる。

(コメントとその他の情報を全て保持/ファイルサイズが最も大きい)
$ /opt/mozjpeg/bin/jpegtran -copy all -outfile /path/to/outfile.jpg /path/to/infile.jpg

(コメント以外の情報を削除)
$ /opt/mozjpeg/bin/jpegtran -outfile /path/to/outfile.jpg /path/to/infile.jpg

(コメントとその他の情報を全て削除/ファイルサイズが最も小さい)
$ /opt/mozjpeg/bin/jpegtran -copy none -outfile /path/to/outfile.jpg /path/to/infile.jpg

コメントに関する既定の動作は “コメント以外の情報を削除” [7]となっており、デジタルカメラなどが保存するEXIFの情報は消えてしまう。EXIFデータを残したいのか、捨てたい(ファイルサイズ削減目的含む)のか、目的に応じて指定を選択する。

指定ディレクトリ以下のPNG/JPEG画像を全て最適化するスクリプト

下はoptipngjpegtranを用いて引数に指定したディレクトリ以下の画像ファイルを全て最適化するスクリプト(CPU数に応じた並列化対応)。これらがインストールされている前提なので注意。最終更新日時は保持する。

[任意]ファイル名:optipngjpeg-recursive.py ライセンス:zlib
#! /usr/bin/python

# optipngjpeg-recursive.py - optimize JPEG and PNG files
# (C) 2015 kakurasan
# Licensed under zlib

from __future__ import print_function

import multiprocessing as mp
try:
  import queue
except:
  import Queue as queue
import subprocess
import shutil
import sys
import os


class Config:
  jpegtran = '/opt/mozjpeg/bin/jpegtran'  # path to jpegtran
  jpegtran_copy_markers = 'all'           # 'none', 'comments', or 'all'
  optipng = '/usr/bin/optipng'            # path to optipng
  optipng_opts = ('-o2',)
#  optipng_opts = ('-o7',)
#  optipng_opts = ('-o7', '-zm1-9')


def do_work (f, root):
  f_lower = f.lower ()
  if f_lower.endswith ('.jpg') or f_lower.endswith ('.jpeg') or f_lower.endswith ('.jpe'):
    dotpos = f.rfind ('.')
    name, ext = f[:dotpos], f[dotpos:]
    infile = os.path.join (root, f)
    tmpfile = os.path.join (root, '{0}-min{1}'.format (name, ext))
    try:
      subprocess.check_call ((Config.jpegtran, '-copy', Config.jpegtran_copy_markers, '-outfile', tmpfile, infile), stderr = subprocess.STDOUT)
    except subprocess.CalledProcessError:
      print ('FAILED "{0}"'.format (infile))
      return
    st_before = os.stat (infile)
    st_after = os.stat (tmpfile)
    os.utime (tmpfile, (st_before.st_atime, st_before.st_mtime))
    os.unlink (infile)
    shutil.move (tmpfile, infile)
    if st_after.st_size == st_before.st_size:
      print ('"{0}" is already optimized.'.format (infile))
    else:
      print ('optimized "{0}" ({1} bytes = {2:.2f}% decrease)'.format (infile, st_before.st_size - st_after.st_size, (st_before.st_size - st_after.st_size) * 100.0 / st_before.st_size))
  elif f_lower.endswith ('.png'):
    infile = os.path.join (root, f)
    args = [Config.optipng, '-quiet']
    for a in Config.optipng_opts:
      args.append (a)
    args.append (infile)
    st_before = os.stat (infile)
    try:
      subprocess.check_call (args, stderr = subprocess.STDOUT)
    except subprocess.CalledProcessError:
      print ('FAILED "{0}"'.format (infile))
      return
    st_after = os.stat (infile)
    os.utime (infile, (st_before.st_atime, st_before.st_mtime))
    if st_after.st_size == st_before.st_size:
      print ('"{0}" is already optimized.'.format (infile))
    else:
      print ('optimized "{0}" ({1} bytes = {2:.2f}% decrease)'.format (infile, st_before.st_size - st_after.st_size, (st_before.st_size - st_after.st_size) * 100.0 / st_before.st_size))

def worker (q):
  while True:
    try:
      f, root = q.get (True, 1)
      do_work (f, root)
      q.task_done ()
    except queue.Empty:
      break


if __name__ == '__main__':
  if len (sys.argv) < 2:
    sys.exit ('USAGE: {0} [DIR...]'.format (__file__))
  q = mp.JoinableQueue ()
  for topdir in sys.argv[1:]:
    for root, _, files in os.walk (topdir):
      for f in files:
        q.put ((f, root))
  for i in range (mp.cpu_count ()):
    p = mp.Process (target = worker, args = (q,))
    p.start ()
  q.join ()
使用例
$ /path/to/optipngjpeg-recursive.py mozjpeg-3.0/
optimized "mozjpeg-3.0/testimages/testorig.jpg" (286 bytes = 4.96% decrease)
optimized "mozjpeg-3.0/doc/html/tab_s.png" (60 bytes = 32.61% decrease)
optimized "mozjpeg-3.0/doc/html/sync_on.png" (29 bytes = 3.43% decrease)
"mozjpeg-3.0/doc/html/nav_f.png" is already optimized.
optimized "mozjpeg-3.0/doc/html/ftv2pnode.png" (66 bytes = 28.82% decrease)
optimized "mozjpeg-3.0/doc/html/ftv2ns.png" (106 bytes = 27.32% decrease)
optimized "mozjpeg-3.0/doc/html/ftv2link.png" (202 bytes = 27.08% decrease)
optimized "mozjpeg-3.0/doc/html/ftv2folderclosed.png" (113 bytes = 18.34% decrease)
optimized "mozjpeg-3.0/doc/html/doxygen.png" (1409 bytes = 37.28% decrease)
optimized "mozjpeg-3.0/doc/html/search/search_r.png" (3 bytes = 0.49% decrease)
optimized "mozjpeg-3.0/doc/html/search/mag_sel.png" (136 bytes = 24.16% decrease)
Unsupported JPEG data precision 12
FAILED "mozjpeg-3.0/testimages/testorig12.jpg"
"mozjpeg-3.0/testimages/testimgari.jpg" is already optimized.

...

optimized "mozjpeg-3.0/doc/html/bdwn.png" (28 bytes = 19.05% decrease)
"mozjpeg-3.0/doc/html/search/search_m.png" is already optimized.
optimized "mozjpeg-3.0/doc/html/search/close.png" (55 bytes = 20.15% decrease)
使用したバージョン:
  • OptiPNG 0.7.5
  • mozjpeg 3.0, 3.1
  • autoconf 2.69
  • automake 1.14.1
  • Python 2.7.8, 3.4.2
[1]: GIFやBMPなどの他形式からPNG形式に変換して圧縮することもできるが、元の形式のまま最適化することはできない
[2]: 圧縮される中で最も低い圧縮率
[3]: バイナリレベルで互換・つまりlibjpeg-turboを用いてビルドされたプログラムが、mozjpegへの置き換えで問題なく動作する
[4]: --without-simdオプションをconfigureへ渡すと無効化できる
[5]: JPEGライブラリ付属のデコードツールdjpegに最適化前後それぞれのファイルを指定すると(デコードの)出力は同じものになる
[6]: 幾つかの(最適化されていないと思われる)ファイルで試したところ、4パーセントから15パーセント程度の範囲で圧縮され、ファイルによって圧縮の程度は大きく異なる
[7]: -copy commentsオプション相当