使用 `IHttpHandler (`.ashx`) 動態生成縮略圖,是 ASP.NET 經典且高效的圖片處理模式:無需預先生成儲存縮圖檔案,即時按需縮放、輸出至回應流,節省磁碟空間且支援任意尺寸參數。本文提供完整實作,含快取控制、錯誤處理、GridView 整合範例。
<%@ WebHandler Language="C#" Class="ImageThumbnailHandler" %>
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Web;
public class ImageThumbnailHandler : IHttpHandler
{
// 圖片根目錄 (建議從 web.config 讀取)
private static readonly string ImageRootPath =
System.Configuration.ConfigurationManager.AppSettings["ImageRootPath"]
?? @"D:\WebImages\";
public void ProcessRequest(HttpContext context)
{
// 1. 取得參數
string imageName = context.Request.QueryString["img"]; // 檔名
string widthParam = context.Request.QueryString["w"]; // 寬度 (選填)
string heightParam = context.Request.QueryString["h"]; // 高度 (選填)
string modeParam = context.Request.QueryString["mode"]; // 模式: fit/crop/fill
if (string.IsNullOrEmpty(imageName))
{
ReturnError(context, "缺少 img 參數");
return;
}
// 2. 安全性:防止路徑遍歷攻擊
string safeName = Path.GetFileName(imageName); // 只取檔名,移除路徑
string physicalPath = Path.Combine(ImageRootPath, safeName);
if (!File.Exists(physicalPath))
{
ReturnError(context, "圖片不存在");
return;
}
try
{
// 3. 載入原圖
using (Image original = Image.FromFile(physicalPath))
{
// 4. 計算目標尺寸
int targetWidth = ParseInt(widthParam, 150);
int targetHeight = ParseInt(heightParam, 150);
Size newSize = CalculateSize(original.Width, original.Height, targetWidth, targetHeight, modeParam);
// 5. 建立縮圖 (高品質插值)
using (Bitmap thumbnail = new Bitmap(newSize.Width, newSize.Height))
using (Graphics g = Graphics.FromImage(thumbnail))
{
// 高品質設定
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
// 繪製
g.DrawImage(original, 0, 0, newSize.Width, newSize.Height);
// 6. 輸出回應
context.Response.ContentType = GetContentType(safeName);
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(30)); // 瀏覽器快取 30 天
context.Response.Cache.SetETag(GenerateETag(physicalPath));
// 支援條件請求 (If-None-Match)
string ifNoneMatch = context.Request.Headers["If-None-Match"];
if (!string.IsNullOrEmpty(ifNoneMatch) && ifNoneMatch == context.Response.Cache.GetETag())
{
context.Response.StatusCode = 304;
context.Response.End();
return;
}
// 輸出至輸出流
ImageFormat format = GetImageFormat(safeName);
thumbnail.Save(context.Response.OutputStream, format);
}
}
}
catch (Exception ex)
{
// 記錄日誌
// Log.Error("縮圖生成失敗: " + imageName, ex);
ReturnError(context, "生成縮圖失敗");
}
}
private Size CalculateSize(int origW, int origH, int maxW, int maxH, string mode)
{
double ratio = (double)origW / origH;
switch ((mode ?? "fit").ToLower())
{
case "crop": // 裁切填滿 (保持比例,居中裁切)
if (ratio > (double)maxW / maxH)
{
// 原圖較寬,以高為準
int cropH = origH;
int cropW = (int)(origH * maxW / (double)maxH);
return new Size(maxW, maxH); // 輸出固定尺寸,DrawImage 會自動裁切
}
else
{
int cropW = origW;
int cropH = (int)(origW * maxH / (double)maxW);
return new Size(maxW, maxH);
}
case "fill": // 拉伸填滿 (不保持比例)
return new Size(maxW, maxH);
case "fit":
default: // 等比縮放 (預設)
if (origW > maxW || origH > maxH)
{
if (ratio > (double)maxW / maxH)
{
return new Size(maxW, (int)(maxW / ratio));
}
else
{
return new Size((int)(maxH * ratio), maxH);
}
}
return new Size(origW, origH); // 原圖較小,不放大
}
}
private string GetContentType(string fileName)
{
string ext = Path.GetExtension(fileName).ToLower();
return ext switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".bmp" => "image/bmp",
".webp" => "image/webp",
_ => "application/octet-stream"
};
}
private ImageFormat GetImageFormat(string fileName)
{
string ext = Path.GetExtension(fileName).ToLower();
return ext switch
{
".jpg" or ".jpeg" => ImageFormat.Jpeg,
".png" => ImageFormat.Png,
".gif" => ImageFormat.Gif,
".bmp" => ImageFormat.Bmp,
_ => ImageFormat.Jpeg
};
}
private int ParseInt(string value, int defaultValue)
{
return int.TryParse(value, out int result) && result > 0 ? result : defaultValue;
}
private string GenerateETag(string filePath)
{
var fi = new FileInfo(filePath);
return $"\"{fi.LastWriteTimeUtc.Ticks}-{fi.Length}\"";
}
private void ReturnError(HttpContext context, string message)
{
context.Response.StatusCode = 404;
context.Response.ContentType = "text/plain; charset=utf-8";
context.Response.Write(message);
}
public bool IsReusable => false; // 每次請求新實例,避免狀態衝突
}
<configuration>
<appSettings>
<add key="ImageRootPath" value="D:\WebImages\" />
</appSettings>
<system.webServer>
<handlers>
<add name="ImageThumbnail" verb="GET" path="Thumb.ashx" type="ImageThumbnailHandler" preCondition="integratedMode" />
</handlers>
</system.webServer>
</configuration>
<!-- 等比縮放,寬度 200px -->
<img src="Thumb.ashx?img=product1.jpg&w=200" alt="產品圖" />
<!-- 固定 200x200 裁切 -->
<img src="Thumb.ashx?img=product1.jpg&w=200&h=200&mode=crop" alt="方形縮圖" />
<!-- 不縮放,原圖輸出 -->
<img src="Thumb.ashx?img=product1.jpg" alt="原圖" />
<asp:GridView ID="gvProducts" runat="server" AutoGenerateColumns="false">
<Columns>
<asp:BoundField DataField="ProductName" HeaderText="產品名稱" />
<asp:ImageField
DataImageUrlField="ImageFileName"
DataImageUrlFormatString="Thumb.ashx?img={0}&w=100&h=100&mode=fit"
HeaderText="縮圖"
ControlStyle-Width="100"
ControlStyle-Height="100" />
<asp:BoundField DataField="Price" HeaderText="價格" DataFormatString="{0:C}" />
</Columns>
</asp:GridView>
<asp:Repeater ID="rptImages" runat="server">
<ItemTemplate>
<div class="image-item">
<img src='Thumb.ashx?img=<%# Eval("FileName") %>&w=150&mode=fit'
alt='<%# Eval("Title") %>' loading="lazy" />
<span><%# Eval("Title") %></span>
</div>
</ItemTemplate>
</asp:Repeater>
| 層級 | 機制 | 實作 |
|---|---|---|
| 瀏覽器 | Cache-Control / ETag / Last-Modified | ASHX 中設定 `SetMaxAge`、`SetETag`、回應 304 |
| 伺服器記憶體 | MemoryCache / OutputCache | `context.Response.Cache.SetCacheability(HttpCacheability.ServerAndClient)` |
| 分散式 | Redis / CDN | 前置 CDN (Cloudflare, Azure CDN) 快取 ASHX 輸出 |
| 檔案系統 | 預生成縮圖檔 | 首次請求生成並儲存至 `Thumbs/` 目錄,後續直接靜態檔服務 |
| 方案 | 優點 | 適用場景 |
|---|---|---|
| ImageResizer / ImageSharp (NuGet) | 功能豐富、管線化、支援 WebP/AVIF、效能優異 | 新專案、高流量、需進階處理 (水印、濾鏡、面部裁切) |
| Azure Blob Storage + CDN | 無伺服器、全球分發、自動縮圖 (Event Grid + Function) | 雲端原生架構 |
| 前端 Canvas 縮放 | 零後端成本、即時預覽 | 上傳前預覽、簡單縮圖需求 |
| ASP.NET Core Middleware | 管線整合、依賴注入、支援 IImageEncoder | ASP.NET Core 專案 |
// NuGet: Install-Package SixLabors.ImageSharp
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Jpeg;
public class ModernThumbnailHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string file = context.Request["img"];
int w = int.TryParse(context.Request["w"], out var wi) ? wi : 150;
string path = Path.Combine(ImageRootPath, Path.GetFileName(file));
if (!File.Exists(path)) { context.Response.StatusCode = 404; return; }
using (var image = Image.Load(path))
{
image.Mutate(x => x.Resize(new ResizeOptions
{
Mode = ResizeMode.Max, // 等比縮放
Size = new Size(w, w)
}));
context.Response.ContentType = "image/jpeg";
image.Save(context.Response.OutputStream, new JpegEncoder { Quality = 85 });
}
}
public bool IsReusable => true; // ImageSharp 執行緒安全
}
ASHX 動態縮圖是 ASP.NET 處理圖片的經典模式:參數驅動 → 安全路徑解析 → 高品質繪圖 → 輸出流 + 快取標頭。核心優勢在於「零磁碟佔用、任意尺寸、即時生成」。現代專案強烈建議遷移至 ImageSharp (跨平台、無 GDI+ 依賴、記憶體安全、支援新世代格式) 或雲端儲存 + CDN 架構。