PHP で JPEG ロスレス回転

ありそうで無かったので作ってみました。
php_jpegtransform-0.0.1.tgz

このモジュールのソースコードには IJG JPEG Library (libjpeg) より以下のファイルを拝借しています。
README (libjpeg のライセンスにより) jinclude.h jpegint.h transupp.c transupp.h


インストールはいつもの通りです。

tar xfz php_jpegtransform-0.0.1.tgz
cd php_jpegtransform
phpize
./configure --with-jpegtransform=/path/to/libjepeg-install-dir
make
sudo make install


この通り、画質を保ったまま JPEG 画像を回転できます。

オリジナル 90度・1回目 2回目 3回目 4回目

デジカメで縦位置撮影した画像にどうぞ。


関数はただ一つ、bool jpegtransform(string $src_path, string $dst_path, int $flags) のみ。
$src_path に元となる JPEG 画像のパス、$dst_path に出力先のパス、$flags に変形オプションを指定します。
$flags は以下の定数のビット和で指定します。

JT_FLIP_H
水平方向に反転
JT_FLIP_V
垂直方向に反転
JT_TRANSPOSE
左上-右下の軸を転置 (JT_FLIP_H | JT_ROT_90 に等しい)
JT_TRANSVERSE
右上-左下の軸を転置 (JT_FLIP_V | JT_ROT_90 に等しい)
JT_ROT_90
時計回りに 90度回転
JT_ROT_180
180度回転 (JT_FLIP_H | JT_FLIP_V に等しい)
JT_ROT_270
時計回りに 270度回転 (JT_FLIP_H | JT_FLIP_V | JT_ROT_90 に等しい)
JT_COPY_NONE
必要最低限のマーカー以外はコピーしない
JT_COPY_COMMENTS
必要最低限のマーカーに加え、COM (Comment) マーカーもコピーする
JT_COPY_ALL
APP1 (Exif) マーカー等のすべてのマーカーをコピーする。Exif 情報を残したまま回転させたいときはこのフラグを使う
JT_KEEP_MTIME
出力ファイルの更新時刻をオリジナルと同じにする

JT_COPY_* の優先順位は JT_COPY_ALL > JT_COPY_COMMENTS > JT_COPY_NONE です。これらのいずれも指定されなかった場合は JT_COPY_COMMENTS が指定されたものとして処理します。
JT_KEEP_MTIME が指定された場合、stat(2) や utimes(2) を直接使わずに、PHP 関数 filemtime() と touch() をコールして更新時刻の取得・変更を行っています。


以下はとりあえず全部のフラグを試してみるコードです。

<?php
!extension_loaded('jpegtransform') && (dl('jpegtransform.so') || exit(1));

var_dump(jpegtransform('sample.jpg', 'jt_flip_h.jpg',        JT_FLIP_H));
var_dump(jpegtransform('sample.jpg', 'jt_flip_v.jpg',        JT_FLIP_V));
var_dump(jpegtransform('sample.jpg', 'jt_transpose.jpg',     JT_TRANSPOSE));
var_dump(jpegtransform('sample.jpg', 'jt_transverse.jpg',    JT_TRANSVERSE));
var_dump(jpegtransform('sample.jpg', 'jt_rot_90.jpg',        JT_ROT_90));
var_dump(jpegtransform('sample.jpg', 'jt_rot_180.jpg',       JT_ROT_180));
var_dump(jpegtransform('sample.jpg', 'jt_rot_270.jpg',       JT_ROT_270));
var_dump(jpegtransform('sample.jpg', 'jt_copy_none.jpg',     JT_ROT_90 | JT_COPY_NONE));
var_dump(jpegtransform('sample.jpg', 'jt_copy_comments.jpg', JT_ROT_90 | JT_COPY_COMMENTS));
var_dump(jpegtransform('sample.jpg', 'jt_copy_all.jpg',      JT_ROT_90 | JT_COPY_ALL));
var_dump(jpegtransform('sample.jpg', 'jt_keep_mtime.jpg',    JT_ROT_90 | JT_COPY_ALL | JT_KEEP_MTIME));

function exif_binary_data_filter($data)
{
    if (is_array($data)) {
        return array_map('exif_binary_data_filter', $data);
    } elseif (is_string($data)) {
        if (preg_match('/[^\\x20-\\x7e]/', $data)) {
            return sprintf('binary or non-ascii character data (%d bytes)', strlen($data));
        } else {
            return $data;
        }
    } else {
        return $data;
    }
}

$exif1 = exif_binary_data_filter(exif_read_data('jt_copy_none.jpg'));
$exif2 = exif_binary_data_filter(exif_read_data('jt_copy_comments.jpg'));
$exif3 = exif_binary_data_filter(exif_read_data('jt_copy_all.jpg'));
var_dump($exif1, $exif2, $exif3);

var_dump(filemtime('sample.jpg'), filemtime('jt_copy_all.jpg'), filemtime('jt_keep_mtime.jpg'));