ASP.NET 專案常需產生圖片縮圖 (列表預覽、上傳後自動生成)。本文提供功能完整的 `ImageHelper` 類別,支援:固定寬高、等比例縮放、百分比縮放、輸出至檔案/串流/Byte[]、JPEG 品質控制、高品質插值模式、自動處理 EXIF 旋轉。
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
public static class ImageHelper
{
/// 預設 JPEG 品質 (1-100)
public const int DefaultJpegQuality = 85;
/// 縮圖模式
public enum ThumbnailMode
{
/// 固定寬高,可能變形
FixedSize,
/// 等比例縮放,寬高不超過指定值 (預設)
KeepAspectRatio,
/// 等比例縮放並裁切至指定寬高 (居中裁切)
CropToSize
}
///
/// 產生縮圖並儲存至檔案
///
public static bool CreateThumbnail(
string sourcePath,
string destPath,
int maxWidth,
int maxHeight,
ThumbnailMode mode = ThumbnailMode.KeepAspectRatio,
int jpegQuality = DefaultJpegQuality)
{
try
{
using (var srcImage = Image.FromFile(sourcePath))
using (var thumb = GenerateThumbnail(srcImage, maxWidth, maxHeight, mode))
{
SaveImage(thumb, destPath, jpegQuality);
return true;
}
}
catch (Exception ex)
{
// 記錄日誌: ex.Message
return false;
}
}
///
/// 產生縮圖並回傳 Byte[] (適合存資料庫或回傳 Web API)
///
public static byte[] CreateThumbnailBytes(
byte[] sourceBytes,
int maxWidth,
int maxHeight,
ThumbnailMode mode = ThumbnailMode.KeepAspectRatio,
int jpegQuality = DefaultJpegQuality)
{
using (var ms = new MemoryStream(sourceBytes))
using (var srcImage = Image.FromStream(ms))
using (var thumb = GenerateThumbnail(srcImage, maxWidth, maxHeight, mode))
{
return ImageToBytes(thumb, jpegQuality);
}
}
///
/// 產生縮圖並寫入輸出串流 (適合 HTTP 回應)
///
public static void CreateThumbnailToStream(
Stream sourceStream,
Stream outputStream,
int maxWidth,
int maxHeight,
ThumbnailMode mode = ThumbnailMode.KeepAspectRatio,
int jpegQuality = DefaultJpegQuality)
{
using (var srcImage = Image.FromStream(sourceStream))
using (var thumb = GenerateThumbnail(srcImage, maxWidth, maxHeight, mode))
{
SaveImageToStream(thumb, outputStream, jpegQuality);
}
}
///
/// 核心縮圖邏輯:支援三種模式、高品質插值、EXIF 旋轉修正
///
private static Image GenerateThumbnail(Image source, int maxWidth, int maxHeight, ThumbnailMode mode)
{
// 1. 修正 EXIF 方向 (避免手機拍照旋轉 90 度)
source = AutoRotateImage(source);
int srcW = source.Width;
int srcH = source.Height;
// 2. 計算目標尺寸
int destW, destH;
switch (mode)
{
case ThumbnailMode.FixedSize:
destW = maxWidth; destH = maxHeight;
break;
case ThumbnailMode.KeepAspectRatio:
double ratio = Math.Min((double)maxWidth / srcW, (double)maxHeight / srcH);
destW = (int)Math.Round(srcW * ratio);
destH = (int)Math.Round(srcH * ratio);
break;
case ThumbnailMode.CropToSize:
double ratioW = (double)maxWidth / srcW;
double ratioH = (double)maxHeight / srcH;
double cropRatio = Math.Max(ratioW, ratioH); // 取較大比例,確保覆蓋目標區域
int cropW = (int)Math.Round(srcW * cropRatio);
int cropH = (int)Math.Round(srcH * cropRatio);
// 先縮放至覆蓋尺寸,再裁切
var temp = new Bitmap(cropW, cropH);
using (var g = Graphics.FromImage(temp))
g.DrawImage(source, 0, 0, cropW, cropH);
// 裁切居中區域
int x = (cropW - maxWidth) / 2;
int y = (cropH - maxHeight) / 2;
var cropped = new Bitmap(maxWidth, maxHeight);
using (var g = Graphics.FromImage(cropped))
g.DrawImage(temp, -x, -y);
temp.Dispose();
return cropped;
default:
throw new ArgumentOutOfRangeException(nameof(mode));
}
// 3. 建立高品質縮圖 (使用 Graphics 而非 GetThumbnailImage,品質更好、可控插值模式)
var result = new Bitmap(destW, destH);
result.SetResolution(source.HorizontalResolution, source.VerticalResolution);
using (var g = Graphics.FromImage(result))
{
// 高品質設定
g.InterpolationMode = InterpolationMode.HighQualityBicubic; // 雙三次插值
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;
// 白色背景 (避免透明 PNG 轉 JPEG 產生黑底)
g.Clear(Color.White);
g.DrawImage(source, 0, 0, destW, destH);
}
return result;
}
/// 根據 EXIF Orientation 標籤自動旋轉圖片
private static Image AutoRotateImage(Image img)
{
const int OrientationId = 0x0112; // EXIF Orientation Tag
if (!img.PropertyIdList.Contains(OrientationId)) return img;
var prop = img.GetPropertyItem(OrientationId);
int orientation = BitConverter.ToUInt16(prop.Value, 0);
RotateFlipType rotateType = orientation switch
{
2 => RotateFlipType.RotateNoneFlipX,
3 => RotateFlipType.Rotate180FlipNone,
4 => RotateFlipType.Rotate180FlipX,
5 => RotateFlipType.Rotate90FlipX,
6 => RotateFlipType.Rotate90FlipNone,
7 => RotateFlipType.Rotate270FlipX,
8 => RotateFlipType.Rotate270FlipNone,
_ => RotateFlipType.RotateNoneFlipNone
};
if (rotateType != RotateFlipType.RotateNoneFlipNone)
{
var rotated = (Image)img.Clone();
rotated.RotateFlip(rotateType);
rotated.RemovePropertyItem(OrientationId); // 移除標籤避免重複旋轉
return rotated;
}
return img;
}
/// 儲存圖片至檔案 (支援 JPEG 品質)
private static void SaveImage(Image image, string path, int jpegQuality)
{
string ext = Path.GetExtension(path).ToLowerInvariant();
if (ext == ".jpg" || ext == ".jpeg")
{
var encoder = GetEncoder(ImageFormat.Jpeg);
var params_ = new EncoderParameters(1);
params_.Param[0] = new EncoderParameter(Encoder.Quality, (long)jpegQuality);
image.Save(path, encoder, params_);
}
else if (ext == ".png")
{
image.Save(path, ImageFormat.Png);
}
else
{
image.Save(path); // 預設格式
}
}
/// 儲存圖片至串流
private static void SaveImageToStream(Image image, Stream stream, int jpegQuality)
{
var encoder = GetEncoder(ImageFormat.Jpeg);
var params_ = new EncoderParameters(1);
params_.Param[0] = new EncoderParameter(Encoder.Quality, (long)jpegQuality);
image.Save(stream, encoder, params_);
}
/// 轉換為 Byte[] (JPEG)
private static byte[] ImageToBytes(Image image, int jpegQuality)
{
using (var ms = new MemoryStream())
{
SaveImageToStream(image, ms, jpegQuality);
return ms.ToArray();
}
}
/// 取得影像編碼器
private static ImageCodecInfo GetEncoder(ImageFormat format)
{
return ImageCodecInfo.GetImageDecoders()
.FirstOrDefault(c => c.FormatID == format.Guid);
}
}
protected void btnUpload_Click(object sender, EventArgs e)
{
if (!fuImage.HasFile) return;
string uploadFolder = Server.MapPath("~/Uploads/");
string thumbFolder = Server.MapPath("~/Thumbs/");
Directory.CreateDirectory(uploadFolder);
Directory.CreateDirectory(thumbFolder);
string fileName = Guid.NewGuid() + Path.GetExtension(fuImage.FileName);
string srcPath = Path.Combine(uploadFolder, fileName);
fuImage.SaveAs(srcPath);
// 產生三種尺寸
ImageHelper.CreateThumbnail(srcPath, Path.Combine(thumbFolder, "large_" + fileName), 800, 600);
ImageHelper.CreateThumbnail(srcPath, Path.Combine(thumbFolder, "medium_" + fileName), 400, 300);
ImageHelper.CreateThumbnail(srcPath, Path.Combine(thumbFolder, "small_" + fileName), 150, 150, ImageHelper.ThumbnailMode.CropToSize); // 正方形裁切
}
// Thumbnail.ashx
public class ThumbnailHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string src = context.Request["src"]; // 相對路徑
int w = int.TryParse(context.Request["w"], out var _w) ? _w : 200;
int h = int.TryParse(context.Request["h"], out var _h) ? _h : 200;
string modeStr = context.Request["mode"] ?? "keep";
ImageHelper.ThumbnailMode mode = Enum.TryParse(modeStr, true, out ImageHelper.ThumbnailMode m) ? m : ImageHelper.ThumbnailMode.KeepAspectRatio;
string physPath = context.Server.MapPath(src);
if (!File.Exists(physPath))
{
context.Response.StatusCode = 404;
return;
}
context.Response.ContentType = "image/jpeg";
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(30));
ImageHelper.CreateThumbnailToStream(
new FileStream(physPath, FileMode.Open, FileAccess.Read, FileShare.Read),
context.Response.OutputStream,
w, h, mode);
}
public bool IsReusable => true;
}
[HttpPost]
public IHttpActionResult GenerateThumb()
{
var file = HttpContext.Current.Request.Files[0];
if (file == null) return BadRequest();
using (var ms = new MemoryStream())
{
file.InputStream.CopyTo(ms);
byte[] thumbBytes = ImageHelper.CreateThumbnailBytes(ms.ToArray(), 300, 200);
return Ok(Convert.ToBase64String(thumbBytes));
}
}
| 技術點 | 說明 |
|---|---|
| Graphics.DrawImage vs GetThumbnailImage | `GetThumbnailImage` 速度快但品質差、不支援插值模式控制。`Graphics.DrawImage` 配合 `InterpolationMode.HighQualityBicubic` 品質最佳。 |
| EXIF 旋轉修正 | 手機拍照含 Orientation 標籤 (1-8),直接顯示會旋轉錯誤。`AutoRotateImage` 讀取標籤並實際旋轉像素,移除標籤。 |
| 白色背景填充 | 透明 PNG 轉 JPEG 會產生黑底。`g.Clear(Color.White)` 解決。 |
| JPEG 品質參數 | 1-100,85 為平衡點。`Encoder.Quality` 需搭配 `ImageCodecInfo` 使用。 |
| 記憶體管理 | 所有 `Image`、`Graphics`、`Stream` 均用 `using` 確保釋放非托管資源,防止 GDI+ 記憶體洩漏。 |
| 解析度保留 | `result.SetResolution` 保持原圖 DPI,列印品質不受影響。 |
本文提供的 `ImageHelper` 類別涵蓋企業級縮圖需求的核心功能:多種縮放模式、高品質插值、EXIF 修正、JPEG 品質控制、多種輸出目標。直接複製至專案共用庫即可使用,並可依專案需求擴充水印、圓角、濾鏡等功能。