关于缩略图的生成与访问策略的一些经验分享
之前在一家做图片分享的互联网公司上班,由于一系列的因素公司业务关停,我们不得不另谋出路。不过还是很感谢公司为我们提供了一个相对宽松的工作环境,同时也让我们体会到了一个创业公司的激情与困惑。
今天就跟大家说说缩略图的那些事儿,以此来做为一个总结和纪念。
对于项目来说,不管大小一般都或多或少的存在一些图片,有时候我们需要用代码对图片进行一些处理,比如加个水印、生成个缩略图等,这些都是比较普通的需求了。为了让大家对接下来的需求有一个直观的了解,我们还是先来看一些截图
(图1) 照片上传页
(图2)照片列表页
(图3)照片单张页
分析代码之前首先介绍一下项目所使用到的一些框架及主要技术点。项目采用MVC3+EF4.1框架搭建,所涉及到的技术主要有GDI、IO流、缓存、正则表达式等,下面是项目截图。
一个很简单的需求,下面我们就来一步步分析如何实现。这里我们讨论的重点不是缩略图本身生成的技术,我们将要讨论的话题主要包括以下几方面:
1.照片信息建模。这是基于EF的Code First开发方式的第一步。
OriginalImage.cs (原始照片信息)
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel.DataAnnotations; namespace ImgService.EF.Domain { public class OriginalImage { public OriginalImage() { ThumbnailImage = new List<ThumbnailImage>(); Created = DateTime.Now; } [Key] public string OriginalID { get; set; } public string Ext { get; set; } public string Path { get; set; } public int Width { get; set; } public int Height { get; set; } public string Description { get; set; } public DateTime Created { get; set; } /// <summary> /// 缩略图信息 /// </summary> public virtual List<ThumbnailImage> ThumbnailImage { get; set; } } }
ThumbnailImage.cs (缩略照片信息)
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel.DataAnnotations; namespace ImgService.EF.Domain { public class ThumbnailImage { public ThumbnailImage() { Created = DateTime.Now; } [Key] public string ID { get; set; } public string Ext { get; set; } public string Path { get; set; } public int Width { get; set; } public int Height { get; set; } public string Description { get; set; } public DateTime Created { get; set; } [Required] public string OriginalID { get; set; } public virtual OriginalImage OriginalImage { get; set; } } }
2.缩略图的基本生成原理。这里不做详细讲解,都是写网上类似的代码,不过应该还有可以优化的地方。
ImageHelper.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; namespace ImgService.Helpers { public class ImageHelper { public static Size GetSize(Stream stream) { System.Drawing.Image imageObj = System.Drawing.Image.FromStream(stream); Size size = imageObj.Size; imageObj.Dispose(); return size; } public static byte[] GetImageBytes(string physicPath) { if (!File.Exists(physicPath)) { return null; } FileStream fs = new FileStream(physicPath, FileMode.Open); int fileLen = (int)fs.Length; byte[] bytes = new byte[fileLen]; fs.Read(bytes, 0, fileLen); fs.Close(); fs.Dispose(); return bytes; } public static void SaveImage(Bitmap img,string physicPath) { img.Save(physicPath); img.Dispose(); } /// <summary> /// 计算新尺寸 /// </summary> /// <param name="width">原始宽度</param> /// <param name="height">原始高度</param> /// <param name="maxWidth">最大新宽度</param> /// <param name="maxHeight">最大新高度</param> /// <returns></returns> private static Size Resize(int width, int height, int maxWidth, int maxHeight) { decimal MAX_WIDTH = (decimal)maxWidth; decimal MAX_HEIGHT = (decimal)maxHeight; decimal ASPECT_RATIO = MAX_WIDTH / MAX_HEIGHT; int newWidth, newHeight; decimal originalWidth = (decimal)width; decimal originalHeight = (decimal)height; if (originalWidth > MAX_WIDTH || originalHeight > MAX_HEIGHT) { decimal factor; // determine the largest factor if (originalWidth / originalHeight > ASPECT_RATIO) { factor = originalWidth / MAX_WIDTH; newWidth = Convert.ToInt32(originalWidth / factor); newHeight = Convert.ToInt32(originalHeight / factor); } else { factor = originalHeight / MAX_HEIGHT; newWidth = Convert.ToInt32(originalWidth / factor); newHeight = Convert.ToInt32(originalHeight / factor); } } else { newWidth = width; newHeight = height; } return new Size(newWidth, newHeight); } /// <summary> /// 生成宽高相等的缩略图,当原图比例严重失衡时缩略图将只截取原图的一部分 /// </summary> /// <param name="bitmapOriginal"></param> /// <param name="newSize"></param> /// <returns></returns> public static Bitmap Resize(Bitmap bitmapOriginal, int newSize) { int originalWidth = bitmapOriginal.Width; int originalHeight = bitmapOriginal.Height; double standard = Convert.ToDouble(newSize) / Convert.ToDouble(newSize); double actualRate = Convert.ToDouble(originalWidth) / Convert.ToDouble(originalHeight); int imgNewWidth = newSize; int imgNewHeight = newSize; int x = 0; int y = 0; if (actualRate > standard) { imgNewWidth = originalWidth * newSize / originalHeight; x = (imgNewWidth - newSize) / 2; } else { imgNewHeight = originalHeight * newSize / originalWidth; y = (imgNewHeight - newSize) / 2; } Bitmap bitmap = new Bitmap(imgNewWidth, imgNewHeight); Graphics graphics = Graphics.FromImage(bitmap); graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.Clear(Color.Transparent); graphics.DrawImage(bitmapOriginal, new Rectangle(0, 0, imgNewWidth, imgNewHeight)); Bitmap bitmap_cut = new Bitmap(newSize, newSize); graphics = Graphics.FromImage(bitmap_cut); graphics.DrawImage(bitmap, new System.Drawing.Rectangle(0, 0, newSize, newSize), new System.Drawing.Rectangle(x, y, newSize, newSize), System.Drawing.GraphicsUnit.Pixel); graphics.Dispose(); graphics = null; bitmap.Dispose(); bitmap = null; //bitmapOriginal.Dispose(); return bitmap_cut; } /// <summary> /// 生成指定宽高的缩略图,当原图比例严重失衡时缩略图将只截取原图的一部分 /// </summary> /// <param name="bitmapOriginal"></param> /// <param name="width"></param> /// <param name="height"></param> /// <returns></returns> public static Bitmap Resize(Bitmap bitmapOriginal, int width, int height) { int originalWidth = bitmapOriginal.Width; int originalHeight = bitmapOriginal.Height; double Standard = Convert.ToDouble(width) / Convert.ToDouble(height); double ActualRate = Convert.ToDouble(originalWidth) / Convert.ToDouble(originalHeight); int imgNewWidth = width; int imgNewHeight = height; int x = 0; int y = 0; if (ActualRate > Standard) { imgNewWidth = originalWidth * height / originalHeight; x = (imgNewWidth - width) / 2; } else { imgNewHeight = originalHeight * width / originalWidth; y = (imgNewHeight - height) / 2; } Bitmap bitmap = new Bitmap(imgNewWidth, imgNewHeight); Graphics graphics = Graphics.FromImage(bitmap); graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.Clear(Color.Transparent); graphics.DrawImage(bitmapOriginal, new Rectangle(0, 0, imgNewWidth, imgNewHeight)); Bitmap bitmap_cut = new Bitmap(width, height); graphics = Graphics.FromImage(bitmap_cut); graphics.DrawImage(bitmap, new System.Drawing.Rectangle(0, 0, width, height), new System.Drawing.Rectangle(x, y, width, height), System.Drawing.GraphicsUnit.Pixel); graphics.Dispose(); graphics = null; bitmap.Dispose(); bitmap = null; //bitmapOriginal.Dispose(); return bitmap_cut; } /// <summary> /// 生成指定宽度的缩略图,高度将等比缩放 /// </summary> /// <param name="bitmapOriginal"></param> /// <param name="width"></param> /// <returns></returns> public static Bitmap ResizeW(Bitmap bitmapOriginal, int width) { int originalWidth = bitmapOriginal.Width; int originalHeight = bitmapOriginal.Height; int height = 0; if (originalWidth < width) { width = originalWidth; height = originalHeight; } else { height = originalHeight * width / originalWidth; } Bitmap bitmap = new Bitmap(width, height); Graphics graphics = Graphics.FromImage(bitmap); graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.Clear(Color.Transparent); graphics.DrawImage(bitmapOriginal, new Rectangle(0, 0, width, height)); graphics.Dispose(); graphics = null; return bitmap; } /// <summary> /// 生成指定高度的缩略图,宽度将等比缩放 /// </summary> /// <param name="bitmapOriginal"></param> /// <param name="height"></param> /// <returns></returns> public static Bitmap ResizeH(Bitmap bitmapOriginal, int height) { int originalWidth = bitmapOriginal.Width; int originalHeight = bitmapOriginal.Height; int width = 0; if (originalHeight < height) { width = originalWidth; height = originalHeight; } else { width = originalWidth * height / originalHeight; } Bitmap bitmap = new Bitmap(width, height); Graphics graphics = Graphics.FromImage(bitmap); graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.Clear(Color.Transparent); graphics.DrawImage(bitmapOriginal, new Rectangle(0, 0, width, height)); graphics.Dispose(); graphics = null; return bitmap; } /// <summary> /// 裁切原图指定区域 /// </summary> /// <param name="bitmapOriginal"></param> /// <param name="width"></param> /// <param name="height"></param> /// <param name="left"></param> /// <param name="top"></param> /// <returns></returns> public static Bitmap Crop(Bitmap bitmapOriginal, int width, int height, int left, int top) { int originalWidth = bitmapOriginal.Width; int originalHeight = bitmapOriginal.Height; double Standard = Convert.ToDouble(width) / Convert.ToDouble(height); double ActualRate = Convert.ToDouble(originalWidth) / Convert.ToDouble(originalHeight); int imgNewWidth = width; int imgNewHeight = height; int x = 0; int y = 0; if (ActualRate > Standard) { imgNewWidth = originalWidth * height / originalHeight; if (left == -1) { x = (imgNewWidth - width) / 2; } else { if (left > (imgNewWidth - width) / 2) { x = (imgNewWidth - width) / 2; } else { x = left; } } } else { imgNewHeight = originalHeight * width / originalWidth; if (top == -1) { y = (imgNewHeight - height) / 2; } else { if (top > (imgNewHeight - height) / 2) { y = (imgNewHeight - height) / 2; } else { y = top; } } } Bitmap bitmap = new Bitmap(imgNewWidth, imgNewHeight); Graphics graphics = Graphics.FromImage(bitmap); graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.CompositingQuality = CompositingQuality.HighQuality; graphics.Clear(Color.Transparent); graphics.DrawImage(bitmapOriginal, new Rectangle(0, 0, imgNewWidth, imgNewHeight)); Bitmap bitmap_cut = new Bitmap(width, height); graphics = Graphics.FromImage(bitmap_cut); graphics.DrawImage(bitmap, new System.Drawing.Rectangle(0, 0, width, height), new System.Drawing.Rectangle(x, y, width, height), System.Drawing.GraphicsUnit.Pixel); graphics.Dispose(); graphics = null; bitmap.Dispose(); bitmap = null; //bitmapOriginal.Dispose(); return bitmap_cut; } } }
3.缩略图的存取策略
原图在上传的同时会生成150 * 150,200 * 200,800 * X三种不同规格的缩略图文件并存储在项目的Images目录之下,为了提高图片的读取速度,三张缩略图在生成的同时就被放入了缓存当中。这里是把图片放入本地缓存,当然如果实际项目图片较多占用的内存较大,可考虑memcache等第三方分布式缓存策略。以下是核心代码
ManageController.cs
//要生成缩略图的保存路径,默认生成800 * x,150 * 150,200 * 200 的三种缩略图 string w800_ImgName = string.Format("{0}_w_800{1}", upLoadFileInfo.FileID, upLoadFileInfo.FileExt); string size150_ImgName = string.Format("{0}_s_150{1}", upLoadFileInfo.FileID, upLoadFileInfo.FileExt); string size200_ImgName = string.Format("{0}_s_200{1}", upLoadFileInfo.FileID, upLoadFileInfo.FileExt); string image800_SavePath = string.Format("~/images/{0}", w800_ImgName); string image150_SavePath = string.Format("~/images/{0}", size150_ImgName); string image200_SavePath = string.Format("~/images/{0}", size200_ImgName); string image800_SavePhysicPath = Server.MapPath(image800_SavePath); string image150_SavePhysicPath = Server.MapPath(image150_SavePath); string image200_SavePhysicPath = Server.MapPath(image200_SavePath); //生成宽为800的缩略图并保存 Bitmap bitmap_800 = ImageHelper.ResizeW(new Bitmap(upLoadFileInfo.PostFile.InputStream), 800); DB.ThumbnailImages.Add(new ThumbnailImage { OriginalID = upLoadFileInfo.FileID, ID = w800_ImgName, Ext = upLoadFileInfo.FileExt, Path = image800_SavePath, Height = bitmap_800.Height, Width = bitmap_800.Width, Description = Description }); ImageHelper.SaveImage(bitmap_800, image800_SavePhysicPath); //将图片存入缓存 ImageCacheHelper.SetImageCahce(w800_ImgName, ImageHelper.GetImageBytes(image800_SavePhysicPath)); //生成宽高为150的缩略图并保存 Bitmap bitmap_150 = ImageHelper.Resize(new Bitmap(upLoadFileInfo.PostFile.InputStream), 150); DB.ThumbnailImages.Add(new ThumbnailImage { OriginalID = upLoadFileInfo.FileID, ID = size150_ImgName, Ext = upLoadFileInfo.FileExt, Path = image150_SavePath, Height = bitmap_150.Height, Width = bitmap_150.Width, Description = Description }); ImageHelper.SaveImage(bitmap_150, image150_SavePhysicPath); //将图片存入缓存 ImageCacheHelper.SetImageCahce(size150_ImgName, ImageHelper.GetImageBytes(image150_SavePhysicPath)); //生成宽高为200的缩略图并保存 Bitmap bitmap_200 = ImageHelper.Resize(new Bitmap(upLoadFileInfo.PostFile.InputStream), 200); DB.ThumbnailImages.Add(new ThumbnailImage { OriginalID = upLoadFileInfo.FileID, ID = size200_ImgName, Ext = upLoadFileInfo.FileExt, Path = image200_SavePath, Height = bitmap_200.Height, Width = bitmap_200.Width, Description = Description }); DB.SaveChanges(); ImageHelper.SaveImage(bitmap_200, image200_SavePhysicPath); //将图片存入缓存 ImageCacheHelper.SetImageCahce(size200_ImgName, ImageHelper.GetImageBytes(image200_SavePhysicPath));
4.缩略图访问服务。来看下面几个地址
http://www.caidian.com/Thumbnail/d92fe65928bc48a8b9ac47d771dad56e_s_200.jpg
http://www.caidian.com/Thumbnail/d92fe65928bc48a8b9ac47d771dad56e_w_800.jpg
想必大家都见到过类似的图片地址,其实地址中隐含了图片的生成规则和尺寸。d92fe65928bc48a8b9ac47d771dad56e_s_200.jpg里面的s表示生成规则,200表示要生成的尺寸大小。我这里的实现方式是,当用户访问类似地址的时候其实是访问了ServiceController控制器中名为Thumbnail的Action,而d92fe65928bc48a8b9ac47d771dad56e_s_200.jpg则是传递给Action的参数,这样还有一个好处就是图片访问有了一个统一的处理入口,如果你访问的图片被删除或者地址错误我们可以返回一个默认图片,再也不用担心会出一个红叉了,具体还是来看实现代码。
ServiceController.cs
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using ImgService.Helpers; using System.Text.RegularExpressions; using ImgService.EF; using ImgService.EF.Domain; using System.Drawing; using System.IO; namespace ImgService.Controllers { public class ServiceController : Controller { private ImgServiceContext DB = new ImgServiceContext(); public FileResult Thumbnail(string key = "") { //1.从缓存提取图片 //2.缓存没有,读取缩略图文件 //3.缩略图不存在,读取图片源文件。生成缩略图,加入缓存 key = key.ToLower(); if (string.IsNullOrEmpty(key)) { return null; } byte[] bytes = null; //正则分析Key的内容,可匹配的格式如 43sdfwerw345343ff_s_150.jpg 或 43sdfwerw345343ff_w_800.jpg 等 string rule = @"(?<p1>([a-z0-9]+)_(s|w)_([\d]+)\.(jpg|png|bmp|tif|tiff))"; Match match = Regex.Match(key, rule, RegexOptions.IgnoreCase); if (match.Success) { string type = ""; //s或w int imgSize = 200; Bitmap originalBitmap = null; Bitmap thumbnaiBitmap = null; MemoryStream ms = null; type = match.Groups[2].Value.Trim().ToLower(); int.TryParse(match.Groups[3].Value, out imgSize); //确认请求的缩略图是否在系统生成范围之内,验证合法性 ThumbnailImage thumbnailData = GetThumbnail(key); if (thumbnailData != null) { //查询缓存 bytes = ImageCacheHelper.GetImageCache(key); if (bytes != null && bytes.Length > 0) { return File(bytes, "image/jpeg"); } //缓存不存在,从文件读取缩略图 bytes = ImageHelper.GetImageBytes(Server.MapPath(thumbnailData.Path)); if (bytes != null && bytes.Length > 0) //缩略图文件存在 { ImageCacheHelper.SetImageCahce(key, bytes); return File(bytes, "image/jpeg"); } else //缩略图不存在,读原图生成缩略图 { bytes = ImageHelper.GetImageBytes(Server.MapPath( thumbnailData.OriginalImage.Path )); if (bytes != null && bytes.Length > 0) //原图存在 { ms = new MemoryStream(bytes); originalBitmap = new Bitmap(ms); if (type == "s") { thumbnaiBitmap = ImageHelper.Resize(originalBitmap, imgSize);//生成缩略图 } if (type == "w") { thumbnaiBitmap = ImageHelper.ResizeW(originalBitmap, imgSize);//生成缩略图 } thumbnaiBitmap.Save(Server.MapPath(thumbnailData.Path)); //保存缩略图 bytes = ImageHelper.GetImageBytes(Server.MapPath(thumbnailData.Path)); ImageCacheHelper.SetImageCahce(key, bytes);//把缩略图放入缓存 ms.Dispose(); originalBitmap.Dispose(); thumbnaiBitmap.Dispose(); return File(bytes, "image/jpeg"); } else //原图不存在 { return File(Server.MapPath("~/404.jpg"), "image/jpeg"); } } } else { return File(Server.MapPath("~/404.jpg"), "image/jpeg"); } } else { return File(Server.MapPath("~/404.jpg"), "image/jpeg"); } } private ThumbnailImage GetThumbnail(string key) { var q = from t in DB.ThumbnailImages where t.ID == key select t; var imgList = q.ToList(); if (imgList == null || imgList.Count == 0) { return null; } return imgList.First(); } } }
5.自定义路由规则,让缩略图地址更友好
如果没有自定义路由规则的话,那么缩略图的访问地址应该类似这样。http://www.caidian.com/Thumbnail?key=d92fe65928bc48a8b9ac47d771dad56e_s_200.jpg
这样的地址给人的第一印象会让人觉着不是一个纯粹的图片地址,所以我对它进行了路由规则的重写,来看看代码
Global.asax.cs
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); //为缩略图的访问配置一个比较友好的地址,访问的地址一般如下 //http://localhost:1087/Thumbnail/d92fe65928bc48a8b9ac47d771dad56e_s_200.jpg //http://localhost:1087/Thumbnail/d92fe65928bc48a8b9ac47d771dad56e_w_800.jpg routes.MapRoute( "ImgService", // Route name "thumbnail/{key}", // URL with parameters new { controller = "Service", action = "Thumbnail", key = "" } // Parameter defaults ); //照片列表,访问的地址一般如下 //http://localhost:1087/photos routes.MapRoute( "Client-photos", // Route name "photos", // URL with parameters new { controller = "Client", action = "Index"} // Parameter defaults ); //单张照片,访问的地址一般如下 //http://localhost:1087/photo/61e69dc5a92940ad86d9d419c496324d routes.MapRoute( "Client-photo", // Route name "photo/{key}", // URL with parameters new { controller = "Client", action = "Photo", key = "" } // Parameter defaults ); //这里请注意,默认的路由配置放在自定义配置后面,否则不认自定义路由规则 routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Manage", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
6.关于如何运行程序的注意事项
先下载源码,然后修改web.config中的数据库连接字符串,依你个人的环境而定。
<connectionStrings> <add name="ImgServiceDB" connectionString="server=.;uid=sa;pwd=123456;database=ImgServiceDB" providerName="System.Data.SqlClient"/> </connectionStrings>
接下来就是运行程序了,会自动的跳转到照片上传页面,由于里面没有任何照片数据所以你必须先进行上传,接下来就是看照片了... ...很简单
当然本项目只是一个很简单的范例程序,真正要开发一套大数据量,高并发的图片分享网站光有这点知识是远远不够的,我在这里也只是算抛砖引玉吧。
最后,感谢你了浏览到这里,如果你觉得有点收获的话不妨点击推荐。