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

ASP.NET 圖片水印實作全攻略:上傳時靜態加水印 vs HttpHandler 動態加水印完整代碼與選型指南

     編輯:ASP.NET 基礎

兩大策略深度比較

維度上傳時加水印 (靜態)請求時動態加水印 (HttpHandler)
原始檔保留❌ 永久修改✅ 完整保留
CPU 負載一次性 (上傳時)每次請求 (需快取緩解)
水印靈活度固定 (改水印需重處理所有圖)極高 (依用戶/權限/時間動態)
CDN/快取友善✅ 靜態檔案直傳需設定 ETag/Max-Age 配合 CDN
實作複雜度低中 (Handler註冊、資源管理、快取)
適用場景版權保護、流量大、水印固定多租戶、個性化、A/B測試、臨時授權

方案一:上傳時靜態加水印 (推薦大多數場景)

核心方法


public static Image AddWatermark(Image source, Image watermark, 
    WatermarkPosition position = WatermarkPosition.BottomRight, 
    int margin = 10, float opacity = 0.5f)
{
    using (var g = Graphics.FromImage(source))
    {
        g.CompositingQuality = CompositingQuality.HighQuality;
        g.SmoothingMode = SmoothingMode.HighQuality;
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;

        // 計算位置
        Point pt = CalculatePosition(source.Width, source.Height, 
            watermark.Width, watermark.Height, position, margin);

        // 透明度矩陣
        var attr = new ImageAttributes();
        float[][] matrix = {
            new float[] {1,0,0,0,0},
            new float[] {0,1,0,0,0},
            new float[] {0,0,1,0,0},
            new float[] {0,0,0,opacity,0},
            new float[] {0,0,0,0,1}
        };
        attr.SetColorMatrix(new ColorMatrix(matrix));

        g.DrawImage(watermark, 
            new Rectangle(pt.X, pt.Y, watermark.Width, watermark.Height),
            0, 0, watermark.Width, watermark.Height, 
            GraphicsUnit.Pixel, attr);
    }
    return source;
}

public enum WatermarkPosition { TopLeft, TopRight, BottomLeft, BottomRight, Center }

private static Point CalculatePosition(int sw, int sh, int ww, int wh, WatermarkPosition pos, int margin)
{
    switch (pos)
    {
        case WatermarkPosition.TopLeft: return new Point(margin, margin);
        case WatermarkPosition.TopRight: return new Point(sw - ww - margin, margin);
        case WatermarkPosition.BottomLeft: return new Point(margin, sh - wh - margin);
        case WatermarkPosition.Center: return new Point((sw - ww) / 2, (sh - wh) / 2);
        default: return new Point(sw - ww - margin, sh - wh - margin); // BottomRight
    }
}

上傳事件整合


protected void btnUpload_Click(object sender, EventArgs e)
{
    if (!fuImage.HasFile) return;

    string ext = Path.GetExtension(fuImage.FileName).ToLower();
    if (!new[] { ".jpg", ".jpeg", ".png", ".bmp" }.Contains(ext)) return;

    // 1. 儲存原始檔 (備份)
    string originalPath = Server.MapPath($"~/Uploads/Original/{Guid.NewGuid():N}{ext}");
    Directory.CreateDirectory(Path.GetDirectoryName(originalPath));
    fuImage.SaveAs(originalPath);

    // 2. 產生水印版
    using (Image source = Image.FromFile(originalPath))
    using (Image watermark = Image.FromFile(Server.MapPath("~/Images/watermark.png")))
    using (Image marked = AddWatermark(source, watermark))
    {
        string markedPath = Server.MapPath($"~/Uploads/Watermarked/{Path.GetFileName(originalPath)}");
        Directory.CreateDirectory(Path.GetDirectoryName(markedPath));
        marked.Save(markedPath, GetImageFormat(ext));
    }

    // 3. 資料庫存儲兩個路徑,前台顯示 Watermarked 版
    // ...
}

private ImageFormat GetImageFormat(string ext)
{
    switch (ext) { case ".png": return ImageFormat.Png; case ".bmp": return ImageFormat.Bmp; default: return ImageFormat.Jpeg; }
}

方案二:HttpHandler 動態加水印 (進階場景)

1. 處理器實作


public class DynamicWatermarkHandler : IHttpHandler
{
    public bool IsReusable => false;

    public void ProcessRequest(HttpContext context)
    {
        string file = context.Request.QueryString["file"]; // 相對路徑
        if (string.IsNullOrEmpty(file)) { context.Response.StatusCode = 400; return; }

        string physicalPath = context.Server.MapPath(file);
        if (!File.Exists(physicalPath)) { context.Response.StatusCode = 404; return; }

        // 參數解析
        string pos = context.Request.QueryString["pos"] ?? "BR";
        float opacity = float.TryParse(context.Request.QueryString["opacity"], out var o) ? Math.Clamp(o, 0, 1) : 0.5f;
        string text = context.Request.QueryString["text"]; // 文字水印模式

        using (Image source = Image.FromFile(physicalPath))
        using (Graphics g = Graphics.FromImage(source))
        {
            g.CompositingQuality = CompositingQuality.HighQuality;

            if (!string.IsNullOrEmpty(text))
            {
                // 文字水印
                using (Font font = new Font("Microsoft JhengHei", 24, FontStyle.Bold))
                using (Brush brush = new SolidBrush(Color.FromArgb((int)(opacity * 255), Color.White)))
                {
                    SizeF sz = g.MeasureString(text, font);
                    PointF pt = GetTextPosition(source.Width, source.Height, sz.Width, sz.Height, pos, 20);
                    g.DrawString(text, font, brush, pt);
                }
            }
            else
            {
                // 圖片水印
                using (Image wm = Image.FromFile(context.Server.MapPath("~/Images/watermark.png")))
                {
                    var attr = new ImageAttributes();
                    float[][] m = { new float[]{1,0,0,0,0}, new float[]{0,1,0,0,0}, new float[]{0,0,1,0,0}, new float[]{0,0,0,opacity,0}, new float[]{0,0,0,0,1} };
                    attr.SetColorMatrix(new ColorMatrix(m));
                    Point pt = GetPosition(source.Width, source.Height, wm.Width, wm.Height, pos, 10);
                    g.DrawImage(wm, new Rectangle(pt.X, pt.Y, wm.Width, wm.Height), 0, 0, wm.Width, wm.Height, GraphicsUnit.Pixel, attr);
                }
            }

            // 快取頭
            context.Response.ContentType = "image/jpeg";
            context.Response.Cache.SetCacheability(HttpCacheability.Public);
            context.Response.Cache.SetMaxAge(TimeSpan.FromDays(30));
            context.Response.Cache.SetETag($"\"{new FileInfo(physicalPath).LastWriteTimeUtc.Ticks:x}\"");

            source.Save(context.Response.OutputStream, ImageFormat.Jpeg);
        }
    }

    private PointF GetTextPosition(int sw, int sh, float tw, float th, string pos, int margin)
    {
        switch (pos.ToUpper())
        {
            case "TL": return new PointF(margin, margin);
            case "TR": return new PointF(sw - tw - margin, margin);
            case "BL": return new PointF(margin, sh - th - margin);
            case "CENTER": return new PointF((sw - tw) / 2, (sh - th) / 2);
            default: return new PointF(sw - tw - margin, sh - th - margin);
        }
    }
    // GetPosition 同方案一
}

2. 註冊 (web.config)




    
        
    

3. 前端使用








效能優化關鍵:輸出快取與 CDN

  1. ETag + Last-Modified:Handler 根據檔案修改時間產生 ETag,瀏覽器/Proxy 發送 `If-None-Match` 時回傳 304。
  2. Max-Age / Expires:靜態水印圖設定長期快取 (1 年);動態版依參數變化設定較短快取或 `no-cache`。
  3. CDN 邊緣快取:動態水印 URL 含參數,CDN 可按 Query String 快取不同版本。配合 `Vary: Accept` 頭。
  4. 記憶體洩漏防護:所有 `Image`/`Graphics`/`Font`/`Brush` 必須在 `using` 區塊中釋放,否則 GDI+ 物件洩漏導致「一般性錯誤」。

現代化替代:ImageSharp (跨平台、無 GDI+、ASP.NET Core 原生)


// NuGet: SixLabors.ImageSharp, SixLabors.ImageSharp.Drawing
public class WatermarkMiddleware
{
    private readonly RequestDelegate _next;
    public WatermarkMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Path.StartsWithSegments("/watermark"))
        {
            string file = context.Request.Query["file"];
            // ... 解析參數
            using (var fs = File.OpenRead(physicalPath))
            using (var image = await Image.LoadAsync(fs))
            using (var watermark = await Image.LoadAsync(watermarkPath))
            {
                image.Mutate(x => x.DrawImage(watermark, new Point(...), opacity));
                context.Response.ContentType = "image/jpeg";
                await image.SaveAsJpegAsync(context.Response.Body);
            }
            return;
        }
        await _next(context);
    }
}

選型決策樹

  1. 水印**永不變更**、流量大、要上 CDN → **上傳時靜態加水印**。
  2. 需**多版本水印** (不同租戶、會員等級、時間)、或**保留原圖** → **HttpHandler 動態加水印**。
  3. 新專案 / .NET Core / Linux Docker → **ImageSharp Middleware** (無 GDI+ 限制、效能更好)。
  4. 舊專案 / IIS / Windows → **HttpHandler + GDI+** (相容性最好)。

總結

水印實作無標準答案,**靜態加水印效能最好、部署最簡單**;**動態加水印靈活度最高、保護原圖**。關鍵技術點:
1. `Graphics.DrawImage` + `ColorMatrix` 控制透明度。
2. `ImageAttributes` 避免修改原水印圖片。
3. **嚴格資源釋放** (`using`) 防 GDI+ 洩漏。
4. **完善快取策略** (ETag, Max-Age, CDN) 抵消動態處理成本。
新架構請直接拥抱 ImageSharp,獲得跨平台、高效能、安全的現代圖片處理體驗。

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