JavaScript と PHP で画像をクロップ (切り抜き) する方法

Web 画面上で画像を切り取るプログラムの作り方を説明します。

まずは出来上がりをご覧ください。ボタンを押すと右側に、切り取られた画像が表示されます。 これは単にその部分を「表示」しているだけではなく、切り取った部分だけの JPEG ファイルを作成してそれを表示しています。

元の画像 切り取り画像
(下のボタンをクリックすると作成されます)
 
画像サイズ: 幅 px 高さ px | クロップ領域: X 座標 px Y 座標 px 幅 px 高さ px

このプログラムの作り方を解説します。が、いろいろな項目を知らないといけないので順を追って説明します。

プログラムの概要

まずはクライアント側は下図のようにいくつかの HTML 要素を重ね合わせています。
一番下にあるのは元画像。それに被さるのは同じ大きさの黒いレイヤーです。 これには透明度が設定されているので、一番下の画像が透けて見えます。 その上にクロップ領域を現す要素を配置しています。これは DIV 要素であり、 この要素の背景画像として元画像が指定されています。画像の内側一部を背景画像と するには background-position CSS 要素を指定します。さらにその上には、 クロップ領域を変更するためのツマミを 8 つ配置しています。

クロップ領域及びツマミ部分には、マウスダウンイベントハンドラを設定しています。 マウスダウン時にドラッグ開始と認識して、マウスムーブイベントにて、クロップ領域の 再描画 (位置の変更) を行います。

これだけでクライアント側のクロップ指定は完了です。実装時にはいくつか注意する点が ありますのでその詳細は後述しますが、概要としてはこれだけです。簡単ですね。

ボタン押下時に、クロップ領域の座標情報を AJAX の方法でサーバーに送ります。 サーバー側では PHP プログラムが座標情報を用い、元画像からクロップされた JPEG 画像を生成します。サーバー側の処理結果をクライアントに返し、 成功していればその画像を表示します。


クライアント側の実装

コードの流れは大きく次の通りです:

  1. 元画像のサイズを取得
  2. 影要素 (黒の透明要素) のサイズを元画像と同一にする
  3. 影要素の位置を変更して元画像にぴったり重ねる
  4. クロップ領域の初期値を設定
  5. 影要素の上にクロップ領域を移動し描画
    この時、クロップ領域の背景画像のオフセットを指定する
  6. クロップ領域の四隅及び辺の中央にツマミを移動
  7. document に mouseup 及び mouseup イベントハンドラ、クロップ領域及びツマミに mousedown イベントハンドラを設定する

    ここまでで初期化終了

  8. クロップ領域またはツマミ上で mousedown イベント発生時にドラッグ開始フラグを立てる。さらに、mousedown イベント発生場所とイベント発生要素の ID を覚えておく
  9. mousemove イベント発生時に、mousemove イベント発生場所と mousedown イベント発生場所とのオフセットを計算する
    mousedown イベント発生要素の ID に応じて、クロップ領域ならオフセット分だけドラッグ、 ツマミならツマミの位置によって上下左右の伸縮を行い、クロップ領域を移動する
  10. mouseup イベント発生時にドラッグを終了
  11. ボタン押下時にクロップ領域の座標情報をサーバーに送信
  12. サーバーからの応答に応じて、画像を表示する

以上を踏まえてコードは以下の通りです:

Event.observe(window,'load',window_onload,false);

var CROP_WINDOW_MIN_WIDTH  = 20;
var CROP_WINDOW_MIN_HEIGHT = 20;

var g_crop_img_top		= 0;
var g_crop_img_left		= 0;
var g_crop_img_height		= 0;
var g_crop_img_width		= 0;

var g_crop_window_top		= 0;
var g_crop_window_left		= 0;
var g_crop_window_height	= 100;
var g_crop_window_width		= 100;

var g_crop_window_lkg_top    = 0;
var g_crop_window_lkg_left   = 0;
var g_crop_window_lkg_height = 0;
var g_crop_window_lkg_width  = 0;

var g_crop_pointerX0 = 0;
var g_crop_pointerY0 = 0;
var g_crop_curr_handle_id = null;

var g_crop_event_skip_cnt = 0;

var g_crop_dragging = false;


function window_onload(evt){

	crop_initialize();
	
}


function crop_initialize(){

	//
	// Image Shadow Layer
	//
	
	// Opacity	
	var html = new TKHtmlElement();
	html.setOpacity('crop_shadow', 0.6);

	// Size and Position
	var c = new TKCoord();
	g_crop_img_left   = c.getLeft('crop_img');
	g_crop_img_top    = c.getTop('crop_img');
	g_crop_img_width  = c.getWidth('crop_img');
	g_crop_img_height = c.getHeight('crop_img');
	 
	var ss = $('crop_shadow').style;
	ss.left = g_crop_img_left + 'px';
	ss.top = g_crop_img_top + 'px';
	ss.width = g_crop_img_width + 'px';
	ss.height = g_crop_img_height + 'px';

	// 
	// Crop Window
	//
	
	var def_x = 0.4;
	var def_y = 0.4;
	
	// Initial Size / Position
	var delta_x = parseInt( g_crop_img_width/2.0 - ( g_crop_img_width * def_x)/2.0 );
	var delta_y = parseInt( g_crop_img_height/2.0 - ( g_crop_img_height * def_y)/2.0 );
	
	g_crop_window_left   = g_crop_img_left + delta_x;
	g_crop_window_top    = g_crop_img_top  + delta_y;
	g_crop_window_width  = parseInt( g_crop_img_width * def_x );
	g_crop_window_height = parseInt( g_crop_img_height * def_y );	 
	
	crop_draw_window(g_crop_window_left, g_crop_window_top, g_crop_window_width, g_crop_window_height);		

	crop_set_event_handlers();
	
}


function crop_set_event_handlers(){

	var hs = document.getElementsByClassName('crop_handle');
	for(var i=0; i<hs.length; i++){
		Event.observe(hs[i], 'mousedown', crop_handle_onmousedown);
	}
	
	Event.observe('crop_window', 'mousedown', crop_handle_onmousedown);
	Event.observe( document, 'mouseup', crop_handle_onmouseup);
	Event.observe( document, 'mousemove', crop_handle_onmousemove);	

	Event.observe('crop_btn', 'click', crop_btn_onclick);
}


function crop_handle_onmousedown(evt) {

	var e = Event.element(evt);
	Event.stop(evt);
	
	g_crop_curr_handle_id = e.id;
	g_crop_pointerX0 = Event.pointerX(evt);
	g_crop_pointerY0 = Event.pointerY(evt);
	
	g_crop_dragging = true;
}


function crop_handle_onmouseup(evt) {

	if( !g_crop_dragging ){
		return;
	}
	g_crop_dragging = false;
	g_crop_curr_handle_id = null;
	
	g_crop_window_left   = g_crop_window_lkg_left;
	g_crop_window_top    = g_crop_window_lkg_top;
	g_crop_window_width  = g_crop_window_lkg_width;
	g_crop_window_height = g_crop_window_lkg_height;
		
}

function crop_handle_onmousemove(evt) {

	if(!g_crop_dragging){
		return;
	}

	if(( (++g_crop_event_skip_cnt) % 4)){
		return;
	}
	else{
		g_crop_event_skip_cnt = 0;
	}
	
	var x = Event.pointerX(evt);
	var y = Event.pointerY(evt);
	
	switch( g_crop_curr_handle_id ){
	case 'crop_h_tl':
		crop_stretch_window( (g_crop_pointerY0 - y), 0, 0, (g_crop_pointerX0 - x) );
		break; 
	case 'crop_h_tc':
		crop_stretch_window( (g_crop_pointerY0 - y), 0, 0, 0 );
		break;
	case 'crop_h_tr':
		crop_stretch_window( (g_crop_pointerY0 - y), (x - g_crop_pointerX0), 0, 0 );
		break;
	case 'crop_h_cl':
		crop_stretch_window( 0, 0, 0, (g_crop_pointerX0 - x) );
		break;
	case 'crop_h_cr':
		crop_stretch_window( 0, x - g_crop_pointerX0, 0, 0);
		break;
	case 'crop_h_bl':
		crop_stretch_window( 0, 0, (y - g_crop_pointerY0), (g_crop_pointerX0 - x) );
		break;
	case 'crop_h_bc':
		crop_stretch_window( 0, 0, (y - g_crop_pointerY0), 0 );
		break;
	case 'crop_h_br':
		crop_stretch_window( 0, (x - g_crop_pointerX0), (y - g_crop_pointerY0), 0 );
		break;
	case 'crop_window':
		crop_moving_window( x - g_crop_pointerX0, y - g_crop_pointerY0 );
		break;
	}
	
}

function crop_moving_window( x, y ){

	var x_max = g_crop_img_left + g_crop_img_width - g_crop_window_left - g_crop_window_width;
	var x_min = g_crop_img_left - g_crop_window_left;
	var y_max = g_crop_img_top + g_crop_img_height - g_crop_window_top - g_crop_window_height;
	var y_min = g_crop_img_top - g_crop_window_top;
	
	if( x_max < x ){
		x = x_max;
	}
	else if( x < x_min ){
		x = x_min;
	}
	
	if( y_max < y ){
		y = y_max;
	}
	else if( y < y_min ){
		y = y_min;
	}

	var l = g_crop_window_left + x;
	var t = g_crop_window_top + y;
	
	crop_draw_window(l,t,g_crop_window_width,g_crop_window_height);

}

function crop_stretch_window( n, e, s, w ){

	var n_max = g_crop_window_top - g_crop_img_top;
	var e_max = g_crop_img_left + g_crop_img_width - g_crop_window_left - g_crop_window_width;
	var s_max = g_crop_img_top + g_crop_img_height - g_crop_window_top - g_crop_window_height;
	var w_max = g_crop_window_left - g_crop_img_left;
	
	if( n_max < n ){
		n = n_max;
	}
	if( e_max < e ){
		e = e_max;
	}
	if( s_max < s ){
		s = s_max;
	}
	if( w_max < w ){
		w = w_max;
	}	

	var l = g_crop_window_left - w;
	var t = g_crop_window_top - n;
	var wi = g_crop_window_width + e + w;
	var h = g_crop_window_height + s + n;
		
	crop_draw_window(l,t,wi,h);	
}

function crop_draw_window(l,t,w,h){

	if( w<CROP_WINDOW_MIN_WIDTH || h<CROP_WINDOW_MIN_HEIGHT){
		return;
	}

	g_crop_window_lkg_top    = t;
	g_crop_window_lkg_left   = l;
	g_crop_window_lkg_height = h;
	g_crop_window_lkg_width  = w;

	
	var delta_x = l - g_crop_img_left;
	var delta_y = t - g_crop_img_top;

	var s = $('crop_window').style;
	s.backgroundPosition = '-' + ( delta_x + 1 ) + 'px -' + ( delta_y + 1 ) + 'px';
	s.left   = l + 'px';
	s.top    = t + 'px';
	s.width  = w + 'px';
	s.height = h + 'px';

	crop_move_handles(l,t,w,h);
	crop_show_info();
}

function crop_show_info(){

	$('crop_img_width').innerHTML = g_crop_img_width;
	$('crop_img_height').innerHTML = g_crop_img_height;
	$('crop_window_left').innerHTML = g_crop_window_lkg_left - g_crop_img_left;
	$('crop_window_top').innerHTML = g_crop_window_lkg_top - g_crop_img_top;
	$('crop_window_width').innerHTML =g_crop_window_lkg_width;
	$('crop_window_height').innerHTML = g_crop_window_lkg_height;

}


function crop_move_handles(l, t, w, h) {

	var x_l = l;
	var x_c = l + parseInt( w/2.0 ) + 1;
	var x_r = l + w + 1;
	var y_t = t;
	var y_m = t + parseInt( h/2.0 ) + 1;
	var y_b = t + h + 1;

	crop_move_handle('crop_h_tl', x_l, y_t );
	crop_move_handle('crop_h_tc', x_c, y_t );
	crop_move_handle('crop_h_tr', x_r, y_t );
	crop_move_handle('crop_h_cl', x_l, y_m );
	crop_move_handle('crop_h_cr', x_r, y_m );
	crop_move_handle('crop_h_bl', x_l, y_b );
	crop_move_handle('crop_h_bc', x_c, y_b );
	crop_move_handle('crop_h_br', x_r, y_b );

}


function crop_move_handle(id, x, y) {

	var s = $(id).style;
	
	s.left = ( x - 3 ) + 'px';
	s.top  = ( y - 3 ) + 'px';
	
}

function crop_btn_onclick(evt){

	$('crop_result').innerHTML = '<table><tr><td style="padding:0px;"><'
		+ 'img src="scr20/ajax-loader3.gif" '
		+ 'alt=""></td><td style="padding:0px;"> 画像を生成しています...'
		+ '</td></tr></table>';

	new Ajax.Request( 
		'get_crop_img.php',
		{ 
			method: 'POST', 
			parameters: { 
				'left': $('crop_window_left').innerHTML,
				'top': $('crop_window_top').innerHTML,
				'width': $('crop_window_width').innerHTML,
				'height': $('crop_window_height').innerHTML
			},
			onComplete: function(trans){
				
				var t = eval('(' + trans.responseText + ')');
				if(!t.code){
					$('crop_result').innerHTML = 'エラーが発生しました<br>' + r.msg;
				}
				else{
					$('crop_result').innerHTML = '<img src="' + t.src + '" alt="">';
				} 
			} 
		}
	);


}

サーバー側の実装

サーバー側はクロップ領域の座標を受け取り、ImageCopy を使って元画像からクロップ画像を作成しています。 特に何も工夫しているところはありませんので、いきなりコードを表示します。

	$r = array();
	
	$img = ImageCreateFromJPEG( $src_name );
	
	$dst_img = ImageCreateTrueColor($crop_width, $crop_height);
	ImageCopy($dst_img, $img, 0, 0, $crop_left, $crop_top, $crop_width, $crop_height);
	
	ImageJpeg( $dst_img, $dst_fname, 85 );

	$r['code'] = 1;
	$r['msg'] = 'OK';
	$r['src'] = $dst_fname;
	
	echo json_encode($r);

	ImageDestroy($img);
	ImageDestroy($dst_img);

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説