本文提供一個可直接用於生產環境的 WatermarkHelper 靜態類別,涵蓋:文字浮水印繪製(支援自訂字型、顏色、位置、透明度)、檔案標頭偵測圖片格式、資源正確釋放。所有方法皆為 static、無狀態、執行緒安全,適合高併發 Web API 或批次處理作業。
| 方法 | 用途 |
|---|---|
AddTextWatermark(Image, string, WatermarkOptions) | 在記憶體中為 Image 物件加文字浮水印,回傳新 Image |
AddTextWatermark(string srcPath, string destPath, string text, WatermarkOptions) | 檔案到檔案,自動偵測格式並保存 |
IsImageFile(string path) | 讀取前 2 Bytes 判斷 JPEG/PNG/GIF/BMP |
SaveImage(Image, string path, ImageFormat) | 統一儲存邏輯,支援品質參數 |
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
public static class WatermarkHelper
{
// 文字浮水印參數物件
public class WatermarkOptions
{
public Font Font { get; set; } = new Font("微軟正黑體", 14, FontStyle.Bold);
public Color TextColor { get; set; } = Color.White;
public Color OutlineColor { get; set; } = Color.Black;
public float Opacity { get; set; } = 0.6f; // 0~1
public ContentAlignment Position { get; set; } = ContentAlignment.BottomRight;
public int Margin { get; set; } = 10; // 邊距像素
public bool DrawBackground { get; set; } = true; // 文字後方半透明矩形
public Color BackgroundColor { get; set; } = Color.FromArgb(128, 0, 0, 0);
}
// 核心:在記憶體 Image 上繪製文字
public static Image AddTextWatermark(Image source, string text, WatermarkOptions opt = null)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (string.IsNullOrEmpty(text)) return source;
opt = opt ?? new WatermarkOptions();
// 建立可寫入的 Bitmap 複本(保持原解析度與 PixelFormat)
Bitmap bmp = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb);
using (Graphics g = Graphics.FromImage(bmp))
{
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
// 繪製原圖
g.DrawImage(source, 0, 0, source.Width, source.Height);
// 量測文字大小
SizeF textSize = g.MeasureString(text, opt.Font);
float x = 0, y = 0;
switch (opt.Position)
{
case ContentAlignment.TopLeft: x = opt.Margin; y = opt.Margin; break;
case ContentAlignment.TopCenter: x = (bmp.Width - textSize.Width) / 2; y = opt.Margin; break;
case ContentAlignment.TopRight: x = bmp.Width - textSize.Width - opt.Margin; y = opt.Margin; break;
case ContentAlignment.MiddleLeft: x = opt.Margin; y = (bmp.Height - textSize.Height) / 2; break;
case ContentAlignment.MiddleCenter: x = (bmp.Width - textSize.Width) / 2; y = (bmp.Height - textSize.Height) / 2; break;
case ContentAlignment.MiddleRight: x = bmp.Width - textSize.Width - opt.Margin; y = (bmp.Height - textSize.Height) / 2; break;
case ContentAlignment.BottomLeft: x = opt.Margin; y = bmp.Height - textSize.Height - opt.Margin; break;
case ContentAlignment.BottomCenter: x = (bmp.Width - textSize.Width) / 2; y = bmp.Height - textSize.Height - opt.Margin; break;
case ContentAlignment.BottomRight: x = bmp.Width - textSize.Width - opt.Margin; y = bmp.Height - textSize.Height - opt.Margin; break;
}
RectangleF textRect = new RectangleF(x, y, textSize.Width, textSize.Height);
// 選擇性繪製背景矩形
if (opt.DrawBackground)
{
using (Brush bgBrush = new SolidBrush(opt.BackgroundColor))
g.FillRectangle(bgBrush, Rectangle.Round(textRect));
}
// 繪製文字描邊(模擬 Outline)
if (opt.OutlineColor.A > 0)
{
using (Brush outlineBrush = new SolidBrush(Color.FromArgb((int)(opt.Opacity * 255), opt.OutlineColor)))
{
float offset = 1.5f;
g.DrawString(text, opt.Font, outlineBrush, x - offset, y - offset);
g.DrawString(text, opt.Font, outlineBrush, x + offset, y - offset);
g.DrawString(text, opt.Font, outlineBrush, x - offset, y + offset);
g.DrawString(text, opt.Font, outlineBrush, x + offset, y + offset);
}
}
// 繪製主文字
using (Brush textBrush = new SolidBrush(Color.FromArgb((int)(opt.Opacity * 255), opt.TextColor)))
g.DrawString(text, opt.Font, textBrush, textRect);
}
return bmp;
}
// 檔案層級封裝
public static void AddTextWatermark(string srcPath, string destPath, string text, WatermarkOptions opt = null)
{
if (!IsImageFile(srcPath)) throw new InvalidDataException("非支援的圖片格式");
using (Image src = Image.FromFile(srcPath))
using (Image dst = AddTextWatermark(src, text, opt))
{
ImageFormat fmt = GetImageFormat(srcPath);
SaveImage(dst, destPath, fmt);
}
}
// 檔案標頭偵測(前 2 Bytes)
public static bool IsImageFile(string path)
{
try
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
using (BinaryReader br = new BinaryReader(fs))
{
byte b1 = br.ReadByte();
byte b2 = br.ReadByte();
ushort sig = (ushort)((b1 << 8) | b2);
// JPEG=0xFFD8, PNG=0x8950, GIF=0x4749, BMP=0x424D
return sig == 0xFFD8 || sig == 0x8950 || sig == 0x4749 || sig == 0x424D;
}
}
catch { return false; }
}
private static ImageFormat GetImageFormat(string path)
{
string ext = Path.GetExtension(path).ToLowerInvariant();
return ext switch
{
".jpg" or ".jpeg" => ImageFormat.Jpeg,
".png" => ImageFormat.Png,
".gif" => ImageFormat.Gif,
".bmp" => ImageFormat.Bmp,
_ => ImageFormat.Png
};
}
private static void SaveImage(Image img, string path, ImageFormat fmt)
{
var encoderParams = new EncoderParameters(1);
if (fmt == ImageFormat.Jpeg)
encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L);
ImageCodecInfo codec = ImageCodecInfo.GetImageEncoders()
.FirstOrDefault(c => c.FormatID == fmt.Guid);
if (codec != null) img.Save(path, codec, encoderParams);
else img.Save(path, fmt);
}
}
[HttpPost("upload")]
public async Task Upload(IFormFile file, string watermarkText)
{
if (file == null || file.Length == 0) return BadRequest();
using var ms = new MemoryStream();
await file.CopyToAsync(ms);
ms.Position = 0;
using var src = Image.FromStream(ms);
using var dst = WatermarkHelper.AddTextWatermark(src, watermarkText,
new WatermarkHelper.WatermarkOptions
{
Font = new Font("微軟正黑體", 20, FontStyle.Bold),
Position = ContentAlignment.Center,
Opacity = 0.4f,
TextColor = Color.White,
OutlineColor = Color.Black
});
var outMs = new MemoryStream();
dst.Save(outMs, ImageFormat.Png);
return File(outMs.ToArray(), "image/png", $"watermarked_{file.FileName}");
}
string[] files = Directory.GetFiles(@"C:\Photos", "*.*", SearchOption.AllDirectories)
.Where(f => WatermarkHelper.IsImageFile(f)).ToArray();
Parallel.ForEach(files, f =>
{
string outPath = Path.Combine(Path.GetDirectoryName(f), "WM_" + Path.GetFileName(f));
WatermarkHelper.AddTextWatermark(f, outPath, "© MyCompany 2025");
});
Graphics、Brush、Bitmap、FileStream 皆正確 Dispose,避免 GDI+ 物件洩漏。MeasureString 只呼叫一次,描邊以位移繪製 4 次取代 GraphicsPath,效能更好。ImageSharp 或 SkiaSharp 純管理式庫。g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit 並確保 Font 大小為整數像素。Format32bppArgb 再繪製(本範例已在建立 Bitmap 時指定)。TextureBrush 搭配 RotateTransform 在迴圈中 FillRectangle,或改用 ImageSharp 的 DrawText 重複繪製。System.Drawing.Common 僅支援 Windows。跨平台請改用 ImageSharp 或 SkiaSharp,API 設計相似但無原生相依。這份 WatermarkHelper 涵蓋從「檔案格式偵測」到「高品質文字繪製」再到「正確資源釋放」的完整流程,可直接納入專案共用程式庫。若專案已遷移至 .NET 6+ / 跨平台,建議同步規劃遷移至 ImageSharp,以獲得一致的跨平台行為與更佳的記憶體安全性。