首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET 一般處理程式 實現圖片縮略圖動態生成

     編輯:ASP.NET 基礎

前言

使用 `IHttpHandler (`.ashx`) 動態生成縮略圖,是 ASP.NET 經典且高效的圖片處理模式:無需預先生成儲存縮圖檔案,即時按需縮放、輸出至回應流,節省磁碟空間且支援任意尺寸參數。本文提供完整實作,含快取控制、錯誤處理、GridView 整合範例。

一、ASHX 縮略圖處理程式完整程式碼

<%@ 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; // 每次請求新實例,避免狀態衝突
}

二、web.config 註冊與配置

<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>

三、前端使用範例

3.1 直接 img 標籤

<!-- 等比縮放,寬度 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="原圖" />

3.2 GridView 整合 (DataImageUrlFormatString)

<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>

3.3 Repeater 靈活繫結

<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-ModifiedASHX 中設定 `SetMaxAge`、`SetETag`、回應 304
伺服器記憶體MemoryCache / OutputCache`context.Response.Cache.SetCacheability(HttpCacheability.ServerAndClient)`
分散式Redis / CDN前置 CDN (Cloudflare, Azure CDN) 快取 ASHX 輸出
檔案系統預生成縮圖檔首次請求生成並儲存至 `Thumbs/` 目錄,後續直接靜態檔服務

五、安全性加固

  • 路徑遍歷防護:`Path.GetFileName()` 移除目錄元件,嚴禁 `Path.Combine(root, userInput)` 直接拼接。
  • 檔案型別驗證:讀取檔案頭 (Magic Bytes) 確認真實圖片格式,而非只信副檔名。
  • 尺寸限制:限制 `w/h` 最大值 (如 2000px),防止惡意請求耗盡記憶體。
  • 速率限制:整合 `AspNetRateLimit` 或 IIS 動態 IP 限制。

六、現代化替代方案

方案優點適用場景
ImageResizer / ImageSharp (NuGet)功能豐富、管線化、支援 WebP/AVIF、效能優異新專案、高流量、需進階處理 (水印、濾鏡、面部裁切)
Azure Blob Storage + CDN無伺服器、全球分發、自動縮圖 (Event Grid + Function)雲端原生架構
前端 Canvas 縮放零後端成本、即時預覽上傳前預覽、簡單縮圖需求
ASP.NET Core Middleware管線整合、依賴注入、支援 IImageEncoderASP.NET Core 專案

七、ImageSharp 遷移範例 (推薦新專案)

// 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 架構。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved