PHPの画像処理ライブラリ「DmImage」を公開しました

f:id:y_d:20121114001018p:plain
PHPで画像処理といえば

辺りを使うのが一般的でしょうか。軽いノリで小さいアプリを実装する時はGDをが向いてると思います。
でもGDってなかなかクセがありませんか?何が違うのか分からない様な似た名前の関数があったり、半透明画像を扱う際は必ず呼ばないといけない関数があったり、テキストの描画が面倒、色の指定が面倒、など。

その辺のややこしい処理をラップして、よく使う機能を実装したライブラリ「DmImage」を作りました。

このライブラリを使うと色々出来ますが、

  • 画像リサイズ
  • ファイル出力
  • DataSchemeURI変換
  • ActionScriptやCanvasAPIの様な図形描画、色指定
  • Instagram風の画像加工
  • フォントのバンドルによる綺麗なフォント描画

辺りが特徴になるかと思っています。

では、早速使い方の説明です。

使い方

使い方1 〜 画像の表示

サイズが横400x縦300で、色が0099FFで、透明度がFF(つまり透明ではない)の画像を作成し、出力している例です。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
$image->display();
exit;

f:id:y_d:20121113234651p:plain

使い方2 〜 DataSchemeURIを使った表示

DataSchemeURIを使った出力にも対応していますので、タグに直接画像を埋め込む事ができます。
なお、この機能を使用する際、ライブラリ内のディレクトリに一時ファイルを作成しますのでディレクトリの権限設定にご注意ください。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
 
?><!DOCTYPE html>
<head>
	<meta charset="UTF-8" />
</head>
<body>
	
	DATA SCHEME URIで表示
	<br>
	<img src="<?= $image->toDataSchemeURI() ?>">
	
</body>
</html>

このコードで出力すると実際に出力されるimgタグは次のようになっています。

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEs
CAYAAADtt+XCAAAEPElEQVR4Xu3VMQ3AMBDAwDT8CRTtl0CkqJ7vZu9+1juzAOCn
fQsA4MRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRA
AEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDE
QABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASA
xEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwE
gMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgM
BIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABI
DASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAA
SAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRA
AEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDE
QABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASA
xEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwE
gMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgM
BIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABI
DASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAA
SAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRA
AEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDE
QABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASA
xEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwE
gMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgM
BIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABI
DASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASAxEAA
SAwEgMRAAEgMBIDEQABIDASAxEAASAwEgMRAAEgMBIDEQABIDASA5AOLiwTvg1Oq
XwAAAABJRU5ErkJggg==">

f:id:y_d:20121113234738p:plain

使い方3 〜 画像のファイル出力

生成したインスタンスは画像ファイルとしても出力可能です。
下記の例ではpng画像1枚、jpg画像2枚、gif画像1枚をサーバー上にファイル出力しています。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
$image->textGraphics
	->setColor(0xFFFFFFFF)
	->setFontSize(30)
	->textTo(80, 150, 'Hello world.')
;
$image->saveTo('/path/to/dir/saved_image.png');
$image->saveTo('/path/to/dir/saved_image.jpg');
$image->saveTo('/path/to/dir/saved_image.jpeg');
$image->saveTo('/path/to/dir/saved_image.gif');

f:id:y_d:20121113234823p:plain

使い方4 〜 ダウンロードダイアログからの画像ダウンロード

下記のコードで画像がブラウザへダウンロードされます。
下記の例ではjpg形式で画像クオリティを50/100でブラウザへのダウンロードを行なっています。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
$image->startDownload('download','jpg',50);

f:id:y_d:20121113234837p:plain

使い方5 〜 背景色指定の一例

色指定の方法は0xFF001122のような16進数指定以外にも、Dm_Colorクラスを使用すれば色の微調整が可能です。

<?php
$color = Dm_Color::argb(1, 255, 0, 0); //RGB指定
$color->v *= 0.5; //明度を半分に(HSV)
$image = new Dm_Image(400,300, $color->toInt());
$image->display();
exit;

f:id:y_d:20121113234853p:plain

使い方6 〜 四角形の描画

XY座標を指定して画像へ四角形を描画します。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
$image->graphics->drawRect(20, 100, 360, 100);
$image->display();
exit;

f:id:y_d:20121113234920p:plain

使い方7 〜 図形描画時のstyle指定

描画前に塗りの色と線の色を指定し、図形描画できます。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
$image->graphics
	->lineStyle(0,0)
	->fillStyle(0xFFFFFFFF)
	->drawRect(70, 40, 200, 150)
	->lineStyle(10,0x6600FFFF)
	->fillStyle(0)
	->drawRect(100, 70, 200, 150)
	->lineStyle(20,0xFF3300FF)
	->fillStyle(0x9900AAFF)
	->drawRect(130, 100, 200, 150)
;
$image->display();
exit;

f:id:y_d:20121113234937p:plain

使い方8 〜 基本図形の描画

色々な図形を描画できます。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
$image->graphics
	->lineStyle(1,0xFFFFFFFF)
	->fillStyle(0xFF66BBFF)
	->drawRect(10, 10, 190, 140)
	->drawCircle(100, 225, 50)
	->drawEllipse(300, 75, 150, 100)
	->drawPie(300, 225, 150, 100, 0, 135)
;
$image->display();
exit;

f:id:y_d:20121113234951p:plain

使い方9 〜 直線図形の描画

直線を使って複雑な図形の描画も可能です。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
$image->graphics
	->lineStyle(1,0xFFFFFFFF)
	->fillStyle(0x99001199)
	->beginLineFill()
	->moveTo(200, 10)
	->lineTo(230, 100)
	->lineTo(340, 100)
	->lineTo(250, 160)
	->lineTo(300, 290)
	->lineTo(200, 190)
	->lineTo(100, 290)
	->lineTo(150, 160)
	->lineTo(60,  100)
	->lineTo(170, 100)
	->lineTo(200, 10)
	->endLineFill()
;
$image->display();
exit;

f:id:y_d:20121113234958p:plain

使い方10 〜 テキストの描画

文字の描画もできます。
GDでテキストを描画するのは面倒ですが、ライブラリ内にフォントが組み込んでいることで簡単に描画できます。

<?php
$image = new Dm_Image(400,300, 0xFF0099FF);
$image->textGraphics
	//->setFontFile('path/to/fontfile.ttf') //フォント指定したい場合
	->setColor(0xFFFFFFFF)
	->setFontSize(30)
	->textTo(70, 230, 'Hello world.',45)
	->setColor(0xFF000000)
	->textTo(244, 210, '日本語。',45)
	->setColor(0xFFFFFFFF)
	->textTo(240, 210, '日本語。',45)
;
$image->display();
exit;

f:id:y_d:20121113235014p:plain

使い方11 〜 filterを使った画像のリサイズ

filter機能を使うと画像の加工も簡単にできます。
デフォルトで画像のリサイズや切り抜きを行うクラスが用意されています。
下記の例では2つのfilterを組み合わせ、200x200に収まるサイズに画像をリサイズしています。

<?php
$originalImagePath = './horse.jpeg';
$width = 200;
$height = 200;
$fitFilter = new Dm_Image_Filter_Fit($width,$height,true);
$cropFilter = new Dm_Image_Filter_Crop($width,$height);
 
$image = new Dm_Image_File($originalImagePath);
$image->applyFilters(array(
		$fitFilter,
		$cropFilter
	));
 
?><!DOCTYPE html>
<head>
	<meta charset="UTF-8" />
</head>
<body>
	<div>
		元画像<br>
		<img src="<?=$originalImagePath?>">
	</div>
	<div>
		Filter適用後(縦<?=$width?> x 横<?=$height?> に収まるようにリサイズ)<br>
		<img src="<?=$image->toDataSchemeURI()?>" />
	</div>
</body>
</html>

f:id:y_d:20121113235050p:plain

使い方12 〜 Instagram風の画像加工フィルタ

Instagramっぽく画像を加工するクラスが3つ用意されています。
色の加工とフレームの組み合わせは変更可能です。

<?php
$originalImagePath = './horse.jpeg';
$filter1 = new Dm_Image_Filter_InstagramLoFi(300,1);
$filter2 = new Dm_Image_Filter_InstagramWalden(300,2);
$filter3 = new Dm_Image_Filter_InstagramToaster(300);
 
$image1 = new Dm_Image_File($originalImagePath);
$image1->applyFilter($filter1);
$image2 = new Dm_Image_File($originalImagePath);
$image2->applyFilter($filter2);
$image3 = new Dm_Image_File($originalImagePath);
$image3->applyFilter($filter3);
 
?><!DOCTYPE html>
<head>
	<meta charset="UTF-8" />
</head>
<body>
	<div>
		元画像<br>
		<img src="<?=$originalImagePath?>">
	</div>
	<div>
		Instagram風Filter適用後<br>
		<img src="<?=$image1->toDataSchemeURI()?>" />
		<img src="<?=$image2->toDataSchemeURI()?>" />
		<img src="<?=$image3->toDataSchemeURI()?>" />
	</div>
</body>
</html>

f:id:y_d:20121114232143p:plain

使い方13 〜 2枚の画像を合成

2枚の画像を合成できます。
下記の例では写真の上に半透明のロゴ画像を合成しています。

<?php
$filter = new Dm_Image_Filter_Fit(400,400);
$image = new Dm_Image_File(dirname(__FILE__).'/horse.jpeg');
$image->applyFilter($filter);
 
$logoImage = new Dm_Image_File(dirname(__FILE__).'/php.gif');
$image->draw($logoImage,20,20);
 
$image->display();
exit;

f:id:y_d:20121113235103p:plain

include方法

一応PSR-0に準拠したつもりなので、Zendのオートローダー等で自動で読み込めると思います。
namespaceは使用していませんが、SplClassLoaderを使う場合と使わない場合はそれぞれこんな感じです。お好きな方法で読み込んでください。

<?php
// SplClassLoaderを使用する場合
require_once 'SplClassLoader.php';
$DmDirPath = '/path/to/lib/';
$classLoader = new SplClassLoader(null, $DmDirPath);
$classLoader->register();

または

<?php
// SplClassLoaderを使用せず、1ファイル毎読み込む場合
$DmDirPath = '/path/to/lib/';
require_once $DmDirPath.'Dm/Color.php';
require_once $DmDirPath.'Dm/Image.php';
require_once $DmDirPath.'Dm/Image/Graphic/Interface.php';
require_once $DmDirPath.'Dm/Image/Graphic/Text.php';
require_once $DmDirPath.'Dm/Image/Graphic/Shape.php';
require_once $DmDirPath.'Dm/Image/File.php';
require_once $DmDirPath.'Dm/Image/Filter/Abstract.php';
require_once $DmDirPath.'Dm/Image/Filter/Fit.php';
require_once $DmDirPath.'Dm/Image/Filter/Crop.php';
require_once $DmDirPath.'Dm/Image/Filter/InstagramNormal.php';
require_once $DmDirPath.'Dm/Image/Filter/InstagramLoFi.php';
require_once $DmDirPath.'Dm/Image/Filter/InstagramWalden.php';
require_once $DmDirPath.'Dm/Image/Filter/InstagramToaster.php';


以上です。
サンプル: http://demouth.github.com/DmImage/
github: https://github.com/demouth/DmImage/

PHPのmb_strwidth()とmb_strimwidth()をJavaScriptで実装する

f:id:y_d:20120801003512p:plain

PHPのmb_strwidth()関数とmb_strimwidth()関数をJavaScriptで移植してみました。あまり動作確認してませんが、どうやらそれっぽく動いているようなので公開します。

mb_strwidth()

int mb_strwidth ( string $str [, string $encoding ] )

文字列 str の幅を返します。
マルチバイト文字は、通常はシングルバイト文字の倍の幅となります。

元となるPHPのインターフェースは第二引数までありますが、今回移植したものには第二引数$encodingはありません(UTF-8以外の動作確認してませんが、多分文字コードにかかわらずうまく動くとおもいます)。

PHPで次のコードを実行すると、16になります。

<?php
var_dump(mb_strwidth("abcアイウあいうえお")); //int(16)
?>

JavaScript版で同様のコードを実行しても16になります。

console.log(mb_strwidth("abcアイウあいうえお")); // 16

mb_strimwidth()

string mb_strimwidth ( string $str , int $start , int $width [, string $trimmarker [, string $encoding ]] )

文字列 str を指定した幅 width で丸めます。

こちらも一番最後の引数$encodingはありませんが、それ以外はPHP版と同じです。

PHPで次のコードを実行すると「Hello W...」と出力されます。

<?php
var_dump(mb_strimwidth("Hello World", 0, 10, "...")); //string(10) "Hello W..."
?>

JavaScript版でも同じく「Hello W...」となります。

console.log(mb_strimwidth("Hello World", 0, 10, "...")); //Hello W... 

ソース

下記のソースをコピペすれば使えます。

;(function(ns){
	/**
	 * mb_strwidth
	 * @param String
	 * @return int
	 * @see http://php.net/manual/ja/function.mb-strwidth.php
	 */
	var mb_strwidth = function(str){
		var i=0,l=str.length,c='',length=0;
		for(;i<l;i++){
			c=str.charCodeAt(i);
			if(0x0000<=c&&c<=0x0019){
				length += 0;
			}else if(0x0020<=c&&c<=0x1FFF){
				length += 1;
			}else if(0x2000<=c&&c<=0xFF60){
				length += 2;
			}else if(0xFF61<=c&&c<=0xFF9F){
				length += 1;
			}else if(0xFFA0<=c){
				length += 2;
			}
		}
		return length;
	};
	
	/**
	 * mb_strimwidth
	 * @param String
	 * @param int
	 * @param int
	 * @param String
	 * @return String
	 * @see http://www.php.net/manual/ja/function.mb-strimwidth.php
	 */
	var mb_strimwidth = function(str,start,width,trimmarker){
		if(typeof trimmarker === 'undefined') trimmarker='';
		var trimmakerWidth = mb_strwidth(trimmarker),i=start,l=str.length,trimmedLength=0,trimmedStr='';
		for(;i<l;i++){
			var charCode=str.charCodeAt(i),c=str.charAt(i),charWidth=mb_strwidth(c),next=str.charAt(i+1),nextWidth=mb_strwidth(next);
			trimmedLength += charWidth;
			trimmedStr += c;
			if(trimmedLength+trimmakerWidth+nextWidth>width){
				trimmedStr += trimmarker;
				break;
			}
		}
		return trimmedStr;
	};
	ns.mb_strwidth   = mb_strwidth;
	ns.mb_strimwidth = mb_strimwidth;
})(window);


//こんな感じで使えます
console.log(mb_strwidth("abcアイウあいうえお")); // 16

gistにもあります。
https://gist.github.com/3217440

以上です。

PHP5.2でstatic functionのオーバーライド

PHP5.2以前(PHP5.2含む)でstaticなfunctionの継承というと面倒な印象があるかと思いますが、簡単な方法を見つけたので紹介します。

やりたいこと

PHP5.2以前で下記のようにstaticなメソッドをオーバーライドしたいとします。
このソースではSuperClassを継承したSubClassでメソッドをオーバーライドして、new SubClass();を実行した時「SubClass::hoge()」と出力される想定です。
しかし実際にはこのソースを実行すると、期待とは違い「SuperClass::hoge()」と出力されてしまいます。

<?php
class SuperClass{
    public function __construct(){
        self::hoge();
    }
    public static function hoge(){
        var_dump("SuperClass::hoge()");
    }
}
class SubClass extends SuperClass{
    public static function hoge(){
        var_dump("SubClass::hoge()");
    }
}
new SubClass(); //"SubClass::hoge()"と出力されて欲しいがSuperClass::hoge()と出力される

よくある解決方法

上記の問題の対策についてググってよく見る解決方法は、マジック定数__CLASS__を使用する方法です。具体的なソースは次のような感じになり、このソースを実行すると期待通り「SubClass::hoge()」と出力されます。実際私も案件でこういったオーバーライドが必要な際にはこの手法を使っています。

<?php
class SuperClass{
    public function __construct($className=__CLASS__){
        call_user_func(array($className, 'hoge'));
    }
    public static function hoge(){
        var_dump("SuperClass::hoge()");
    }
}
class SubClass extends SuperClass{
    public function __construct($className=__CLASS__){
        parent::__construct($className);
    }
    public static function hoge(){
        var_dump("SubClass::hoge()");
    }
}
new SubClass(); //string(16) "SubClass::hoge()"

それにしてもこの方法では__constructをサブクラスでも実装しなおしているので、無駄な記述が増えます。PHPの経験が浅い人がこのソースを見た時何をしているかパッと見で分かりにくいというデメリットもあると思います。

もっと簡単な方法

__CLASS__を使うよりももっと簡単な方法があります。$thisを使う方法です。
実はstaticなfunctionでも$thisを使って呼び出すことができる事をご存知でしょうか。
この実装方法だとPHP5.2以前でも遅延的束縛っぽいことをでき、$thisを使った次のコードで想定通りstaticなfunctionをoverrideできます。

<?php
class SuperClass{
    public function __construct(){
        $this->hoge();
    }
    public static function hoge(){
        var_dump("SuperClass::hoge()");
    }
}
class SubClass extends SuperClass{
    public static function hoge(){
        var_dump("SubClass::hoge()");
    }
}
new SubClass(); //string(16) "SubClass::hoge()"

この方法のデメリットはstaticなfunctionを$thisを使って呼び出していることで、この仕様をしらない人からすると一見記述ミスに見えることでしょうか。
この書き方がいいか悪いか、意見が別れるところだと思います。

PHP5.3からは遅延静的束縛が使える

PHP5.3以降なら次のようにstatic::を使って呼び出せますので、こちらを使えば簡単に実装できるかと思います。
上記のような余計な事を考える必要はありませんね。

<?php
class SuperClass{
    public function __construct(){
        static::hoge();
    }
    public static function hoge(){
        var_dump("SuperClass::hoge()");
    }
}
class SubClass extends SuperClass{
    public static function hoge(){
        var_dump("SubClass::hoge()");
    }
}
new SubClass(); //string(16) "SubClass::hoge()"

 

以上です。
PHP5.2を使う機会はだんだん減ってくるかと思いますが、何かの役にたてばと思います。