概要

縮小画像を作成する際にファイルサイズ(バイト数)を制限して縮小画像を作ります。携帯電話で表示させるためにサムネイル画像を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 "";

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";

新たに作成される画像ファイルのファイル名を作成します。元画像と区別するために_x.という形式にします。

&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 "";

作成された新画像を表示します。表示サイズは最初の指定サイズです。二度目に作成されたサイズの画像をそのサイズで表示します。その辺は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します。
yum grouplist
yum groupinfo "X Window System"
yum upgrade xorg-x11-xfs
yum upgrade xorg-x11
yum upgrade xorg-x11-devel
yum upgrade xorg-x11-libs
yum groupupdate "X Window System"
yum upgrade xorg-x11-libs
yum install xorg-x11-Xvfb
yum install firefox



# ダウンロード
svn checkout http://svn.arege.jp/repos/mozshot/trunk mozshot

# mv trunk /usr/local/src/

# chmod +x /usr/local/src/trunk/mozshot.rb

# ln -s /usr/local/src/trunk/mozshot.rb /usr/local/bin/mozshot.rb

MozShot で必要なものをインストールする。
2 ruby を yum でインストール。

# yum install ruby ruby-gnome2 ruby-gtkmozembed

3 Xvfb 仮想フレームバッファ を yum でインストール。

# yum -y install Xvfb

4 サーバー環境であれば firefox を yum でインストール。

# yum -y install firefox

5 仮想フレームを常時起動させるのであれば ※ 手動は 6 を参照

# vi /etc/rc.d/init.d/xvfb

-------------------------ここから-------------------------

#!/bin/bash
#
# chkconfig: - 91 35
# description: Starts and stops XVfb. \
# used to provide virtual frame buffer.

# Source function library.
. /etc/init.d/functions

prog=$"Xvfb"

# Xvfb program
XVFB=/usr/bin/Xvfb

start() {
echo -n $"Starting $prog: "
daemon /usr/bin/Xvfb :1 -screen 0 1024x800x24 &
echo
}

stop() {
echo -n $"Shutting down $prog: "
killproc Xvfb
echo
}

# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
;;
*)
echo $"Usage: $0 {start|stop|restart}"
exit 1
esac

exit 0

-------------------------ここまで-------------------------

※ 1024x1024x24 は好きなサイズに変更してください。
  :1 は仮想ディスプレイ番号

# chmod 755 /etc/rc.d/init.d/xvfb

# /etc/rc.d/init.d/xvfb start

※ スタートさせたときにフォント系のエラーが出るかもしれませんが、取り合えず無視です。

# /sbin/chkconfig --add xvfb

# /sbin/chkconfig xvfb on

# /sbin/chkconfig --list xvfb
xvfb 0:off 1:off 2:on 3:on 4:on 5:on 6:off

6 仮想フレームを手動起動させるのであれば

起動するときは
# /usr/bin/Xvfb :1 -screen 0 1024x800x24 &

止めるときは
# killall Xvfb

※ 1024x800x24 は好きなサイズに変更してください。
  :1 は仮想ディスプレイ番号

MozShot側のサイズを変更するときは
# vi /usr/local/src/trunk/mozshot.rb

-------------------------ここから-------------------------

@opt = { :mozprofdir => "/home/server/.mozilla/mozshot",
:winsize => [1024, 800], :imgsize => [], ← 34行め付近
:timeout => 30, :imgformat => "png",
:keepratio => true, :shot_timeouted => false,
:retry => 0 }

-------------------------ここまで-------------------------

1024, 800 を変更する。

以上です。
これで環境が整いました。

簡単な説明です。
手動でサムネイルをとるとき

この色の文字は、スクリーンショットをとるサイトのURL
この色の文字は、スクリーンショットをとるサイトの画像名

なので、こんな感じ
$ mozshot.rb サイトURL 画像名

画像名を省略すれば、mozshot.png で作成される。

一連の流れはこんな感じ

仮想ディスプレイ番号を指定する。
$ export DISPLAY=:1.0

WEBサムネイル作成
$ mozshot.rb http://server.lunq.net/ server.png
Using profile proc-32331
Loading: http://server.lunq.net, opt: {:imgsize=>[], :timeout=>30, :imgformat=>"
png", :keepratio=>true, :mozprofdir=>"/home/server/.mozilla/mozshot", :shot_time
outed=>false, :winsize=>[1024, 800], :retry=>0}
Load Done.

できているかの確認
$ ll server.png
-rw-rw-r-- 1 server server 242843 2008-06-12 11:00 server.png

自動で複数のサイトのスクリーンショットをとるとき

まずスクリプトを作成する。
# vi webshot.sh

-------------------------ここから-------------------------

#!/bin/bash

export DISPLAY=:1.0

# カレントディレクトリのファイル名リストを配列に格納
files=(`cat ./weblist`)

# インデックスに @ を指定して、全ての要素を for 文の値リストに指定
for file in ${files[@]}
do

# URLから名前を作成
name=`echo $name | sed 's/http://g'`
name=`echo $file | sed 's/\///g'`
name=`echo $name | sed -e "s/\./_/g"`
name=`echo $name | sed -e "s/~/-/g"`

/usr/local/bin/mozshot.rb $file $name.png

done

exit 0

-------------------------ここまで-------------------------

実行権付与
# chmod 755 webshot.sh

スクリーンショットを作成したいURLのリストを作る。
# echo "http://server.lunq.net/" >> weblist

# echo "http://www.lunq.net/" >> weblist

# echo "http://www.google.com/" >> weblist

# echo "http://www.yahoo.co.jp/" >> weblist

リスとの確認をする。
# cat weblist
http://server.lunq.net/
http://www.lunq.net/
http://www.google.com/
http://www.yahoo.co.jp/

スクリプトを実行する。
# ./webshot.sh
Using profile proc-370
Loading: http://server.lunq.net/, opt: {:imgsize=>[], :timeout=>30, :imgformat=>
"png", :keepratio=>true, :mozprofdir=>"/home/server/.mozilla/mozshot", :shot_tim
eouted=>false, :winsize=>[1024, 800], :retry=>0}
Load Done.
Using profile proc-398
Loading: http://www.lunq.net/, opt: {:imgsize=>[], :timeout=>30, :imgformat=>"pn
g", :keepratio=>true, :mozprofdir=>"/home/server/.mozilla/mozshot", :shot_timeou
ted=>false, :winsize=>[1024, 800], :retry=>0}
Load Done.
Using profile proc-424
Loading: http://www.google.com/, opt: {:imgsize=>[], :timeout=>30, :imgformat=>"
png", :keepratio=>true, :mozprofdir=>"/home/server/.mozilla/mozshot", :shot_time
outed=>false, :winsize=>[1024, 800], :retry=>0}
Load Done.
Using profile proc-444
Loading: http://www.yahoo.co.jp/, opt: {:imgsize=>[], :timeout=>30, :imgformat=>
"png", :keepratio=>true, :mozprofdir=>"/home/server/.mozilla/mozshot", :shot_tim
eouted=>false, :winsize=>[1024, 800], :retry=>0}
Load Done.

画像を確認してみる。
# ls -al ./

-rw-rw-r-- 1 server server 92 2008-06-12 11:28 weblist
-rwxr-xr-x 1 server server 463 2008-06-12 11:30 webshot.sh
-rw-rw-r-- 1 server server 200932 2008-06-12 12:13 server_lunq_net.png
-rw-rw-r-- 1 server server 70981 2008-06-12 12:13 www_google_com.png
-rw-rw-r-- 1 server server 209454 2008-06-12 12:13 www_lunq_net.png
-rw-rw-r-- 1 server server 220340 2008-06-12 12:13 www_yahoo_co_jp.png


OK!

画像名は、上記のスクリプトで

「 http: 」は削除
「 / (スラッシュ) 」は削除
「 . (ドット) 」は「 _ (アンダーバー) 」
「 ~ (チルダ) 」は「 - (ハイフン) 」

に変更している。
Linuxサーバー環境でのサイトサムネイル化:PHPで実行
Linux・Unix・Apache (19 items)
2006年09月20日
SimpleAPIに代表されるサイトのサムネイル化サービスですが、最近では更に便利なサービスがどんどん出て来ていますね。それだけサイトのサムネイル化のニーズが高いんでしょう。
斯く言う当サイトも、Research Artisanの解析結果や、ランキングタグにてサイトサムネイル化機能を独自で開発し利用しています。

・・で、どないしてサイトのサムネイル化してんねん?って事ですが、有り難い事に既にネットでも詳しい情報が書かれています。
当サイトでは特に以下のサイトが参考になりました。(ありがとうございます)

ウエブサイトの自動スクリーンショット
SimpleAPI の仕組みについて考察してみる
webnail - webサイトをサムネイル化



サイトサムネイル化は、Windowsサーバー・Linuxサーバー双方で実現可能ですが、当サイトではLinuxサーバーで実現しています。なので、ここではLinuxサーバーにて実現する方法を記述します。

<スポンサードリンク>
上記参考サイトからわかるように、サイトサムネイル化の処理の流れは以下のようなイメージになると思います。

1.Xvfb(仮想フレームバッファ)起動

2.firefox(ブラウザ)起動

3.firefox(ブラウザ)でURLにアクセス

4.ImageMagickでスクリーンショットを取る

5.画像保存

6.firefox(ブラウザ)終了

7.Xvfb(仮想フレームバッファ)サーバー終了


要は、自分で1個1個サイトのサムネイルを作成する動き(ブラウザで対象URLにアクセスし、”Print Screen”キーでスクリーンショットを取って画像を保存する)を、Xvfb(仮想フレームバッファ)上でやっている、、それをコマンドで叩いている、だけの事なんですね。

この動きで大事なのが、仮想でGUI環境を作成するXvfb(仮想フレームバッファ)とスクリーンショットを取るImageMagickになります。これらはインストール必須。

→Xvfbインストール

yum grouplist

yum groupinfo "X Window System"

yum upgrade xorg-x11-xfs

yum upgrade xorg-x11

yum upgrade xorg-x11-devel

yum upgrade xorg-x11-libs

yum groupupdate "X Window System"

yum upgrade xorg-x11-libs

yum install xorg-x11-Xvfb

yum install firefox


→ImageMagickインストール

yum install ImageMagick




長々言葉で書いてもあれなんで、実現コード例を書いてみます。もちろんシェルでやるのが一般的かもですが、画像加工をGDでやりたいんでコマンド発行自体もPHPでやりたいと思います。(PHPのsystem関数でコマンド発行)

//Xvfb(仮想Xサーバー)起動

$command = '/usr/X11R6/bin/Xvfb :1 -screen 0 1024x768x24 > /dev/null &';

$last_line = system($command , $rtn);



//firefox起動(プロファイル”user”で起動)

$command = '/usr/bin/firefox -display :1 -width 1024 -height 800 -P "user" > /dev/null &';

$last_line = system($command , $rtn);

//60秒待つ

sleep(60);



$url = 'http://www.res-system.com/weblog/';



//サムネイル画像パス指定

$thumbimg = '/thumb/t.gif'; //サムネイル画像

$img = '/img/i.gif'; //サムネイル元画像



//firefox上でサイト表示

$command = '/usr/bin/firefox -display :1 -remote "openurl(' . $url . ')" &';

$last_line = system($command , $rtn);

//10秒待つ

sleep(10);



//ImageMagickでスクリーンショットを画像に保存

$command = '/usr/bin/import -display :1 -window root ' . $img;

$last_line = system($command , $rtn);



//保存した画像のサイズを求める

list($width, $height, $type, $attr) = getimagesize($img);



//画像の作成

$im = imagecreatefromgif($img);



//サムネイル画像加工(サムネイル元画像からサイズ縮小)

$thumb_im = imagecreatetruecolor(120, 90);

imagecopyresampled($thumb_im, $im, 0, 0, 0, 90, 120, 90, $width, $height);



//サムネイル画像を保存

imagegif($thumb_im, $thumbimg);



//firefox終了

$command = 'killall firefox-bin > /dev/null &';

$last_line = system($command , $rtn);



//Xvfb(仮想Xサーバー)終了

$command = 'killall Xvfb > /dev/null &';

$last_line = system($command , $rtn);


※コマンド発行後は、sleep関数である程度待つことも重要ですね。
※この処理は、ブラウザからのリクエスト毎にやるより、後でcronでまとめて処理したほうがいいでしょう。


さて、まあこんな感じで実現はできるんですが、サイトサムネイルという処理自体Linuxでやるものなの?っていう疑問もあると思います。いや、上記でできるんですが、如何せん処理時間が掛かるし、Flash満載のサイトのサムネイルが作成しにくかったりと問題も多く、、つまりWindowsサーバーでやる方がいいのではないかと思っています。(はてなスクリーンショットなんかもサムネイル化はWindowsサーバーのようです)
後、これをサービスとすると結構サーバーに負荷が掛かるんで中々難しい問題もあると思いますね。(だからサービス化しているサイトは素晴らしいと思いますが)