需求
上周,領導給我分配了一個需求:服務器上的圖片文件非常大,每天要用掉兩個G的大小的空間,要做一個自動壓縮圖片的工具處理這些大圖片。領導的思路是這樣的:
1)打開一個圖片,看它的屬性里面象素是多少,大於1000就按比例縮小到1000。
2)再看它的品質屬性,比如我們標准是50,如果大於這個值再修改品質。
壓縮后的文件大小不能超過200k。
思路
因為服務器上的圖片文件名是加密處理過的,和圖片文件一起存在的還有其它附件,沒有后綴名,用肉眼根本看不出來是否是圖片文件。所以剛開始的時候,我的思路是先批量修改后綴名,再獲取圖片的像素,最后再進行壓縮。后來在做的過程中,發現不用處理后綴名,直接獲取圖片信息就能識別文件是否是圖片。
所以,最后的做法是:
1)遍歷文件夾下的圖片文件的時候,先根據圖片信息,把圖片文件提取到列表
2)然后再根據圖片的像素大小進行處理。像素在1000以內的直接修改圖片品質處理,像素大於1000的按尺寸大小壓縮圖片,然后再修改圖片品質處理。
(像素大於1000這種情況之所以有兩步是因為按尺寸大小進行壓縮后,圖片大小大於1M,不符合預期的要求,所以壓縮圖片后再修改圖片品質。這一步為了避免混淆,我把按尺寸大小壓縮圖片放到另一個文件夾處理,這個文件夾在處理好圖片后,會把壓縮圖片文件進行刪除,所以這個文件夾永遠是空的,不會占空間)
//做的時候一聽到是自動壓縮圖片,批量處理文件,以為很難,很深奧,真正動手后其實是辦法總比困難多。總有辦法實現的,只是時間問題。
代碼片段
1)因為文件的位置不固定,文件夾下面有圖片,也有文件夾,里面還有圖片。所以要遍歷子目錄。
/// <summary>
/// 遍歷文件
/// </summary>
/// <param name="di"></param>
public void ListFiles(DirectoryInfo di)
{
if (!di.Exists)
{
return;
}
if (di == null)
{
return;
}
//返回當前目錄的文件列表
FileInfo[] files = di.GetFiles();
for (int i = 0; i < files.Length; i++)
{
try
{
//判斷是否具有照片信息,報錯即不是照片文件
GetMetaData.GetExifByMe(files[i].FullName);
//把圖片文件添加到列表視圖
this.lvSourceFolderList.Items.Add(files[i].FullName);
//把圖片文件添加到圖片列表
imageList.Add(files[i].FullName);
}
catch (Exception)
{
//Logging.Error(System.IO.Path.GetFileName(files[i].FullName) + ",非圖片文件," + ex.Message);
continue;
}
}
this.lbInfomation.Text = "共" + this.lvSourceFolderList.Items.Count + "條數據";
//返回當前目錄的子目錄
DirectoryInfo[] dis = di.GetDirectories();
for (int j = 0; j < dis.Length; j++)
{
// Console.WriteLine("目錄:" + dis[j].FullName);
ListFiles(dis[j]);//對於子目錄,進行遞歸調用
}
}
2)判斷圖片是否具有照片信息,我用的是MetadataExtractor,直接在nuget里面添加安裝好,再添加一個GetExifByMe即可。這里在調用GetExifByMe的時候,不是圖片文件會報錯,報錯的我直接忽略,繼續continue。
//判斷是否具有照片信息,報錯即不是照片文件
GetMetaData.GetExifByMe(files[i].FullName);
#region 通過metadata-extractor獲取照片參數
//參考文獻
//官網: https://drewnoakes.com/code/exif/
//nuget 官網:https://www.nuget.org/
//nuget 使用: http://www.cnblogs.com/chsword/archive/2011/09/14/NuGet_Install_OperatePackage.html
//nuget MetadataExtractor: https://www.nuget.org/packages/MetadataExtractor/
/// <summary>通過MetadataExtractor獲取照片參數
/// </summary>
/// <param name="imgPath">照片絕對路徑</param>
/// <returns></returns>
public static Dictionary<string, string> GetExifByMe(string imgPath)
{
var rmd = ImageMetadataReader.ReadMetadata(imgPath);
var rt = new Dictionary<string, string>();
foreach (var rd in rmd)
{
foreach (var tag in rd.Tags)
{
var temp = EngToChs(tag.Name);
if (temp == "其他")
{
continue;
}
if (!rt.ContainsKey(temp))
{
rt.Add(temp, tag.Description);
}
}
}
return rt;
}
/// <summary>篩選參數並將其名稱轉換為中文
/// </summary>
/// <param name="str">參數名稱</param>
/// <returns>參數中文名</returns>
private static string EngToChs(string str)
{
var rt = "其他";
switch (str)
{
case "Exif Version":
rt = "Exif版本";
break;
case "Model":
rt = "相機型號";
break;
case "Lens Model":
rt = "鏡頭類型";
break;
case "File Name":
rt = "文件名";
break;
case "File Size":
rt = "文件大小";
break;
case "Date/Time":
rt = "拍攝時間";
break;
case "File Modified Date":
rt = "修改時間";
break;
case "Image Height":
rt = "照片高度";
break;
case "Image Width":
rt = "照片寬度";
break;
case "X Resolution":
rt = "水平分辨率";
break;
case "Y Resolution":
rt = "垂直分辨率";
break;
case "Color Space":
rt = "色彩空間";
break;
case "Shutter Speed Value":
rt = "快門速度";
break;
case "F-Number":
rt = "光圈";//Aperture Value也表示光圈
break;
case "ISO Speed Ratings":
rt = "ISO";
break;
case "Exposure Bias Value":
rt = "曝光補償";
break;
case "Focal Length":
rt = "焦距";
break;
case "Exposure Program":
rt = "曝光程序";
break;
case "Metering Mode":
rt = "測光模式";
break;
case "Flash Mode":
rt = "閃光燈";
break;
case "White Balance Mode":
rt = "白平衡";
break;
case "Exposure Mode":
rt = "曝光模式";
break;
case "Continuous Drive Mode":
rt = "驅動模式";
break;
case "Focus Mode":
rt = "對焦模式";
break;
}
return rt;
}
#endregion
文件瀏覽完畢后的截圖:
3)文件全部瀏覽完畢后,就開始進行壓縮。
因為文件數量大,原來的簡單壓縮版本總是容易卡死,這里的新版本用了線程,就沒有卡死的問題了。
這里的壓縮核心代碼直接參考了
用C#開發一個WinForm版的批量圖片壓縮工具
Thread workThread = new Thread(new ThreadStart(CompressAll));
workThread.IsBackground = true;
workThread.Start();
我添加了i標識處理成功的文件數量,壓縮失敗的時候i-=1。
if (CompressPicture(item, fileName))
{
if (this.InvokeRequired)
{
this.Invoke(new DelegateWriteResult(WriteResult), new object[] { item, true });
}
else
{
this.WriteResult(item, true);
}
}
else
{
i -= 1;
if (this.InvokeRequired)
{
this.Invoke(new DelegateWriteResult(WriteResult), new object[] { item, false });
}
else
{
this.WriteResult(item, false);
}
}
改變圖片質量這里就是第二步思路,分兩步走:
像素在1000以內的直接修改圖片品質處理;
像素大於1000的按尺寸大小壓縮圖片,然后再修改圖片品質處理。
/// <summary>
/// 改變圖片質量
/// </summary>
/// <param name="imgPath">文件路徑</param>
/// <param name="imgName">文件名</param>
private static bool VaryQualityLevel(string imgPath, string imgName)
{
bool result = false;
Bitmap bmp1 = new Bitmap(imgPath);
//獲取照片信息
// GetExifByMe(imgPath);
//先獲取圖片的像素
var imgPixl = RGB2Gray(bmp1);
//像素超出,先壓縮圖片
if (imgPixl.Width > 1000 && imgPixl.Height > 1000)
{
double width = 0;
double height = 0;
if (imgPixl.Width > 2000 && imgPixl.Height > 2000)
{
width = System.Math.Ceiling(Convert.ToDouble(imgPixl.Width / 4));
height = System.Math.Ceiling(Convert.ToDouble(imgPixl.Height / 4));
}
else if (imgPixl.Width > 1000 && imgPixl.Height > 1000)
{
width = System.Math.Ceiling(Convert.ToDouble(imgPixl.Width / 2));
height = System.Math.Ceiling(Convert.ToDouble(imgPixl.Height / 2));
}
//cutimg先創建好
//檢查是否存在文件夾
string subPath = @"d:/cutimg/";
if (false == System.IO.Directory.Exists(subPath))
{
//創建pic文件夾
System.IO.Directory.CreateDirectory(subPath);
}
result = FixSize(imgPath, Convert.ToInt32(width), Convert.ToInt32(height), subPath + imgName, imgName);
}
else
{
result = SetImgQuality(imgPath, imgPath, imgName);
}
return result;
}
調用按圖片尺寸壓縮方法,先存儲壓縮后的圖片,圖片大小往往還超過1M。再設置圖片的質量,二次處理,壓縮后的圖片大小小於200K。
/// <summary> 按圖片尺寸大小壓縮圖片</summary>
/// <param name="sourceFile">原始圖片文件</param>
/// <param name="xWidth">圖片width</param>
/// <param name="yWidth">圖片height</param>
/// <param name="outputFile">輸出文件名</param>
/// <param name="imgName">文件名</param>
/// <returns>成功返回true,失敗則返回false</returns>
public static bool FixSize(string sourceFile, int xWidth, int yWidth, string outputFile, string imgName)
{
try
{
Bitmap sourceImage = new Bitmap(sourceFile);
ImageCodecInfo myImageCodecInfo = GetEncoderInfo("image/jpeg");
Bitmap newImage = new Bitmap((int)(xWidth), (int)(yWidth));
Graphics g = Graphics.FromImage(newImage);
g.DrawImage(sourceImage, 0, 0, xWidth, yWidth);
sourceImage.Dispose();
g.Dispose();
newImage.Save(outputFile);
//設置圖片質量
SetImgQuality(sourceFile, outputFile, imgName);
newImage.Dispose();
//刪除該圖片文件
File.Delete(outputFile);
return true;
}
catch (Exception ex)
{
Logging.Error("FixSize:" + imgName + " 壓縮出錯:" + ex.Message);
return false;
}
}
調用按圖片尺寸壓縮的時候,這里發生“GDI+發生一般性錯誤”這個提示,原因是因為調用了SetImgQuality這個方法,文件還沒有釋放出來,在最后加上bmp1.Dispose();就解決了。
//設置圖片質量
SetImgQuality(sourceFile, outputFile, imgName);
在文件壓縮出錯的時候,我把出錯的文件寫入文本:
for (int j = 0; j < this.lvSourceFolderList.Items.Count; j++)
{
if (fileName == this.lvSourceFolderList.Items[j].Text)
{
//壓縮失敗的文件寫入文本
using (StreamWriter my_writer = new StreamWriter(@"d:\CompressFailFile.txt", true, System.Text.Encoding.Default))
{
string txtstr = "壓縮失敗:" + fileName + "\r\n";
my_writer.Write(txtstr);
my_writer.Flush();
}
this.lvSourceFolderList.Items[j].BackColor = SystemColors.ControlDark;
}
}
在這里出現“文件正由另一進程使用,該進程無法訪問該文件”的錯誤提示,當時在本地上跑沒有任何問題,放在服務器上跑就報錯。后來把服務器上面的文件拿到本地測試,發現是這里出錯了。換了using后完美解決。
壓縮出錯的文件除了在文本記錄外,我還做了高亮顯示。選中高亮數據的時候,因為無法復制,添加了SelectedIndexChanged事件以及文本框顯示。
/// <summary>
/// 選擇行
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lvSourceFolderList_SelectedIndexChanged(object sender, EventArgs e)
{
ListView.SelectedIndexCollection indexes = lvSourceFolderList.SelectedIndices;//
string pr = "";
foreach (int index in indexes)
{
pr = lvSourceFolderList.Items[index].Text;
}
this.lblChoose.Visible = true;
this.txtContent.Visible = true;
this.txtContent.Text = pr;// 顯示選擇的行的內容
}
最后,貼上我的源碼。因為自己在做的過程中參考、借鑒了很多前輩的分享,我也把自己完整的代碼分享出來。
參考資源
在做的過程中,我走了很多彎路,幸好在這個互聯網發達的時代,在知識共享的時代,我有幸參考了各路前輩分享的資料,才得以完成這個任務。非常感謝以下前輩的分享,還有一個分享當時沒有保存到鏈接,找不着了。無論如何,我心永存感激。