2012年5月6日 星期日

ASP.NET MVC 3 + jQuery imgAreaSelect + fancyBox


在今年一月的時候有曾經寫過一篇「圖片裁剪大頭貼功能 - ASP.NET MVC + jQuery + imgAreaSelect」,

這是因為工作上的需求而延伸出來的,工作上的專案是使用 ASP.NET WebForm,而關於這個主題則是 MVC 與 WebForm 都有實作,

當初的實作在之後的專案上也做了一些修正,讓使用情境可以更為便利以及合理,所以也將當初的實作加以修正,

就如本篇文章的主題,這一次的實作有加入使用 fancyBox,使用 fancyBox 是相當簡單的,對於使用者來說是會更加的方便,

至少不用在上傳、裁剪、主頁等這幾個頁面一直跳來跳去的,

這一篇文章會著重於修改以及加強功能的說明,所以就不會再從頭到尾的把製作過程鉅細靡遺的詳述一次,

如果對於本篇文章有完全不懂的地方,會比較建議依序看過以下的幾篇文章,並且把範例程式抓回去看過後再回過頭來看這一篇,

  1. 圖片裁剪大頭貼功能 - ASP.NET MVC + jQuery + imgAreaSelect
  2. 圖片裁剪大頭貼功能 - ASP.NET WebForm + jQuery + imgAreaSelect
  3. 圖片裁剪大頭貼功能 - ASP.NET (MVC, WebForm) + jQuery + imgAreaSelect 原始檔

 



回顧並說明前一版本的問題

我們先來看看之前實作的版本,看看有什麼問題,

image

 

上傳圖片

之前所做的上傳圖片,雖然基本的功能都有,也有去限制上傳檔案的大小,但錯就錯在我完全忘記系統本身 4096 KB 的安全性限制,

一般來說如果有需要修改上傳的大小限制,可以修改系統本身的設定,但不一定系統中的每個系統都需要可以上傳超過 4096 KB,

所以比較好的方法是在系統中的 Web.Config 中去增加條件,先看看沒有增加設定前上傳超過 4096KB 的錯誤畫面,

image

我們可以在 Web.Config 中增加以下的設定,就可以把上傳檔案的大小限制做個修改,

image

<httpRuntime maxRequestLength="10485760" appRequestQueueLimit="100" useFullyQualifiedRedirectUrl="true" executionTimeout="120" />

在修改過設定後,上傳超過 10MB 的檔案就可以顯示正確的錯誤訊息,而不會有系統錯誤黃頁出現,

image

可以正確的上傳超過 4096KB 的檔案

image

 

裁剪圖片

如果上傳的圖片尺寸不是很大的時候是看不出來裁剪圖片有什麼異狀,

但如果上傳了一張尺寸很大的圖片,就會知道有什麼不對勁了……

image

雖然說不會影響裁剪功能的操作,但是給使用者用這種操作介面,絕大部分的使用者一定會覺得這是個 BUG !

裁剪的操作介面是可以再來做個修正,

image

在完成圖片的裁剪後,假如我們想再對已經裁剪過的圖片再重新進行裁剪,此時我們在列表頁上點選「Crop」的連結進入裁剪頁面,

有些使用者會覺得奇怪,為什麼原始圖片上片的選擇裁剪區塊會是在圖片的左上角,而且跟我們之前所裁剪的內容並不一致,

比較好的作法,應該是進入一個已經有操作過裁剪的圖片資料時,原始圖片的選擇範圍必須要是之前所選擇的區域。

image

 

 

修正方案

其實上面所提出的問題,第一項「上傳檔案大小限制」,是已經有得到修正與解決外,

第二個「裁剪圖片」的相關問題,這個部分是比較棘手,

先說重現裁剪位置的問題,其實這是可以解決的,因為選取裁剪位置的四個數字是可以存起來的,

如果有儲存這四個方位點的數字,在重新裁剪圖片時,進入裁剪功能頁面就可以先看到之前裁剪所選擇的位置,

這完全是上一個版本沒有想到的。

再來就是上傳圖片後或是重新裁剪的圖片顯示大小問題,

也許有人會想說,既然圖片太大一張了,那可以去修改圖片的顯示屬性呀,這樣就可以讓圖片不會那麼大一張,

我必須說,這方法行不通,因為你顯示圖片雖然是縮小了,但是選擇裁剪範圍的四個位置的數字,會是縮小後的範圍位置,

這在後端處理圖片裁剪時,還是拿原始的圖片來做裁剪,以致於產生裁剪圖片就會裁錯位置,

依照縮小後的圖片進行圖片裁剪位置的選擇,選擇範圍的這四個方位點數字會是用縮小後的圖片的位置,並不是原始圖片上的位置。

 

在多次的實作後,得到一個比較好的解決方案,就是我們可以先預設一個圖片的尺寸大小,

凡是上傳的圖片如果尺寸大小超過預設值,那麼這張上傳的圖片就必須做縮圖處理,

如此一來在上傳後圖片的操作裁剪頁面就可以得到解決,而且也可以做到重現之前裁剪選擇位置的功能了。

 


ImageCrop Advance

做程式解說前,先看看已經完成功能與畫面的呈現,

image

 

上傳圖片

使用 fancyBox 來做上傳功能的呈現

image

上傳一張圖片尺寸大小超過預設值 1280 * 720 的圖片

image

於確定儲存後就會做縮圖處理,將圖片尺寸縮為預設值的大小,

image

 

裁剪圖片

經過圖片上傳、縮圖處理後就可以進行圖片裁剪的操作,

一張新上傳或是未曾做過圖片裁剪的圖片資料,一開始的圖片裁剪位置會是在左上角的去設位置上,

image

選定裁剪範圍並完成裁剪

image

回到列表頁可以看到列表上所顯示的裁剪位置範圍的方位點數值,

image

點選「Crop」對已做過裁剪的圖片再進行裁剪圖片的操作,

進入圖片裁剪的操作頁面,可以看到有重現之前所選擇的裁剪範圍,

image

重新選擇裁剪範圍後並完成裁剪的儲存,

image

image

 

另外點選 OriginalImage 的圖片則增加了使用 fancyBox 來顯示原來上傳或是上傳並縮圖過的圖片,

image

 


程式說明

主要的網站專案架構仍然不變,

image

增加了 fancyBox 套件的使用

image

fancyBox

http://fancyapps.com/fancybox/

使用的是 2.0.6 版

image

 

Table Schema

與之前的「UploadImage」有多了四個欄位,分別是「SelectionX1, SelectionX2, SelectionY1, SelectionY2」,

這四個新的欄位是用來儲存裁剪範圍的四個方位數值,

image

-- ----------------------------
-- Table structure for [dbo].[UploadImage2]
-- ----------------------------
CREATE TABLE [dbo].[UploadImage2] (
[ID] uniqueidentifier NOT NULL ,
[OriginalImage] nvarchar(50) NOT NULL ,
[CropImage] nvarchar(50) NULL ,
[SelectionX1] int NULL ,
[SelectionX2] int NULL ,
[SelectionY1] int NULL ,
[SelectionY2] int NULL ,
[CreateDate] datetime NOT NULL DEFAULT (getdate()) ,
[UpdateDate] datetime NOT NULL DEFAULT (getdate()) 
)
GO
-- ----------------------------
-- Primary Key structure for table [dbo].[UploadImage2]
-- ----------------------------
ALTER TABLE [dbo].[UploadImage2] ADD PRIMARY KEY ([ID])
GO
 

CropImageUtility

在 CropImageUtility 這邊有做了一些的修正,雖然還是很簡陋,但至少可以做一些彈性的設定,

可以調整檔案大小、圖片尺寸大小以及裁剪的範圍大小等設定,

欄位、屬性、建構式的部份:

public string UploadPath
{
    get;
    set;
}
public string OriginalPath
{
    get;
    set;
}
public string CropPath
{
    get;
    set;
}
private int _MaxWidth = 1024;
public int MaxWidth
{
    get
    {
        return _MaxWidth;
    }
    set 
    {
        _MaxWidth = value;
    }
}
private int _MaxHeight = 576;
public int MaxHeight
{
    get { return _MaxHeight; }
    set { _MaxHeight = value; }
}
private int _CropWidth = 100;
public int CropWidth
{
    get { return _CropWidth; }
    set { _CropWidth = value; }
}
private int _CropHeight = 100;
public int CropHeight
{
    get { return _CropHeight; }
    set { _CropHeight = value; }
}
private int _MaxRequestLength = 10485760;
public int MaxRequestLength
{
    get
    {
        return _MaxRequestLength;
    }
    set
    {
        _MaxRequestLength = value;
    }
}
public CropImageUtility(string uploadPath, string originalPath)
{
    this.UploadPath = uploadPath;
    this.OriginalPath = originalPath;
}
public CropImageUtility(string uploadPath, string originalPath, string cropPath = "")
    :this(uploadPath, originalPath)
{
    this.CropPath = cropPath;
}

 

在上傳圖片這邊多做了容量的檢查以及錯誤訊息顯示的修正,會依據設定的檔案大小來做顯示的調整,

//容量超過指定的上限, 預設 10M (10240KB)
string limitLength = string.Empty;
if (this.MaxRequestLength >= (1024 * 1024))
{
    limitLength = string.Concat((this.MaxRequestLength / (1024 * 1024)).ToString(), " Mb");
}
else if ((this.MaxRequestLength / (1024 * 1024)) < 1)
{
    limitLength = string.Concat((this.MaxRequestLength / 1024).ToString(), " Kb");
}
jo.Add("result", "error");
jo.Add("msg", string.Format("上傳檔案不可超過 {0}!請重新選擇檔案!", limitLength));
return jo;

 

圖片上傳完成後,如果確定儲存圖片,則會再做圖片尺寸大小的判斷,如果超過設定值就會做縮圖的處理,

/// <summary>
/// Makes the thumbnail image.
/// </summary>
/// <param name="fileName">Name of the file.</param>
/// <returns></returns>
private string MakeThumbnailImage(string fileName)
{
    //如果上傳圖片寬度大於 maxWidth,確定儲存上傳圖片就要做縮圖動作
    string tempFileName = fileName;
    int uploadImageWidth = 0;
    int uploadImageHeight = 0;
    using (Bitmap bitmap = new Bitmap(string.Format(@"{0}\{1}", this.UploadPath, tempFileName)))
    {
        uploadImageWidth = bitmap.Width;
        uploadImageHeight = bitmap.Height;
        if (uploadImageWidth > this.MaxWidth || uploadImageHeight > this.MaxHeight)
        {
            // 計算維持比例的縮圖大小
            int[] thumbnailScale = this.GetThumbnailImageScale(this.MaxWidth, this.MaxHeight, uploadImageWidth, uploadImageHeight);
                
            // 產生縮圖
            System.Drawing.Image imgThumbnail = System.Drawing.Image.FromFile(string.Format(@"{0}\{1}", this.UploadPath, tempFileName), false);
            System.Drawing.Image hb = new System.Drawing.Bitmap(thumbnailScale[0], thumbnailScale[1]);
            System.Drawing.Graphics graphics = System.Drawing.Graphics.FromImage(hb);
            graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
            graphics.DrawImage(imgThumbnail, new Rectangle(0, 0, thumbnailScale[0], thumbnailScale[1]), 0, 0, imgThumbnail.Width, imgThumbnail.Height, GraphicsUnit.Pixel);
            //存檔
            fileName = String.Concat(MiscUtility.makeGUID().Replace("-", string.Empty).Substring(0, 20), ".jpg");
            hb.Save(string.Format(@"{0}\{1}", this.UploadPath.Replace("~", ""), fileName));
            graphics.Dispose();
            hb.Dispose();
            imgThumbnail.Dispose();
        }
    }
    return fileName;
}
/// <summary>
/// 計算維持比例的縮圖大小
/// </summary>
/// <param name="maxWidth"></param>
/// <param name="maxHeight"></param>
/// <param name="oldWidth"></param>
/// <param name="oldHeight"></param>
/// <returns></returns>
private int[] GetThumbnailImageScale(int maxWidth, int maxHeight, int oldWidth, int oldHeight)
{
    int[] result = new int[] { 0, 0 };
    if (oldWidth < maxWidth && oldHeight < maxHeight)
    {
        result = new int[] { oldWidth, oldHeight };
    }
    else
    {
        float widthDividend, heightDividend, commonDividend;
        widthDividend = (float)oldWidth / (float)maxWidth;
        heightDividend = (float)oldHeight / (float)maxHeight;
        commonDividend = (heightDividend > widthDividend) ? heightDividend : widthDividend;
        result[0] = (int)(oldWidth / commonDividend);
        result[1] = (int)(oldHeight / commonDividend);
    }
    return result;
}

 

HomeController

HomeController 這邊的修改就比較大,

在一開始增加幾個欄位來放置檔案大小、圖片尺寸大小等設定值,

這邊我設定上傳的圖片不可超過 10MB,圖片最大寬度、高度分別為 1280px, 720px,裁剪範圍的寬度、高度為 150px,

private UploadImage2Service service = new UploadImage2Service();
#region -- Fields & Properties --
private readonly int MaxRequestLength = 10485760;
private readonly int MaxWidth = 1280;
private readonly int MaxHeight = 720;
private readonly int CropWidth = 150;
private readonly int CropHeight = 150;
private string UploadFolder
{
    get
    {
        return @"FileUpload/Temp";
    }
}
private string OriginalFolder
{
    get
    {
        return @"FileUpload/Original";
    }
}
private string CropFolder
{
    get
    {
        return @"FileUpload/Crop";
    }
}
private string UploadPath
{
    get
    {
        return Server.MapPath("~/" + this.UploadFolder);
    }
}
private string OriginalPath
{
    get
    {
        return Server.MapPath("~/" + this.OriginalFolder);
    }
}
private string CropPath
{
    get
    {
        return Server.MapPath("~/" + this.CropFolder);
    }
}
#endregion

 

在圖片檔案上傳完成後,確定儲存圖片,在這邊的 Action 方法也做了一點小修正,

增加圖片儲存時所需要的設定值以及 entity 存入資料庫的欄位值的儲存,

[HttpPost]
public JsonResult Save(string fileName)
{
    Dictionary<string, string> jo = new Dictionary<string, string>();
    CropImageUtility cropUtils = new CropImageUtility(this.UploadPath, this.OriginalPath, this.CropPath);
    cropUtils.MaxWidth = this.MaxWidth;
    cropUtils.MaxHeight = this.MaxHeight;
    
    Dictionary<string, string> result = cropUtils.SaveUploadImageToOriginalFolder(fileName);
    if (!result["result"].Equals("Success", StringComparison.OrdinalIgnoreCase))
    {
        jo.Add("result", result["result"]);
        jo.Add("msg", result["msg"]);
    }
    else
    {
        try
        {
            if (result["result"].Equals("Success", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(result["msg"]))
            {
                fileName = result["msg"].ToString();
            }
            UploadImage2 instance = new UploadImage2();
            instance.ID = Guid.NewGuid();
            instance.OriginalImage = fileName;
            instance.SelectionX1 = 0;
            instance.SelectionX2 = this.CropWidth;
            instance.SelectionY1 = 0;
            instance.SelectionY2 = this.CropHeight;
            instance.CreateDate = DateTime.Now;
            instance.UpdateDate = instance.CreateDate;
            service.Add(instance);
            jo.Add("result", "Success");
            jo.Add("msg", string.Format(@"/{0}/{1}", OriginalFolder, fileName));
            jo.Add("id", instance.ID.ToString());
        }
        catch (Exception ex)
        {
            jo.Add("result", "Exception");
            jo.Add("msg", ex.Message);
        }
    }
    return Json(jo);
}

 

最大的修正就是 Crop 與 CropImage 的 Action 方法,

因應裁剪範圍的方位數值的儲存與傳到前端頁面上,

public ActionResult Crop(string id)
{
    ViewData["ErrorMessage"] = "";
    ViewData["UploadImage_ID"] = "";
    ViewData["OriginalImage"] = "";
    ViewData["CropImape"] = "";
    ViewBag.CropWidth = this.CropWidth;
    ViewBag.CropHeight = this.CropHeight;
    if (string.IsNullOrWhiteSpace(id))
    {
        ViewData["ErrorMessage"] = "沒有輸入資料編號";
        return View();
    }
    Guid imageID;
    if (!Guid.TryParse(id, out imageID))
    {
        ViewData["ErrorMessage"] = "資料編號錯誤";
        return View();
    }
    var instance = service.FindOne(imageID);
    if (instance == null)
    {
        ViewData["ErrorMessage"] = "資料不存在";
        return View();
    }
    ViewData["UploadImage_ID"] = imageID;
    ViewData["OriginalImage"] = string.Format(@"/{0}/{1}", OriginalFolder, instance.OriginalImage);
    if (!string.IsNullOrWhiteSpace(instance.CropImage))
    {
        ViewData["CropImape"] = string.Format(@"/{0}/{1}", CropFolder, instance.CropImage);
    }
    bool checkSelection = instance.SelectionX1.Equals(0)
        && instance.SelectionX2.Equals(0)
        && instance.SelectionY1.Equals(0)
        && instance.SelectionY2.Equals(0);
    if (checkSelection)
    {
        ViewData["SelectionX1"] = 0;
        ViewData["SelectionX2"] = this.CropWidth;
        ViewData["SelectionY1"] = 0;
        ViewData["SelectionY2"] = this.CropHeight;
    }
    else
    {
        ViewData["SelectionX1"] = instance.SelectionX1;
        ViewData["SelectionX2"] = instance.SelectionX2;
        ViewData["SelectionY1"] = instance.SelectionY1;
        ViewData["SelectionY2"] = instance.SelectionY2;
    }
    return View();
}
[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);
    cropUtils.MaxWidth = this.MaxWidth;
    cropUtils.MaxHeight = this.MaxHeight;
    cropUtils.MaxRequestLength = this.MaxRequestLength;
    cropUtils.CropWidth = this.CropWidth;
    cropUtils.CropHeight = this.CropHeight;
    Dictionary<string, string> processResult = cropUtils.ProcessImageCrop
    (
        instance,
        new int[] { x1.Value, x2.Value, y1.Value, y2.Value }
    );
    if (processResult["result"].Equals("Success", StringComparison.OrdinalIgnoreCase))
    {
        //裁剪圖片檔名儲存到資料庫
        instance.CropImage = processResult["CropImage"];
        instance.SelectionX1 = x1.Value;
        instance.SelectionX2 = x2.Value;
        instance.SelectionY1 = y1.Value;
        instance.SelectionY2 = y2.Value;
        service.Update(instance);
        //如果有之前的裁剪圖片,則刪除
        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);
}

 

而有關 View 頁面的程式部分,就請大家下載程式再仔細的看,會比較清楚,

這一次的修改後的功能完整程度就比之前的那一個版本更加完整,操作上也比較方便,流程也更為順暢,

程式功能的設計本來就是要經過一次又一次的修正後才會更加完備,

當然擃所提供的程式還是有很多不是完善的地方,就留待日後再做修正與補強。

 

接下來就放上一段影片,這是把整個操作流程給錄製下來,畢竟靜態的圖片與文字說明是不如實際的操作過程影片來得清楚,

如果覺得影片的說明相當不清不楚,請各位見諒,第一次錄製系統操作的說明影片,請各位包含。

 

 

程式下載:
http://dl.dropbox.com/u/26764200/Lab/20120506_ImageCropAdvance.zip

 

以上

沒有留言:

張貼留言

提醒

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