| 維度 | 上傳時加水印 (靜態) | 請求時動態加水印 (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; }
}
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 同方案一
}
// 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. `Graphics.DrawImage` + `ColorMatrix` 控制透明度。
2. `ImageAttributes` 避免修改原水印圖片。
3. **嚴格資源釋放** (`using`) 防 GDI+ 洩漏。
4. **完善快取策略** (ETag, Max-Age, CDN) 抵消動態處理成本。
新架構請直接拥抱 ImageSharp,獲得跨平台、高效能、安全的現代圖片處理體驗。