29 April 2010

(Linux) jpegtranを用いたgThumbのロスレス回転とExifデータの完全コピー

gThumbのロスレス回転はjpegtranライブラリを用いているため、『ロスレス回転』となっているはずだが、なぜか回転後のファイルサイズが小さくなるので、その機能に疑問を持っていた。

■ 問題点1 : そもそもロスレス回転しているのか

gThumbのソースコードを調べてみる。まず、ソースコードをダウンロードし、添付されていたパッチを当てて、とりあえずビルドする。


$ apt-get source gthumb

$ patch -p0 < gthumb_2.10.11-1ubuntu1.diff

$ cd gthumb-2.10.11

$ ./configure

$ make

まず、jpegtranがソースコードのどこから呼び出されているのか、調べてみると

src/rotation-utils.c から抜粋

gboolean apply_transformation_jpeg (FileData *file, GthTransform transform, JpegMcuAction mcu_action, GError **error)
{
tmp_dir = get_temp_dir_name ();
local_file = get_cache_filename_from_uri (file->path);
switch (transform) {
case GTH_TRANSFORM_ROTATE_90:
transf = JXFORM_ROT_90;
break;
case GTH_TRANSFORM_ROTATE_270:
transf = JXFORM_ROT_270;
break;
}
tmp_output_file = get_temp_file_name (tmp_dir, NULL);
jpegtran (local_file, tmp_output_file, transf, mcu_action, error);
local_file_move (tmp_output_file, local_file);
}

ちゃんと、jpegtranが呼び出されている。

では、検証してみよう。
適当なデジカメで撮影したjpegファイルを、jpegtranコマンドと、gThumbの双方で90度×4回を行って、出力されたファイルの全てのピクセルの相違を比較してみる。

まず、jpegtranコマンドライン版で回転


オリジナルのjpegファイルを保存しておく
$ cp imgp0037.jpg imgp0037.org.jpg

jpegtranで右90度回転を4回行う
$ cp imgp0037.jpg /tmp/tmp.jpg; jpegtran -rot 90 -copy all /tmp/tmp.jpg > imgp0037.jpg
$ cp imgp0037.jpg /tmp/tmp.jpg; jpegtran -rot 90 -copy all /tmp/tmp.jpg > imgp0037.jpg
$ cp imgp0037.jpg /tmp/tmp.jpg; jpegtran -rot 90 -copy all /tmp/tmp.jpg > imgp0037.jpg
$ cp imgp0037.jpg /tmp/tmp.jpg; jpegtran -rot 90 -copy all /tmp/tmp.jpg > imgp0037.jpg

無圧縮BMPファイルに展開する
$ djpeg -bmp imgp0037.org.jpg > imgp0037.org.bmp
$ djpeg -bmp imgp0037.jpg > imgp0037.bmp

BMPファイル同士を比較する
$ cmp -l imgp0037.bmp imgp0037.org.bmp

cmpコマンドが何も返さないので、双方のファイルは全く同一。つまり、ロスレス変換されている。

次に、gThumbでも右90度回転×4回を行って、ビットマップを比較してみる。こちらも、同様に変換前と後のビットマップファイルは全く同一だ。

ちなみに、cmpコマンドがちゃんと動いているのか、テキスト形式じゃないと表示されないんじゃないかという風に思ったので、Gimpでビットマップの1ヶ所に黒の点を書き込んだ画像と比較してみる。 2ヶ所の差異が表示された。


$ cmp imgp0037.org.bmp imgp0037.bmp
imgp0037.bmp imgp0037-mod.bmp differ: char 36, line 1

$ cmp -l imgp0037.org.bmp imgp0037.bmp
36 0 310
37 0 257
39 0 23
40 0 13
43 0 23
44 0 13
10080745 201 0
10080746 133 0
10080747 103 0

画面上で見るとこんな感じになっている。ファイル先頭部分は、BMPINFOHEADERの解像度がjpegtranではセットされていない(0になっている)ために差があるが、実際は、10080745バイトからの3バイト分が、「黒い点」の差異の部分だ。(vbindiffでの表示例)

20100501-jpegtran-01.png
BMPINFOHEADERの部分

20100501-jpegtran-02.png
1ピクセルの黒い点を描画した部分

参考資料 BMP ファイルフォーマット

結論1: gThumbはjpegtranを用いてロスレス回転を行っている

■ 問題点2 : Exif情報をちゃんと引き継げてないのではないか

これも、ソースコードを確認してみる。jpegtranのコマンドライン版は、デフォルトで全てのExif情報が引き継げない。(メーカーノートなど、標準データ以外の部分が切り捨てられる仕様)

これは、jpegtranの中で次のように設定されているためなのだが…

libgthumb/jpegutils/transupp.h から抜粋

typedef enum {
JCOPYOPT_NONE, /* copy no optional markers */
JCOPYOPT_COMMENTS, /* copy only comment (COM) markers */
JCOPYOPT_ALL /* copy all optional markers */
} JCOPY_OPTION;

#define JCOPYOPT_DEFAULT JCOPYOPT_COMMENTS /* recommended default */

ただし、gThumbではこのコマンドライン版のデフォルト値は使われておらず、jpegtran関数を呼び出すときにJCOPYOPT_ALLが無条件に指定されている。

libgthumb/jpegutils/jpegtran.c から抜粋

gboolean jpegtran (const char *input_filename, const char *output_filename, JXFORM_CODE transformation, JpegMcuAction mcu_action, GError **error)
{

input_file = fopen (input_filename, "rb");
output_file = fopen (output_filename, "wb");
jpeg_create_compress (&dstinfo);
jpeg_stdio_src (&srcinfo, input_file);
jpeg_stdio_dest (&dstinfo, output_file);
jpegtran_internal (&srcinfo, &dstinfo, transformation, JCOPYOPT_ALL, mcu_action, error);
jpeg_destroy_compress (&dstinfo);
jpeg_destroy_decompress (&srcinfo);
fclose (input_file);
fclose (output_file);
}

apply_transformation_jpeg 関数から呼び出された jpegtran関数の内部で、確かに 『 JCOPYOPT_ALL 』 が指定されている。これはメーカーノートも含めた全てのExifヘッダをコピーできるオプションのはずだ。

でも、元の画像ファイルと、gThumbで回転後のファイルのExifヘッダを比較してみる。


$ LANG=C;exif imgp0037.org.jpg
EXIF tags in 'imgp0037.org.jpg' ('Motorola' byte order):
--------------------+----------------------------------------------------------
Tag |Value
--------------------+----------------------------------------------------------
Manufacturer |PENTAX Corporation
Model |PENTAX *ist DS
~ 略 ~
Focal Length |23.0 mm
Maker Note |55296 bytes undefined data
FlashPixVersion |FlashPix Version 1.0
~ 略 ~

$ LANG=C;exif imgp0037.jpg
EXIF tags in 'imgp0037.jpg' ('Motorola' byte order):
--------------------+----------------------------------------------------------
Tag |Value
--------------------+----------------------------------------------------------
Manufacturer |PENTAX Corporation
Model |PENTAX *ist DS
~ 略 ~
Focal Length |23.0 mm
FlashPixVersion |FlashPix Version 1.0
~ 略 ~

確かにメーカーノートが削除されている。jpegtranのコマンドライン版のソースコードをダウンロードして、差異を見てみると、

コマンドライン版のリリース年は、おそらく2001年
* jpegtran.c
*
* Copyright (C) 1995-2001, Thomas G. Lane.

gThumbに入っている版のリリース年は、1997年
* jpegtran.c
*
* Copyright (C) 1995-1997, Thomas G. Lane.

この間に、Exifのハンドリング範囲が増えているのだろうか…

■ 完全にExifヘッダをコピーするための打開策

gThumbには「テンキー」に外部プログラムを割り付ける機能がある。それを使って、コマンドライン版のjpegtranを呼び出すことにする。

次のようなスクリプトファイルを作って、それを呼び出すようにした。


#!/bin/bash

if [ $# -ne 1 ]
then
echo "Rotate jpeg image 90-degree right, with jpegtran."
echo "usage : imgrotate90.sh input_filename"
exit
fi

SRC="$1"

EXT=${SRC##*.}
if [ $EXT != "jpg" -a $EXT != "JPG" ]
then
echo "(Error) Input file $SRC is not .jpg"
exit
fi

if [ ! -f $SRC ]
then
echo "(Error) Input file $SRC not exists"
exit
fi

TMP="/tmp/jpegtran$RANDOM$RANDOM$RANDOM.tmp"

if [ -f $TMP ]
then
echo "(Error) Temporary file $TMP already exists"
exit
fi

cp $SRC $TMP && jpegtran -rot 90 -copy all $TMP > $SRC

rm -f $TMP