在 ASP.NET 中動態產生圖片 (如縮圖、水印、驗證碼) 最標準的做法是實作 `IHttpHandler` (`.ashx`),直接輸出二進位串流至 `Response.OutputStream`,避免 `.aspx` 頁面生命週期的額外開銷。本文展示完整的 **BMP 轉 JPEG 動態壓縮** 範例,支援查詢字串參數控制壓縮品質 (0-100)。
<%@ WebHandler Language="C#" Class="DynamicJpegHandler" %>
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Web;
public class DynamicJpegHandler : IHttpHandler
{
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext context)
{
// 1. 取得壓縮品質參數 (0-100,預設 80)
int quality = 80;
if (!string.IsNullOrEmpty(context.Request.QueryString["comp"]))
int.TryParse(context.Request.QueryString["comp"], out quality);
quality = Math.Max(0, Math.Min(100, quality));
// 2. 來源圖片路徑 (實務建議從資料庫/快取/Blob 讀取)
string sourcePath = context.Server.MapPath("~/Images/source.bmp");
if (!System.IO.File.Exists(sourcePath))
{
context.Response.StatusCode = 404;
context.Response.Write("Source image not found");
return;
}
// 3. 輸出 JPEG
context.Response.Clear();
context.Response.ContentType = "image/jpeg";
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(30)); // 客戶端快取 30 天
context.Response.Cache.SetETag("W/\"source-bmp-" + quality + "\"");
using (Bitmap bmp = new Bitmap(sourcePath))
{
// 設定 JPEG 編碼參數
ImageCodecInfo jpegCodec = GetEncoderInfo("image/jpeg");
EncoderParameters encoderParams = new EncoderParameters(1);
encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, (long)quality);
// 直接輸出至 Response.OutputStream (無需暫存檔)
bmp.Save(context.Response.OutputStream, jpegCodec, encoderParams);
}
}
private ImageCodecInfo GetEncoderInfo(string mimeType)
{
foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders())
{
if (codec.MimeType == mimeType) return codec;
}
return null;
}
}
<system.webServer>
<handlers>
<add name="DynamicJpeg" verb="GET" path="DynamicJpeg.ashx"
type="DynamicJpegHandler" preCondition="integratedMode" />
</handlers>
</system.webServer>
<!-- 基本用法 -->
<img src="DynamicJpeg.ashx" alt="動態縮圖" />
<!-- 指定壓縮品質 60 -->
<img src="DynamicJpeg.ashx?comp=60" alt="低品質縮圖" />
<!-- 搭配 ASP.NET Image 控制項 -->
<asp:Image ID="imgDynamic" runat="server" />
// 後置碼
imgDynamic.ImageUrl = "DynamicJpeg.ashx?comp=75";
public class AdvancedImageHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// 參數解析
int width = ParseInt(context.Request["w"], 0);
int height = ParseInt(context.Request["h"], 0);
int quality = ParseInt(context.Request["q"], 85);
string format = context.Request["f"] ?? "jpeg"; // jpeg, png, gif
string watermark = context.Request["wm"]; // 水印文字
string source = context.Request["src"]; // 來源圖片虛擬路徑
if (string.IsNullOrEmpty(source))
{
context.Response.StatusCode = 400;
return;
}
string physicalPath = context.Server.MapPath(source);
if (!System.IO.File.Exists(physicalPath))
{
context.Response.StatusCode = 404;
return;
}
using (Image src = Image.FromFile(physicalPath))
{
// 計算目標尺寸 (等比例)
int targetW = src.Width, targetH = src.Height;
if (width > 0 || height > 0)
{
double ratio = 1.0;
if (width > 0 && height > 0)
ratio = Math.Min((double)width / src.Width, (double)height / src.Height);
else if (width > 0)
ratio = (double)width / src.Width;
else
ratio = (double)height / src.Height;
targetW = (int)(src.Width * ratio);
targetH = (int)(src.Height * ratio);
}
using (Bitmap dest = new Bitmap(targetW, targetH))
using (Graphics g = Graphics.FromImage(dest))
{
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.DrawImage(src, 0, 0, targetW, targetH);
// 水印
if (!string.IsNullOrEmpty(watermark))
{
using (Font font = new Font("Arial", 12, FontStyle.Bold))
using (Brush brush = new SolidBrush(Color.FromArgb(128, 255, 255, 255)))
{
g.DrawString(watermark, font, brush, targetW - 100, targetH - 20);
}
}
// 輸出
context.Response.Clear();
context.Response.ContentType = "image/" + format;
ImageCodecInfo codec = GetEncoderInfo("image/" + format);
EncoderParameters eps = new EncoderParameters(1);
eps.Param[0] = new EncoderParameter(Encoder.Quality, (long)quality);
dest.Save(context.Response.OutputStream, codec, eps);
}
}
}
private int ParseInt(string s, int def) => int.TryParse(s, out int v) ? v : def;
private ImageCodecInfo GetEncoderInfo(string mime) =>
Array.Find(ImageCodecInfo.GetImageEncoders(), c => c.MimeType == mime);
public bool IsReusable => false;
}
| 層面 | 建議 |
|---|---|
| 快取策略 | 輸出 `ETag`/`Last-Modified`,搭配 `HttpCacheability.Public` + `MaxAge`;伺服器端可用 `MemoryCache`/`Redis` 快取產生後的位元組陣列 |
| 影子複製/檔案鎖定 | `Image.FromFile` 會鎖定檔案至物件 Dispose。高併發建議先 `File.ReadAllBytes` → `MemoryStream` → `Image.FromStream`,或使用 ImageSharp 避免鎖定 |
| 非同步處理 | ASP.NET Core 改用 `IAsyncHttpHandler` / Minimal API `app.MapGet`,支援 `await` 非同步 I/O |
| CDN 整合 | 動態圖片產生後上傳至 Blob Storage,回傳 CDN URL,後續請求直接命中 CDN |
| 跨平台 | .NET Core / 5+ 請改用 SixLabors.ImageSharp,`System.Drawing.Common` 僅支援 Windows 且效能較差 |
// Program.cs
app.MapGet("/api/image/{**path}", async (string path, int? w, int? h, int? q, HttpContext ctx) =>
{
// 驗證路徑安全性 (防止路徑遍歷)
string physical = Path.Combine(env.WebRootPath, "images", path);
if (!File.Exists(physical)) return Results.NotFound();
using var src = Image.Load(physical); // ImageSharp
// ... 縮放、水印、編碼 ...
var ms = new MemoryStream();
src.SaveAsJpeg(ms, new JpegEncoder { Quality = q ?? 80 });
ms.Position = 0;
return Results.File(ms, "image/jpeg", enableRangeProcessing: true);
});
總結: `.ashx` HttpHandler 是傳統 ASP.NET 動態圖片輸出的標準做法,輕量、高效、無 ViewState 開銷。新專案請採用 ASP.NET Core + ImageSharp + Minimal API / Controller,獲得跨平台、非同步、現代化架構優勢。