画像ファイルサイズを制限したサムネイルの作り方

CGIテクニック集トップへ





概要

縮小画像を作成する際にファイルサイズ(バイト数)を制限して縮小画像を作ります。携帯電話で表示させるためにサムネイル画像を25KB等を上限として画像を作ることができます。当然画質はそれなりに劣化します。携帯用アルバムプロファイル2ディスカッションメッセージボード5等で使われています。 サムネイルの作り方もご参照ください。

必用な処理

  1. 元の画像ファイルからオブジェクトを作成
  2. そのオブジェクトから画像ファイルのサイズを取得
  3. サイズ変更後の画像のオブジェクトを作成
  4. そのオブジェクトに元オブジェクトからサイズを変更して画像をコピーする
  5. サイズ変更後のオブジェクトをファイルに書き出す
  6. 作成された新しいサイズの画像のファイルサイズを取得する
  7. そのファイルサイズが指定サイズよりも大きい場合に更に必要な縮小率を求める
  8. 新たに算出された縮小率を元に新画像サイズを割り出す
  9. 新画像サイズで画像ファイルを再作成する

サンプルコード

 use GD;
 
 my ($body,$path,$ext) = fileparse("$imagefile",'\.\w+');
 
 my $im;
 $im =~ /\.jpe?g$/i and $im = GD::Image->newFromJpeg($imagefile);
 $im =~ /\.gif$/i   and $im = GD::Image->newFromGif($imagefile);
 $im =~ /\.png$/i   and $im = GD::Image->newFromPng($imagefile);
 
 my ($width, $height) = $im->getBounds();
 
 my $new_width;
 my $new_height;
 if ($width > $target_width or $height > $target_height){
        my $width_shrink  = $target_width  / $width;
        my $height_shrink = $target_height / $height;
        my $shrink_ratio;
        if ($width_shrink < $height_shrink){
            $shrink_ratio = $width_shrink;
        } else {
            $shrink_ratio = $height_shrink;
        }
        $new_width  = int($width  * $shrink_ratio);
        $new_height = int($height * $shrink_ratio);
 } else {
        $new_width  = $width;
        $new_height = $height;
 }
 
 my $thumbsbody = "${body}_${new_width}x${new_height}";
 my $thumbimage = "$thumbsbody.$ext";
 &genimage($im,$new_width,$new_height,$width,$height,$thumbimage);
 
 my $byte = -s "$thumbimage";
 if ($byte > $size_limit){
        my $size_ratio = sqrt($size_limit/$byte);
        my $new_width2 = int($new_width * $size_ratio);
        my $new_height2 = int($new_height * $size_ratio);
        $thumbsbody = "${body}_${new_width2}x${new_height2}";
        $thumbimage = "$thumbsbody.$ext";
        &genimage($im,$new_width2,$new_height2,$width,$height,$thumbimage);
 }
 
 undef $im;
 
 print "<img src=\"$thumbimage\" width=\"$new_width\" height=\"$new_height\">";
 
 sub genimage {
    my ($im,$new_width,$new_height,$width,$height,$thumbimage) = @_;
    my $target_im = new GD::Image($new_width,$new_height,1);
    $target_im->copyResized($im,0,0,0,0,$new_width,$new_height,
                            $width,$height);
    
    unless (open(IMAGE, "> $thumbimage")){
        &error ("縮小画像ファイル作成に失敗しました。");
    }
    binmode(IMAGE);
    if ($thumbimage =~ /\.jpe?g$/i){
        print IMAGE $target_im->jpeg(85);
    } elsif ($thumbimage =~ /\.gif$/i) {
        print IMAGE $target_im->gif();
    } elsif ($thumbimage =~ /\.png$/i) {
        print IMAGE $target_im->png();
    }
    close(IMAGE);
    undef $target_im;
 }

コードの解説

 use GD;

GDを定義します。GDのさまざまなメソッドを利用します。


 my ($body,$path,$ext) = fileparse("$imagefile",'\.\w+');

画像ファイル名を後で使うためにパス名、ボディ名、拡張子と分けます。

 my $im;
 $im =~ /\.jpe?g$/i and $im = GD::Image->newFromJpeg($imagefile,1);
 $im =~ /\.gif$/i   and $im = GD::Image->newFromGif($imagefile);
 $im =~ /\.png$/i   and $im = GD::Image->newFromPng($imagefile);

$imagefileには画像ファイル名が入っていると仮定します。ファイルの拡張子によりファイルタイプを認識しGD::Image->newFromxxxによりそのファイルからオブジェクトを作成します。オブジェクトは$imに格納されます。

古いバージョンのGDではnewFromGifがサポートされていません。拡張子と実際の画像タイプが違う場合はGDからエラーがでます。ファイルサイズが0などとにかくGDを騙すようなファイルの場合にはGDがエラーになります。これらのエラーはFatalでスクリプト自体が動かなくなります。サーバーのerror log等から原因を解明しエラーの原因となるファイルを修正するとよいでしょう。

 $im = GD::Image->newFromJpeg($image,1);

newFromJpegメソッドの第二引数はTruecolor指定の際に1にします。ドキュメントにはファイルからオブジェクトを作成する場合は自動的にTruecolorになると書いてありますが作成された画像を見ると画質に違いがあるのでここでは指定します。

 my ($width, $height) = $im->getBounds();

getBoundsメソッドにより画像サイズを取得します。$widthに幅、$heightに高さ情報が入ります。

 my $new_width;
 my $new_height;
 if ($width > $target_width or $height > $target_height){
        my $width_shrink  = $target_width  / $width;
        my $height_shrink = $target_height / $height;
        my $shrink_ratio;
        if ($width_shrink < $height_shrink){
            $shrink_ratio = $width_shrink;
        } else {
            $shrink_ratio = $height_shrink;
        }
        $new_width  = int($width  * $shrink_ratio);
        $new_height = int($height * $shrink_ratio);
 } else {
        $new_width  = $width;
        $new_height = $height;
 }

$target_widthと$target_heightに予め設定されている縮小サイズが入っています。元画像から抽出した画像の高さと幅が縮小サイズより大きければ、幅と高さの縮尺比を出し縮小サイズに沿うように幅か高さの比のどちらかを選択します。その比を元の幅と高さに適用し新たな幅と高さを求めます。

 my $thumbsbody = "${body}_${new_width}x${new_height}";
 my $thumbimage = "$thumbsbody.$ext";

新たに作成される画像ファイルのファイル名を作成します。元画像と区別するために<filename>_<width>x<height>.<ext>という形式にします。

 &genimage($im,$new_width,$new_height,$width,$height,$thumbimage);

genimageファンクションにより新画像を作成します。このファンクションの説明は後ほどされています。この時点で$thumbimageで指定されているファイル名で縮小画像が作成されますが、ファイルサイズとしてはこの時点では指定サイズを超えている可能性があります。

 my $byte = -s "$thumbimage";

作成された画像のファイルサイズを取得します。$byteにファイルサイズが入ります。

 if ($byte > $size_limit){

新画像が指定したサイズより大きい場合に以下の処理を続けます。

        my $size_ratio = sqrt($size_limit/$byte);

指定サイズよりオーバーしたファイルサイズの比率を計算します。

        my $new_width2 = int($new_width * $size_ratio);
        my $new_height2 = int($new_height * $size_ratio);

サイズの比率を元に縦横のサイズを再度計算します。画像サイズ変更で、より小さな画像を作成し、ファイルサイズを指定サイズ以下に収めます。ファイルサイズを画像の縦横サイズにより制御しているので多少誤差が出る可能性もあります。小さな画像でもHTMLでの表示時に表示サイズを指定できるので画質はファイルサイズに沿って落ちますが目的は果たせます。

        $thumbsbody = "${body}_${new_width2}x${new_height2}";
        $thumbimage = "$thumbsbody.$ext";

新たに作り直す画像のファイル名を作成します。元画像に上書きしないようにします。上書きしたい場合はこのように新たにファイル名を作成し直す必要はありません。

        &genimage($im,$new_width2,$new_height2,$width,$height,$thumbimage);
 }

画像を新サイズで作成します。

 undef $im;

オブジェクトを廃棄します。通常この処理は必要ないのですが、場合によりガーベージコレクションが失敗しテンポラリファイルがサーバーに残る可能性があるので明示的にオブジェクトをundefします。

 print "<img src=\"$thumbimage\" width=\"$new_width\" height=\"$new_height\">";

作成された新画像を表示します。表示サイズは最初の指定サイズです。二度目に作成されたサイズの画像をそのサイズで表示します。その辺はHTMLで自由にできます。

 sub genimage {

genimageのファンクションを説明します。

    my ($im,$new_width,$new_height,$width,$height,$thumbimage) = @_;

引数を受け取ります。

    my $target_im = new GD::Image($new_width,$new_height,1);

$new_widthと$new_heightにはサイズ変更後の新しい幅と高さが入っています。そのサイズで新しい画像のオブジェクトを作成します。第三引数はTruecolorでイメージを作るときに1を指定します。

    $target_im->copyResized($im,0,0,0,0,$new_width,$new_height,
                            $width,$height);

copyResizedメソッドにより元画像のオブジェクトをサイズを変更して新オブジェクトにコピーします。参考までにcopyResizedは、

 $im->copyResized($sourceImage,$dstX,$dstY,
                     $srcX,$srcY,$destW,$destH,$srcW,$srcH)
 
 $sourceImage : 元の画像オブジェクト
 $dstX        : ターゲット画像の開始座標X
 $dstY        : ターゲット画像の開始座標Y
 $srcX        : 元画像の開始座標X
 $srcY        : 元画像の開始座標Y
 $destW       : ターゲット画像の幅
 $destH       : ターゲット画像の高さ
 $srcW        : 元画像の幅
 $srcH        : 元画像の高さ

の引数を取ります。

    unless (open(IMAGE, "> $thumbimage")){
        &error ("縮小画像ファイル作成に失敗しました。");
    }
    binmode(IMAGE);
    if ($thumbimage =~ /\.jpe?g$/i){
        print IMAGE $target_im->jpeg(85);
    } elsif ($thumbimage =~ /\.gif$/i) {
        print IMAGE $target_im->gif();
    } elsif ($thumbimage =~ /\.png$/i) {
        print IMAGE $target_im->png();
    }
    close(IMAGE);

ターゲットオブジェクトをファイルに書き出します。元のファイルと同じフォーマットの画像ファイルを作成するために拡張子でフォーマットを判断しています。元のファイルと同じフォーマットである必要はないので変換したい場合はここでできます。 作成された画像ファイルはサイズが変更されたサムネイルとなっています。

  $target_im->jpeg(85);

jpegメソッドの引数はQualityです。0から100までの数字になります。数字が大きいと画質はいいですがファイルサイズが大きくなります。省略するとjpegメソッドがちょうどいい画質を選ぶそうですが風景画像による経験からするとデフォルトではあまりいい画像になりません。100ではファイルサイズが大きすぎるので80から90がいいでしょう。

    undef $target_im;
 }

オブジェクトを廃棄します。通常この処理は必要ないのですが、場合によりガーベージコレクションが失敗しテンポラリファイルがサーバーに残る可能性があるので明示的にオブジェクトをundefします。

関連するCGIテクニック

画像サイズの取得
Image::Sizeを使って画像サイズを取得する方法
画像サイズ変更の仕方
サムネイルの作り方
画像プリロードの仕方
Exif情報の取得の仕方