2012年1月19日 星期四

圖片裁剪大頭貼功能 - ASP.NET MVC + jQuery + imgAreaSelect


最近專案上客戶提出一個需求,希望做個類似facebook大頭貼的功能,而且操作功能要很簡單而且不要太繁瑣,

這一類的功能其實也真的不是很難,簡單的說明一下需求內容:

  • 上傳圖片,上傳後判斷是否要使用這張圖片
  • 儲存上傳圖片後,進行裁剪圖片,裁剪圖片不希望只能裁剪固定大小,可以有等比例裁剪的功能(大頭貼有定寬高,在原上傳圖片可以選取任一區域,於裁剪圖片後做等比例的縮圖)
  • 裁剪圖片時,希望可以即時預覽等比例縮圖的結果
  • 可以隨時對原上傳圖片做重新裁剪(原裁剪圖片可以不保留)

大部分的開發人員在網路上找這一類的jQuery功能套件時,多會採用「jCrop」,這是一個不錯且相當好用的jQuery Plugin,

不過我卻是採用另一套也是不錯的jQuery Plugin「imgAreaSelect」,使用方式與 jCrop 相差不大,

另外原專案是使用ASP.NE WebForm開發,而本篇文章內容將會是以ASP.NET MVC的架構下來做說明,

而在下一篇文章也會說明如何在ASP.NET WebForm中開發這個功能,讓大家可以看看兩個架構下的開發會有什麼樣的差異。



jQuery Plugin:imgAreaSelect

網址:http://odyniec.net/projects/imgareaselect/

範例:http://odyniec.net/projects/imgareaselect/examples.html

文件:http://odyniec.net/projects/imgareaselect/usage.html

 

前端裁剪圖片時,會記錄一組矩形裁剪區域的座標,而我們可以將這一組座標值傳回到後端,讓後端程式依據這一組區域座標值對圖片進行裁剪的動作。

有關ASP.NET MVC的圖片上傳就不再說明,本篇將會著重說明前端的imageAreaSelect使用方式以及後端裁剪圖片的處理。

 

系統操作環境:

  • ASP.NET MVC 3 ( Razor )
  • .NET Framework 4.0 ( C# )
  • jQuery 1.7.1
  • imgAreaSelect 0.9.8

目錄架構:

image


前端程式的設定與使用

圖片上傳、存檔處理之後,要做的事情就是讓使用者可以在前端頁面上進行裁剪圖片的區域選擇,

而接下來就來說明有關前端使用imgAreaSelect的方式。

 

在Crop的頁面上放置了以下幾個Element:

<div id="Panel1" style="height: 100px; padding: 5px 0px 5px 0px;">
    <img id="Image3" style="display: none;" />
</div>
<hr />
<input type="button" id="ButtonCrop" value="Save the Crop Image" />
<div align="center">
    <img id="Image1" style="float: left; margin-right: 10px;" alt="Create Thumbnail" />
    <div style="float: left; position: relative; overflow: hidden; width: 100px; height: 100px;">
        <img id="Image2" style="position: relative;" alt="Thumbnail Preview" />
    </div>
</div>
<input type="hidden" id="x1" name="x1" value="" runat="server" />
<input type="hidden" id="y1" name="y1" value="" runat="server" />
<input type="Hidden" id="x2" name="x2" value="" runat="server" />
<input type="hidden" id="y2" name="y2" value="" runat="server" />

Panel1的Div Element是要用來放置存檔後的裁剪圖片,如果是一個新上傳的圖片不會顯示任何物件,反之如果有做過裁剪並存檔,就會顯示既有的裁剪圖片,

下圖後為新上傳的圖片且尚未進行裁剪處理;下圖前為有做過裁剪處理並且有存檔,顯示之前裁剪圖片內容。

image

Image1的Image Element為要做裁剪圖片的原始圖片,

Image2的Image Element為即時顯示使用者選擇裁剪區域的預覽圖片。

image

而四個Hidden Field(x1, x2, y1, y2)則是用來儲存裁剪圖片區域的座標值。

 

接下來直接看前端jQuery的程式,如何設定imgAreaSelect:

$(document).ready(function ()
{
    $('img#Image1').imgAreaSelect(
    {
        handles: true,
        aspectRatio: '1:1',
        x1: 0, y1: 0, x2: 100, y2: 100,
        onSelectChange: preview
    });
 
    $('#ButtonCrop').click(function ()
    {
        SaveCropEventHandler();
    });
});

Image1的image element是要操作圖片裁剪的目標物件,所以對Image1進行imgAreaSelect設定,

handles

這是設定選取裁剪區域的虛線框是否出現各角落與各邊框中間的小方框,讓使用者可以在使用滑鼠易於拖拉區塊。

handles: true

image

handles: false

image

aspectRatio

這個參數可以用來設定選取區域的選取框拖拉的比例,

「aspectRatio: ‘1:1’」在拖拉選取框時只能選取寬高比為一比一的矩形區域,也就是只能選取正方形。

image

「aspectRatio: ‘4:3’」拖拉選取框時只能選取寬高比為四比三的取形區域。

image

如果不限定選取區域寬高比的話,可以直接設定為「aspectRatio: ‘1’」」或是不用設定aspectRatio也可以。

image

「x1: 0, y1: 0, x2: 100, y2: 100」
這個設定就是可以設定一個預設選取的區域,
「x1: 0, y1: 0, x2: 100, y2: 100」就是預設選去一個 Width=100px 以及 Height=100px 的區域,
image
設定為「x1: 50, y1: 50, x2: 150, y2: 150」

image

 

onSelectChange: preview

這一個參數設定是可以讓使用者在選取裁剪區域時能夠即時預覽裁剪後的圖像,

下圖是沒有重新拖拉區域的大小所看到的預覽,

image

拉大裁剪的選取區域,在Image2的element中可以即時預覽縮圖後的圖像

image

而處理即時顯示預覽縮圖的function preview內容如下:

function preview(img, selection)
{
    var scaleX = 100 / selection.width;
    var scaleY = 100 / selection.height;
 
    var img = new Image();
    img.src = $('#Image1').attr('src');
    var pic_real_width = img.width;
    var pic_real_height = img.height;
 
    $('#Image1 + div > img').css({
        width: Math.round(scaleX * pic_real_width) + 'px',
        height: Math.round(scaleY * pic_real_height) + 'px',
        marginLeft: '-' + Math.round(scaleX * selection.x1) + 'px',
        marginTop: '-' + Math.round(scaleY * selection.y1) + 'px'
    });
    $('input[name="x1"]').val(selection.x1);
    $('input[name="y1"]').val(selection.y1);
    $('input[name="x2"]').val(selection.x2);
    $('input[name="y2"]').val(selection.y2);
}

在選取裁剪的區域並產生預覽圖的同時,也會把選取區域的座標值給放到四個Hidden Field中,

在等一下於後端儲存裁剪圖片時就可以使用這一組座標值來進行後續的處理。

 

以上是使用imgAreaSelect的使用設定方式,選取好要裁剪的區域之後就是把選取區域的座標值給傳送到後端,

後端就會依據前端所送來的座標值進行裁剪圖片、縮圖以及存檔的動作,

以下是前端的儲存按鍵的前端事件處理程式內容:

function SaveCropEventHandler()
{
    var x1 = $('input[name="x1"]').val();
    var x2 = $('input[name="x2"]').val();
    var y1 = $('input[name="y1"]').val();
    var y2 = $('input[name="y2"]').val();
 
    if (x1.length == 0 && x2.length == 0 && y1.length == 0 && y2.length == 0)
    {
        alert('請選擇裁剪區域');
        return false;
    }
    else
    {
        $.ajax({
            type: 'post',
            url: '@Url.Action("CropImage", "Home")',
            data: { id: $('#UploadImage_ID').val(), x1: x1, x2: x2, y1: y1, y2: y2 },
            dataType: 'json',
            async: false,
            cache: false,
            success: function (result)
            {
                if (result)
                {
                    if (result.result != 'OK')
                    {
                        alert(result.msg);
                        window.location.href = '@Url.Action("Crop", "Home")' + '?id=' + $('#UploadImage_ID').val();
                    }
                    else
                    {
                        alert('裁剪完成');
                        $('img#Image3').attr('src', result.CropImage);
                        $('img#Image3').show();
                    }
                    return false;
                }
            }
        });
    }
}

當後端的裁剪圖片、縮圖、存檔的處理都完成之後,後端會以JSON的方式把裁剪好並以儲存完成的檔案路徑、名稱傳送回前端,前端就可以馬上顯示裁剪完成的圖片。

image

 


後端:Controller的內容

這邊來看一看前端存檔的事件處理所對應的後端Controller的Action程式內容:

[HttpPost]
public JsonResult CropImage(string id, int? x1, int? x2, int? y1, int? y2)
{
    Dictionary<string, string> result = new Dictionary<string, string>();
    if (string.IsNullOrWhiteSpace(id))
    {
        result.Add("result", "error");
        result.Add("msg", "沒有輸入資料編號");
        return Json(result);
    }
    if (!x1.HasValue || !x2.HasValue || !y1.HasValue || !y2.HasValue)
    {
        result.Add("result", "error");
        result.Add("msg", "裁剪圖片區域值有缺少");
        return Json(result);
    }
    Guid imageID;
    if (!Guid.TryParse(id, out imageID))
    {
        result.Add("result", "error");
        result.Add("msg", "資料編號錯誤");
        return Json(result);
    }
    var instance = service.FindOne(imageID);
    if (instance == null)
    {
        result.Add("result", "error");
        result.Add("msg", "資料不存在");
        return Json(result);
    }
    CropImageUtility cropUtils = new CropImageUtility(this.UploadPath, this.OriginalPath, this.CropPath);
    Dictionary<string, string> processResult = cropUtils.ProcessImageCrop
    (
        instance,
        new int[] { x1.Value, x2.Value, y1.Value, y2.Value }
    );
    if (processResult["result"].Equals("Success", StringComparison.OrdinalIgnoreCase))
    {
        //裁剪圖片檔名儲存到資料庫
        service.Update(instance.ID, processResult["CropImage"]);
        //如果有之前的裁剪圖片,則刪除
        if (!string.IsNullOrWhiteSpace(processResult["OldCropImage"]))
        {
            cropUtils.DeleteCropImage(processResult["OldCropImage"]);
        }
        result.Add("result", "OK");
        result.Add("msg", "");
        result.Add("OriginalImage", string.Format(@"/{0}/{1}", this.OriginalFolder, processResult["OriginalImage"]));
        result.Add("CropImage", string.Format(@"/{0}/{1}", this.CropFolder, processResult["CropImage"]));
    }
    else
    {
        result.Add("result", processResult["result"]);
        result.Add("msg", processResult["msg"]);
    }
    return Json(result);
}

上面的程式當中,有關 CropImageUtility類別以及此類別的ProcessImageCrop方法就是用來處理裁剪圖片、縮圖、存檔,

接下來會做個簡短的說明。

 


後端:圖片裁剪、縮圖以及存檔的處理

在C#中對於圖片的裁剪相關的類別與資料如下:

MSDN - Bitmap 類別

MSDN - Bitmap.Clone 方法 (Rectangle, PixelFormat)

MSDN - Rectangle 結構

MSDN - Rectangle 建構函式 (Int32, Int32, Int32, Int32)

上面的連結資料中比較關鍵的地方就在於 Rectangle建構函式:

   1: public Rectangle(
   2:     int x,
   3:     int y,
   4:     int width,
   5:     int height
   6: )

前面兩個值是裁剪區區域座標值的X與Y,裁減區域的左上方的那一個點的位置,如下圖所標示的地方,

image

後兩個參數值就是要裁剪區域的寬與高,因為我們傳入的座標值是裁剪區域的左上與右下兩個座標,

所以我們可以經由兩個座標值的相減求得裁剪區域的寬與高,

例如:

//取得裁剪的區域座標
int section_x1 = sectionValue[0];
int section_x2 = sectionValue[1];
int section_y1 = sectionValue[2];
int section_y2 = sectionValue[3];
 
//取得裁剪的圖片寬高
int width = section_x2 - section_x1;
int height = section_y2 - section_y1;

如此一來我們就可以知道如何在後端對圖片進行裁剪處理:

//讀取原圖片
System.Drawing.Image sourceImage = System.Drawing.Image.FromFile
(
    string.Format(@"{0}\{1}", this.OriginalPath, currentImage.OriginalImage)
);
 
//從原檔案取得裁剪圖片
System.Drawing.Image cropImage = this.CropImage(
    sourceImage,
    new Rectangle(section_x1, section_y1, width, height)
);
 
////////////////////////////////////////////////////////////////////////////
 
#region -- CropImage --
/// <summary>
/// Crops the image.
/// </summary>
/// <param name="img">The img.</param>
/// <param name="cropArea">The crop area.</param>
/// <returns></returns>
private System.Drawing.Image CropImage(System.Drawing.Image img, Rectangle cropArea)
{
    Bitmap bmpImage = new Bitmap(img);
    Bitmap bmpCrop = bmpImage.Clone(cropArea, bmpImage.PixelFormat);
    return bmpCrop as System.Drawing.Image;
}
 
#endregion

 

取得裁剪後的圖片,接下來就是要做等比例縮圖的處理,

MSDN – System.Drawing.Size 結構

MSDN - Size 建構函式 (Int32, Int32)

//將採剪下來的圖片做縮圖處理
Bitmap resizeImage = this.ResizeImage(cropImage, new Size(100, 100));
 

將得到的裁剪圖片去指定寬度與高度然後做等比例的縮圖處理,

#region -- ResizeImage --
/// <summary>
/// Resizes the image.
/// </summary>
/// <param name="imgToResize">The img to resize.</param>
/// <param name="size">The size.</param>
/// <returns></returns>
private Bitmap ResizeImage(System.Drawing.Image imgToResize, Size size)
{
    int sourceWidth = imgToResize.Width;
    int sourceHeight = imgToResize.Height;
 
    float nPercent = 0;
    float nPercentW = 0;
    float nPercentH = 0;
 
    nPercentW = ((float)size.Width / (float)sourceWidth);
    nPercentH = ((float)size.Height / (float)sourceHeight);
 
    if (nPercentH < nPercentW)
    {
        nPercent = nPercentH;
    }
    else
    {
        nPercent = nPercentW;
    }
 
    int destWidth = (int)(sourceWidth * nPercent);
    int destHeight = (int)(sourceHeight * nPercent);
 
    Bitmap bmp = new Bitmap(destWidth, destHeight);
    Graphics g = Graphics.FromImage((System.Drawing.Image)bmp);
    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
 
    g.DrawImage(imgToResize, 0, 0, destWidth, destHeight);
    g.Dispose();
 
    return bmp;
}
#endregion

而最後就勢將處理好的縮圖進行存檔的動作,因為可接受上傳的圖片格式有:jpg, jpeg, png, gif,

為了裁剪後的圖片格式能夠一致處理,所以都會存為JPG格式,而裁剪後的縮圖檔名會由系統產生,

//將縮圖處理完成的圖檔儲存為JPG格式
string fileName = String.Concat(MiscUtility.makeGUID().Replace("-", string.Empty).Substring(0, 20), ".jpg");
string savePath = string.Format(@"{0}\{1}", this.CropPath, fileName);
SaveJpeg(savePath, resizeImage, 100L);

存JPG圖檔的處理:

#region -- SaveJpeg --
/// <summary>
/// Saves the JPEG.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="img">The img.</param>
/// <param name="quality">The quality.</param>
private void SaveJpeg(string path, Bitmap img, long quality)
{
    // Encoder parameter for image quality
    EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
 
    // Jpeg image codec
    ImageCodecInfo jpegCodec = this.getEncoderInfo("image/jpeg");
 
    if (jpegCodec == null)
    {
        return;
    }
 
    EncoderParameters encoderParams = new EncoderParameters(1);
    encoderParams.Param[0] = qualityParam;
 
    img.Save(path, jpegCodec, encoderParams);
    img.Dispose();
}
 
/// <summary>
/// Gets the encoder info.
/// </summary>
/// <param name="mimeType">Type of the MIME.</param>
/// <returns></returns>
private ImageCodecInfo getEncoderInfo(string mimeType)
{
    // Get image codecs for all image formats
    ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
 
    // Find the correct image codec
    for (int i = 0; i < codecs.Length; i++)
    {
        if (codecs[i].MimeType == mimeType)
        {
            return codecs[i];
        }
    }
    return null;
}
#endregion

MSDN - EncoderParameter 建構函式 (Encoder, Int64)
「使用指定的 Encoder 物件和一個 64 位元整數,初始化 EncoderParameter 類別的新執行個體。 設定 ValueType 屬性為 ValueTypeLong (32 位元),並設定 NumberOfValues 屬性為 1。」

MSDN - Encoder 類別
「Encoder 物件會封裝全域唯一識別項 (GUID),這個識別項識別影像編碼器參數的分類。」

MSDN - ImageCodecInfo 類別
「ImageCodecInfo 類別提供必要的儲存成員和方法,以擷取已安裝之影像編碼器和解碼器 (稱為轉碼器) 的所有相關資訊。」

 

以上有關裁剪圖片(CropImage)、縮圖處理(ResizeImage)、圖片存檔(SaveJpeg)的方法是參考以下文章的內容:

Switch On The Code

C# Tutorial - Image Editing: Saving, Cropping, and Resizing

 

而CropImageUtility類別的ProcessImageCrop Method的完整程式內容如下:

#region -- ProcessImageCrop --
/// <summary>
/// Processes the image crop.
/// </summary>
public Dictionary<string, string> ProcessImageCrop(UploadImage currentImage, int[] sectionValue)
{
    Dictionary<string, string> result = new Dictionary<string, string>();
 
    try
    {
        //取得裁剪的區域座標
        int section_x1 = sectionValue[0];
        int section_x2 = sectionValue[1];
        int section_y1 = sectionValue[2];
        int section_y2 = sectionValue[3];
 
        //取得裁剪的圖片寬高
        int width = section_x2 - section_x1;
        int height = section_y2 - section_y1;
 
        //讀取原圖片
        System.Drawing.Image sourceImage = System.Drawing.Image.FromFile
        (
            string.Format(@"{0}\{1}", this.OriginalPath, currentImage.OriginalImage)
        );
 
        //從原檔案取得裁剪圖片
        System.Drawing.Image cropImage = this.CropImage(
            sourceImage,
            new Rectangle(section_x1, section_y1, width, height)
        );
 
        //將採剪下來的圖片做縮圖處理
        Bitmap resizeImage = this.ResizeImage(cropImage, new Size(100, 100));
 
        //將縮圖處理完成的圖檔儲存為JPG格式
        string fileName = String.Concat(MiscUtility.makeGUID().Replace("-", string.Empty).Substring(0, 20), ".jpg");
        string savePath = string.Format(@"{0}\{1}", this.CropPath, fileName);
        SaveJpeg(savePath, resizeImage, 100L);
 
        //釋放檔案資源
        resizeImage.Dispose();
        cropImage.Dispose();
        sourceImage.Dispose();
 
        //如果有之前的裁剪圖片,暫存既有的裁剪圖片檔名
        string oldCropImageFileName = string.Empty;
        if (!string.IsNullOrWhiteSpace(currentImage.CropImage))
        {
            oldCropImageFileName = currentImage.CropImage;
        }
 
        //JSON
        result.Add("result", "Success");
        result.Add("OriginalImage", currentImage.OriginalImage);
        result.Add("CropImage", fileName);
        result.Add("OldCropImage", oldCropImageFileName);
    }
    catch (Exception ex)
    {
        result.Add("result", "Exception");
        result.Add("msg", ex.Message);
    }
    return result;
}
 
#endregion

 



操作流程示意

Step 1:首頁,尚未上傳任何圖片

image

 

Step 2:上傳圖片頁面

image

上傳圖片,上傳的圖檔將會先暫放在Temp目錄中,
待確認存檔後才會把圖檔存到另一個目錄中,如果不存則會把檔案從Temp目錄中刪除。

image

存檔之後,顯示「Crop Image」Button,按下Buttoon後會移到 Crop頁。

image

 

Step 3:Crop頁

一個新上傳的圖片因為還沒有做過裁剪圖片,所以在下途中紅色框線的地方顯示任何內容,

image

選取裁剪區域並且在右方即時預覽

image

選取裁剪區域後按下「Save Crop Image」,後端完成裁剪圖片處理後就會顯示存檔後的裁剪圖檔

image

 

Step 4;系統首頁

顯示已上傳的圖片資料,如果有操作過裁剪圖片功能則會顯示裁剪圖檔內容。

image

刪除圖片資料

image

 


這一篇的內容是以ASP.NET MVC 3加上jQuery以及使用imgAreaSelect來達成上傳圖片和裁剪圖片的需求,

功能上都相當的陽春,因為只是為了要使用imgAreaSelect去實作裁剪大頭貼圖片的功能,

其實整個系統的操作介面上還可以做到更好更為流暢,

如果要做得更精緻一些的話,其實可以大量的採用AJAX效果並且使用jQuery UI或是LigntBox等套件來搭配使用,

讓整個系統功能的操作都在同一個頁面中完成,就不用在三個頁面去切換操作,不過這部份就等待日後有時間時再來加強,

而下一篇文章將會在ASP.NET WebForm的架構下來做出同樣的功能,最後再分享出這兩種架構的程式碼,讓大家可以參考。

 

參考連結:

imgAreaSelect
http://odyniec.net/projects/imgareaselect/

Switch On The Code - C# Tutorial - Image Editing: Saving, Cropping, and Resizing
http://www.switchonthecode.com/tutorials/csharp-tutorial-image-editing-saving-cropping-and-resizing

 

延伸閱讀:

Deep Liquid – Jcrop ( the jQuery Image Cropping Plugin )
http://deepliquid.com/content/Jcrop.html

Tallan’s Technology Blog - Using MVC3, Razor Helpers, and jCrop to upload and crop images.
http://blog.tallan.com/2011/02/04/using-mvc3-razor-helpers-and-jcrop-to-upload-and-crop-images/

Jerry 我的Coding之路 - C# 搭配jQuery Jcrop plugin 製作圖片縮圖
http://www.dotblogs.com.tw/lastsecret/archive/2010/10/12/18300.aspx

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。