Web Widget - Make a basic crop image function

之前工作有幸接觸到關於image crop的處理,這個功能可以運用到供使用者在線上上傳圖片,

並且針對此圖片選取crop的區塊,再將crop後的圖片鑲嵌到商品上來製作屬於自己客制化的

商品。由於當時功能早已建構出,因此現在是嘗試去建構屬於自己的crop function。

本篇主要說明,自行去製作一個簡易版的crop功能,會分成幾個部分:

一、建構image crop的模式

二、選取image crop的區塊

三、進行image crop的處理

簡易操作流程如下:

一開始直接針對一張圖片由點選button來進入crop模式,經由一個可調整大小的小區塊,利用

滑鼠點選並拖移小區塊右上、右下、左上、左下的方框進而調整crop大小。最後,點選button

傳送小區塊此時的x, y position與長、寬給server端的PHP做crop,處理完後在client端顯示crop的

結果。

以下圖片來源為Window 7 (我的圖片),以此程式作為示範之用


一、建構image crop的模式



起始畫面如上圖,相關程式碼如下:

一開始的圖片大小由HTML載入後,利用JavaScript來計算圖片適當的大小
<img id="activate_img"></img><br />
<input type="button" id="exec_crop_preview" value="crop image" />
<input type="button" id="clear_result" value="clear" />
<img src="images/ajax-loader.gif" id="process_img_crop" style="display:none" />
<div id="output_result"></div>

這個部分的JavaScript code如下,activate_w, activate_h變數主要是設定一張圖的寬與高限制,

假如您的原圖大小為:1952x3264,那麼經由下面的判斷式會進入以height為主的部分,

再以比例來比較,若原圖的比例大於縮圖比例,則新圖的高會等於縮圖,如此寬就會較小

,若是以寬為主(512),那麼縮圖後的高會很長。由於一般螢幕都是寬大於高的比例,因此

在這邊的考量是當原圖比例大於縮圖比例時就會以高為縮圖大小,以避免過長!

var img_obj = new Image();
var activate_w = 512;
var activate_h = 384;
img_obj.src = "Hydrangeas.jpg";
img_obj.onload = function(){
   var ori_aspect;
   var thumb_aspect;
   var new_height;
   var new_width;
   if(img_obj.width > img_obj.height){
      ori_aspect = img_obj.width/img_obj.height;
      thumb_aspect = activate_w/activate_h;
   } else{
      ori_aspect = img_obj.height/img_obj.width;
      thumb_aspect = activate_h/activate_w;
   } 
   if(ori_aspect >= thumb_aspect){
      new_height = activate_h;
      new_width = img_obj.width/(img_obj.height/activate_h); 
   } else{
      new_width = activate_w;
      new_height = img_obj.height/(img_obj.width/activate_w);
   }
   img_obj.width = new_width;
   img_obj.height = new_height;
   $("#activate_img").attr({
      "src": img_obj.src,
      "width" : img_obj.width,
      "height": img_obj.height
   });
};

再來是點選crop image button進入編輯模式,如下圖:


HTML語法如下:
<div class="crop_block_background">
   <div class="crop_block">
        <div id="crop_img"></div>
        <img id="crop_img_opacity"></img>
        <div class="exec_crop_region">
            <div id="adjudge_east_north" class="adjudge_style en"></div>
            <div id="adjudge_east_south" class="adjudge_style es"></div>
            <div id="adjudge_west_north" class="adjudge_style wn"></div>
            <div id="adjudge_west_south" class="adjudge_style ws"></div>
        </div>
        <div style="margin-top:20px">
            <input type="button" id="exec_crop" value="exec crop" />
            <input type="button" id="reset_crop" value="cancel" /><br />
            Drag Box Offset : <span id="reg_pos"></span><br />
        </div>    
   </div>
</div>

CSS語法如下:
.crop_block{
    position: absolute;
    background-color: #FFFFFF;
    border:1px solid #00F;
    z-index: 2;
}
.crop_block_background{
    background-image: url(images/background.png); 
    position: absolute;
    top: 0px;
    left: 0px;
    width:100%; 
    height:100%; 
    z-index:1; 
    display:none;
}
.adjudge_style{
    background-image:url(images/adjudge_region_direction.png);
    width: 10px;
    height: 10px;
    z-index: 4;
    position: absolute;
}
.exec_crop_region{
    border:1px dashed #000;
    width:125px;
    height:125px;
    position: absolute;
    cursor: move;
    z-index: 3;
}
#crop_img{
    position:absolute;
}
.en{
    top:-5px; right:-5px; 
    cursor: ne-resize;
}
.es{
    bottom:-5px; right:-5px; 
    cursor: se-resize;
}
.wn{
    top:-5px; left:-5px; 
    cursor: nw-resize;
}
.ws{
    bottom:-5px; left:-5px; 
    cursor: sw-resize;
}

此部分JavaScript Code如下:

var adjust_ini_l = 20;
var adjust_ini_t = 20;
var drag_box = {
    state : false, 
    min_w : 125, 
    min_h : 125, 
    w : 125, 
    h : 125, 
    ori_x : 0, 
    ori_y : 0, 
    m_x : 0, 
    m_y : 0, 
    e_mousedown : null
};
$("#exec_crop_preview").click(function(){     
    $(".crop_block").css({
       "left" : Math.floor($(window).width()/3),
       "top" : 0,
       "width":  img_obj.width,
       "height": img_obj.height+100,
       "padding": adjust_ini_l+"px "+adjust_ini_t+"px"
    });
    $("#crop_img_opacity").attr({
       "src": img_obj.src,
       "width" : img_obj.width,
       "height": img_obj.height
    });
    $("#crop_img_opacity").css("opacity", "0.3");
    $("#crop_img").css({
       "background-image": "url("+img_obj.src+")",
       "background-size" : img_obj.width+"px "+img_obj.height+"px",
       "width" : drag_box.min_w,
       "height": drag_box.min_h,
       "left"  : adjust_ini_l,
       "top"   : adjust_ini_t
    });
    $(".exec_crop_region").css({
       "left" : adjust_ini_l,
       "top" :  adjust_ini_t
    });
    $(".crop_block_background").show();
    drag_box.ori_x = $(".exec_crop_region").offset().left;
    drag_box.ori_y = $(".exec_crop_region").offset().top;
    offset_img.x   = $("#crop_img").offset().left;
    offset_img.y   = $("#crop_img").offset().top;
});

最外層的div為crop_block_background,主要是建立一個編輯模式的感覺,此底圖為一張半透

明圖,再來上一層為crop_block區塊,此區塊的position為JavaScript計算其值,預設是取螢幕

寬度的1/3值(左移多少)。再來是<div id="crop_img"></div>指的是圖片區塊,預設呈現的部分

為125x125,到時會因應拉大crop區塊而調整,但最小仍為125x125,皆由JavaScript控制。

接著是<img id="crop_img_opacity"></img>,這個區塊為透明圖部分,主要目的是呈現尚未

含括到的部分,因此在這裡主要有兩張圖重疊!

<div class="exec_crop_region"> ...  </div>這個部分為虛線區塊及四個控制鈕,包住目前的

crop_image呈現,也是控制可視大小的控制區塊。


二、選取image crop的區塊

這個部分是比較複雜的部分,主要功能分成:

1. 拖移區塊


相關JavaScript code如下:

var drag_box = {
    state : false, 
    min_w : 125, 
    min_h : 125, 
    w : 125, 
    h : 125, 
    ori_x : 0, 
    ori_y : 0, 
    m_x : 0, 
    m_y : 0, 
    e_mousedown : null
};
var crop_box = {w : 0, h : 0, x : 0, y : 0};
var offset_img = {x : 0 , y : 0};
var delta = {x : 0, y : 0};
$(".exec_crop_region").mousedown(function(e){
     console.log("exec_crop_region mousedown");
     if(!drag_box.state){           
        drag_box.state = true;
        drag_box.m_x = e.pageX;
        drag_box.m_y = e.pageY;
        drag_box.e_mousedown = this;
     }
     return false;
});
$(document).mouseup(function(e){
    if(drag_box.state){
       drag_box.state = false;
       drag_box.e_mousedown = null;
    }  
});
$(".crop_block").mouseout(function(){
    if(drag_box.state){
       $(".exec_crop_region").offset({
          "left" : $("#crop_img").offset().left,
          "top" : $("#crop_img").offset().top
       });
       drag_box.state = false;
       drag_box.e_mousedown = null;
    }
);
$(document).mousemove(function(e){
    if(drag_box.state){
       delta.x = e.pageX - drag_box.m_x;
       delta.y = e.pageY - drag_box.m_y;
       d_box_offset = $(".exec_crop_region").offset();
       $("#reg_pos").text(
         "box position:("+d_box_offset.left+","+d_box_offset.top+")");
       crop_box.x = outside_check(d_box_offset.left + delta.x,  
         drag_box.ori_x, img_obj.width  - drag_box.w + drag_box.ori_x);
       crop_box.y = outside_check(d_box_offset.top  + delta.y,
         drag_box.ori_y, img_obj.height - drag_box.h + drag_box.ori_y);
       $(".exec_crop_region").offset({
          "left" : crop_box.x,
          "top"  : crop_box.y
       });
       img_ori_x = outside_check(d_box_offset.left - offset_img.x, 0, 
                   img_obj.width  - drag_box.w);
       img_ori_y = outside_check(d_box_offset.top  - offset_img.y, 0, 
                   img_obj.height - drag_box.h);
       $("#crop_img").css({
          "background-position" : 
                "-"+(img_ori_x)+"px -"+(img_ori_y)+"px",
          "left" : img_ori_x + adjust_ini_l,
          "top"  : img_ori_y + adjust_ini_t 
       });
       drag_box.m_x = e.pageX;
       drag_box.m_y = e.pageY;
   }
});
function outside_check(value, bottom, top){
   if(value < bottom){
       value = bottom;
   }else if(value > top){
       value = top;
   }
   return value;
}

上面的JavaScript code主要是針對點選image crop區塊,可以做拖移的動作,並且侷限在圖

片內。在這邊提一下mousemove的部分,在取得位移的差值時,會利用outside_check function

來檢查是否低於最小及最大值(實際的圖寬高 - 不透明圖寬高)的界線。

crop_box.x, crop_box.y為計算crop image(虛線框框)位移後當下的position,

而img_ori_x, img_ori_y是針對不透明的圖片當下的position!

$(".exec_crop_region").offset是相對於document的位移;

而$("#crop_img").css是相對於它的parent的位移。

2. 拉大區塊



相關JavaScript code如下:
var adjudge = {
    ES : false,
    EN : false,
    WS : false,
    WN : false,
    m_x : 0, 
    m_y : 0,
    r_width : 0,
    r_height : 0,
    left : 0,
    top : 0,
    e_mousedown : null
  };
$("#adjudge_east_south").mousedown(function(e){
    if(!adjudge.ES){
        d_box_offset = adjudge_ini(e, this);
        adjudge.ES = true;
        adjudge.r_width  = img_obj.width  - 
                           (d_box_offset.left - drag_box.ori_x);
        adjudge.r_height = img_obj.height - 
                           (d_box_offset.top  - drag_box.ori_y); 
        adjudge.left = d_box_offset.left;
        adjudge.top  = d_box_offset.top;
    }
    return false;
});
$("#adjudge_east_north").mousedown(function(e){
    if(!adjudge.EN){
        d_box_offset = adjudge_ini(e, this);
        adjudge.EN = true;
        adjudge.r_width  = img_obj.width - 
                           (d_box_offset.left - drag_box.ori_x);
        adjudge.r_height = $(".exec_crop_region").height() + 
                           (d_box_offset.top  - drag_box.ori_y);
        adjudge.left = d_box_offset.left;
        adjudge.top  = d_box_offset.top + (drag_box.h - drag_box.min_h);
    }
    return false;
});
$("#adjudge_west_north").mousedown(function(e){
    if(!adjudge.WN){
        d_box_offset = adjudge_ini(e, this);
        adjudge.WN = true;
        adjudge.r_width  = $(".exec_crop_region").width()  
        + (d_box_offset.left - drag_box.ori_x);
        adjudge.r_height = $(".exec_crop_region").height() 
        + (d_box_offset.top  - drag_box.ori_y);
        adjudge.left = d_box_offset.left+ (drag_box.w - drag_box.min_w);
        adjudge.top  = d_box_offset.top + (drag_box.h - drag_box.min_h);
    }
    return false;
});
$("#adjudge_west_south").mousedown(function(e){
    if(!adjudge.WS){
        d_box_offset = adjudge_ini(e, this);
        adjudge.WS = true;
        adjudge.r_width  = $(".exec_crop_region").width() 
        + (d_box_offset.left - drag_box.ori_x);
        adjudge.r_height = img_obj.height 
        - (d_box_offset.top - drag_box.ori_y); 
        adjudge.left = d_box_offset.left + (drag_box.w - drag_box.min_w);
        adjudge.top  = d_box_offset.top;
    } 
    return false;
});
  
function adjudge_ini(e, this_obj){
   adjudge.m_x = e.pageX;
   adjudge.m_y = e.pageY;
   adjudge.e_mousedown = this_obj;
   return $(".exec_crop_region").offset();
}
  
$(document).mousemove(function(e){
   if(adjudge.e_mousedown != null){
      d_box_offset = $(".exec_crop_region").offset();
      curr_crop_w  = $(".exec_crop_region").width();
      curr_crop_h  = $(".exec_crop_region").height();
      delta.x = e.pageX - adjudge.m_x;
      delta.y = e.pageY - adjudge.m_y;
      if(adjudge.ES){
         crop_box.w = outside_check(curr_crop_w + delta.x, 
                      drag_box.min_w, adjudge.r_width);
         crop_box.h = outside_check(curr_crop_h + delta.y, 
                      drag_box.min_h, adjudge.r_height);
         crop_box.x = adjudge.left;
         crop_box.y = adjudge.top;
      }else if(adjudge.EN){
         crop_box.w = outside_check(curr_crop_w + delta.x, 
                      drag_box.min_w, adjudge.r_width);
         crop_box.h = outside_check(curr_crop_h - delta.y, 
                      drag_box.min_h, adjudge.r_height);
         crop_box.x = adjudge.left;
         crop_box.y = outside_check(d_box_offset.top + delta.y, 
                      drag_box.ori_y, adjudge.top);
      }else if(adjudge.WN){
         crop_box.w = outside_check(curr_crop_w - delta.x, 
                      drag_box.min_w, adjudge.r_width);
         crop_box.h = outside_check(curr_crop_h - delta.y, 
                      drag_box.min_h, adjudge.r_height);
         crop_box.x = outside_check(d_box_offset.left + delta.x, 
                      drag_box.ori_x, adjudge.left);
         crop_box.y = outside_check(d_box_offset.top  + delta.y, 
                      drag_box.ori_y, adjudge.top);
      }else if(adjudge.WS){
         crop_box.w = outside_check(curr_crop_w - delta.x, 
                      drag_box.min_w, adjudge.r_width);
         crop_box.h = outside_check(curr_crop_h + delta.y, 
                      drag_box.min_h, adjudge.r_height);
         crop_box.x = outside_check(d_box_offset.left + delta.x, 
                      drag_box.ori_x, adjudge.left);
         crop_box.y = adjudge.top;
      }
      img_ori_x = outside_check(crop_box.x  - offset_img.x, 0, 
                   img_obj.width  - drag_box.min_w);
      img_ori_y = outside_check(crop_box.y  - offset_img.y, 0, 
                   img_obj.height - drag_box.min_h);
      $(".exec_crop_region").offset({
         "left" : crop_box.x, 
         "top"  : crop_box.y
      });
      $(".exec_crop_region").css({
         "width"  : crop_box.w,
         "height" : crop_box.h
      });
      $("#crop_img").offset({
         "left" : crop_box.x,
         "top"  : crop_box.y
      });
      $("#crop_img").css({
         "background-position" : "-"+(img_ori_x)+"px -"+(img_ori_y)+"px",
         "width": crop_box.w,
         "height":crop_box.h
      });
      adjudge.m_x = e.pageX;
      adjudge.m_y = e.pageY;
  }
}).mouseup(function(e){
     if(adjudge.ES){
        adjudge.ES = false;
     }else if(adjudge.EN){
        adjudge.EN = false;
     }else if(adjudge.WN){
        adjudge.WN = false;
     }else if(adjudge.WS){
        adjudge.WS = false;
     }
     drag_box.h = $(".exec_crop_region").height();
     drag_box.w = $(".exec_crop_region").width();
     adjudge.e_mousedown = null;
     e.stopPropagation();
});

這個部分主要都會分成四個類似的小功能做,分別是右上、右下、左上、左下等四個

方塊,來調整當下能夠延展的大小!

當您點下某個方塊時(以左上的為例),此時,您要得到當下crop image現在的offset的position,

再來是計算adjudge.r_width, adjust.r_height (還能夠延展的長與寬)

adjust.r_width = $(".exec_crop_region").width()  + (d_box_offset.left - drag_box.ori_x);

圖示如下:



d_box_offset為當下的位移 減去 drag_box一開始的位移就會得到左向箭頭的長度,再加上

目前crop_image的長度,即表示最大能夠位移到的臨界值。若左拉過來之目前的長度等於

此臨界值就無法繼續拉大了!高度以此類推。

再來是adjudge.left = d_box_offset.left+ (drag_box.w - drag_box.min_w);

一般來說width與height若是增長都是向右方及下方增長,等於左上角的原點是固定的!

因此假如我們現在要往左上增長,除了增長寬及高,還必須位移left及top才可以達到效果

以這段code為例:先計算出adjudge.top的最高位置

adjudge.top  = d_box_offset.top + (drag_box.h - drag_box.min_h);

若當下image crop在的位置是(579, 178),那麼往上拉會由下面的code計算出新的top

crop_box.y = outside_check(d_box_offset.top  + delta.y, drag_box.ori_y, adjudge.top);

d_box_offset.top  + delta.y是一直遞減,遞減到最小值drag_box.ori_y(image cop的初始top : 20)

此時若要往回拉,拉回原本的大小, 即d_box_offset.top + (drag_box.h - drag_box.min_h);

20 +  283(drag_box目前高度) - 125 = 178,此時往回拉即可以回到原來的位置,大致的概

念是這樣,寬度的部分以此類推。最簡單的是右下,往右邊及下面拉,基本上top, left是不

需要移動的!


三、進行image crop的處理

再來就是進行crop的處理,在這邊必須傳給Server端幾個參數,才可以進行crop,分別是:

var x_axis_pos = parseInt($(".exec_crop_region").css("left")) - adjust_ini_l;
var y_axis_pos = parseInt($(".exec_crop_region").css("top")) - adjust_ini_t;
var crop_width = $(".exec_crop_region").width();
var crop_height= $(".exec_crop_region").height();

寬度及高度就不用說囉!重點是$(".exec_crop_region").css("left"),表示取得當下

image_crop相對於其parent的位移,要減掉adjust_ini_l是因為crop image與圖片一開始均padding

內縮20px,因此exec_crop_region初始值為(20px, 20px)

相關的JavaScrip code如下:
$("#exec_crop").click(function() {
    $(this).attr("disabled", true);
    var x_axis_pos = parseInt(
        $(".exec_crop_region").css("left")) - adjust_ini_l;
    var y_axis_pos = parseInt(
        $(".exec_crop_region").css("top")) - adjust_ini_t;
    var crop_width = $(".exec_crop_region").width();
    var crop_height= $(".exec_crop_region").height();
    var post_data = "&width="+crop_width+"&height="+
        crop_height+"&x_pos="+x_axis_pos+"&y_pos="+y_axis_pos;
    $.ajax({
        type: 'POST',
        url: 'do_img_crop.php?r='+Math.random(),
        data: post_data,  
        dataType: 'text',
        beforeSend: function(){
           $("#process_img_crop").show();
        },
        complete: function(){
           $("#process_img_crop").hide();
        },
        success: function(text){
           if(text == "true"){
              $("#output_result").html("<div>(x, y) => ("+x_axis_pos+", "+y_axis_pos+"), w : "+crop_width+"px, h :"+crop_height+"px</div><img src='crop_Hydrangeas.jpg' />");
} else{
              $("#output_result").html("Image Crop Error!");
           }
           reset_setting();
        }
     }); 
     console.log(x_axis_pos+","+y_axis_pos+";width:"+
                 crop_width+";"+crop_height);
  });
  
  $("#clear_result").click(function(){
     $("#exec_crop").attr("disabled", false);
     $("#output_result").empty();
  });
  
  function reset_setting(){
    $(".crop_block_background").hide();
    $("#reg_pos").text("");
    $("#crop_img").css({
       "background-position" : "0px 0px",
       "width"  : drag_box.min_w,
       "height" : drag_box.min_h
    });
    $(".exec_crop_region").css({
       "width"  : drag_box.min_w,
       "height" : drag_box.min_h
    });
  }


再來是PHP code的部分:

<?php
   $img_name = "Hydrangeas.jpg";
   $crop_width = $_POST["width"];
   $crop_height = $_POST["height"];
   $crop_x = $_POST["x_pos"];
   $crop_y = $_POST["y_pos"];
   // Resample
   $image_crop = imagecreatetruecolor($crop_width, $crop_height);
   $image      = imagecreatefromjpeg($img_name);
   imagecopyresampled($image_crop, $image, 0, 0, $crop_x, $crop_y, 
   $crop_width, $crop_height, $crop_width, $crop_height);
   // Output
   if(imagejpeg($image_crop, "crop_".$img_name, 100)){
     echo "true";
   } else{
     echo "false";
   }
   // Free up memory
   imagedestroy($image);
   imagedestroy($image_crop);
?>

PHP crop image的sample可以在官網找到,觀念也講得很清楚!

最後進行image crop:

選定image crop範圍

image crop後直接呈現在下面

在這邊要提醒的是,圖片名稱是寫死的(因此每次切割後的圖片會一直覆蓋掉舊的圖片),

因此您可以自行製作上傳圖片的功能,但要注意的是Client端假設圖片呈現是512x384,

那麼Server端也需要有相同大小的圖片,以便做切割之用!

最後,這篇文章有點長及繁瑣,若有問題的地方請不吝指正!

留言