JavaScript と PHP で画像をクロップ (切り抜き) する方法
Web 画面上で画像を切り取るプログラムの作り方を説明します。
まずは出来上がりをご覧ください。ボタンを押すと右側に、切り取られた画像が表示されます。 これは単にその部分を「表示」しているだけではなく、切り取った部分だけの JPEG ファイルを作成してそれを表示しています。
元の画像 | 切り取り画像 (下のボタンをクリックすると作成されます) |
|
|
画像サイズ: 幅 px 高さ px
| クロップ領域: X 座標 px Y 座標 px
幅 px 高さ px |
このプログラムの作り方を解説します。が、いろいろな項目を知らないといけないので順を追って説明します。
プログラムの概要
まずはクライアント側は下図のようにいくつかの HTML 要素を重ね合わせています。
一番下にあるのは元画像。それに被さるのは同じ大きさの黒いレイヤーです。
これには透明度が設定されているので、一番下の画像が透けて見えます。
その上にクロップ領域を現す要素を配置しています。これは DIV 要素であり、
この要素の背景画像として元画像が指定されています。画像の内側一部を背景画像と
するには background-position CSS 要素を指定します。さらにその上には、
クロップ領域を変更するためのツマミを 8 つ配置しています。
クロップ領域及びツマミ部分には、マウスダウンイベントハンドラを設定しています。 マウスダウン時にドラッグ開始と認識して、マウスムーブイベントにて、クロップ領域の 再描画 (位置の変更) を行います。
これだけでクライアント側のクロップ指定は完了です。実装時にはいくつか注意する点が ありますのでその詳細は後述しますが、概要としてはこれだけです。簡単ですね。
ボタン押下時に、クロップ領域の座標情報を AJAX の方法でサーバーに送ります。 サーバー側では PHP プログラムが座標情報を用い、元画像からクロップされた JPEG 画像を生成します。サーバー側の処理結果をクライアントに返し、 成功していればその画像を表示します。
クライアント側の実装
コードの流れは大きく次の通りです:
- 元画像のサイズを取得
- 影要素 (黒の透明要素) のサイズを元画像と同一にする
- 影要素の位置を変更して元画像にぴったり重ねる
- クロップ領域の初期値を設定
- 影要素の上にクロップ領域を移動し描画
この時、クロップ領域の背景画像のオフセットを指定する - クロップ領域の四隅及び辺の中央にツマミを移動
- document に mouseup 及び mouseup イベントハンドラ、クロップ領域及びツマミに mousedown イベントハンドラを設定する
ここまでで初期化終了 - クロップ領域またはツマミ上で mousedown イベント発生時にドラッグ開始フラグを立てる。さらに、mousedown イベント発生場所とイベント発生要素の ID を覚えておく
- mousemove イベント発生時に、mousemove イベント発生場所と mousedown イベント発生場所とのオフセットを計算する
mousedown イベント発生要素の ID に応じて、クロップ領域ならオフセット分だけドラッグ、 ツマミならツマミの位置によって上下左右の伸縮を行い、クロップ領域を移動する - mouseup イベント発生時にドラッグを終了
- ボタン押下時にクロップ領域の座標情報をサーバーに送信
- サーバーからの応答に応じて、画像を表示する
以上を踏まえてコードは以下の通りです:
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);