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

C# 圖片加浮水印完整實作:文字繪製、格式偵測與記憶體管理

     編輯:關於C語言

本文提供一個可直接用於生產環境的 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);
    }
}

使用範例

1. ASP.NET Core 控制器上傳即加浮水印

[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}");
}

2. 批次處理資料夾所有圖片

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");
});

效能與記憶體管理重點

  • 全程 using 區塊:Graphics、Brush、Bitmap、FileStream 皆正確 Dispose,避免 GDI+ 物件洩漏。
  • Format32bppArgb:確保 Alpha 通道正確混色,避免透明度異常。
  • 量測一次、繪製多次:MeasureString 只呼叫一次,描邊以位移繪製 4 次取代 GraphicsPath,效能更好。
  • Parallel.ForEach:批次處理時自動利用多核心,但注意 GDI+ 非完全執行緒安全,大量併發建議改用 ImageSharp 或 SkiaSharp 純管理式庫。

常見問題 FAQ

Q: 文字模糊怎麼辦?
A: 設定 g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit 並確保 Font 大小為整數像素。
Q: PNG 透明背景變黑?
A: 原圖若為 Indexed PixelFormat,請先轉 Format32bppArgb 再繪製(本範例已在建立 Bitmap 時指定)。
Q: 想要斜向重複浮水印?
A: 改用 TextureBrush 搭配 RotateTransform 在迴圈中 FillRectangle,或改用 ImageSharp 的 DrawText 重複繪製。
Q: .NET Core / Linux 環境報 GDI+ 錯誤?
A: System.Drawing.Common 僅支援 Windows。跨平台請改用 ImageSharp 或 SkiaSharp,API 設計相似但無原生相依。

結語

這份 WatermarkHelper 涵蓋從「檔案格式偵測」到「高品質文字繪製」再到「正確資源釋放」的完整流程,可直接納入專案共用程式庫。若專案已遷移至 .NET 6+ / 跨平台,建議同步規劃遷移至 ImageSharp,以獲得一致的跨平台行為與更佳的記憶體安全性。

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