最常用且可控的图片文字水印方案是创建新Bitmap作画布,用Graphics.DrawString绘制,设置ClearTypeGridFit抗锯齿、Color.FromArgb控制透明度、TranslateTransform+RotateTransform实现旋转,并保存为PNG以保留Alpha通道;JPEG不支持透明度,需预先合成到底色。

如何用 Graphics 在图片上绘制文字水印(最常用且可控)
直接在 Bitmap 上用 Graphics.DrawString 是 C# 实现水印最主流的方式,适合对位置、透明度、旋转有明确控制需求的场景。关键不是“能不能画”,而是“怎么画得不糊、不遮挡、不崩内存”。
常见错误是直接在原图 Graphics.FromImage 后调用 DrawString,结果文字边缘发虚、中文乱码、或处理大图时抛出 OutOfMemoryException。
- 务必创建新
Bitmap作画布(尺寸与原图一致),避免复用原图位图对象; - 设置
graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit,否则小字号中文会模糊; - 用
Color.FromArgb(128, 0, 0, 0)控制透明度(Alpha 值 0–255),别用Color.Black硬套; - 旋转水印时,先
graphics.TranslateTransform移动原点到中心,再RotateTransform,否则文字会跑出画布; - 最后记得调用
graphics.Dispose()和bitmap.Dispose(),尤其在循环批量加水印时,不释放会导致 GDI 句柄泄漏。
Image.Save 后水印消失或质量暴跌?检查编码器参数
水印明明画上了,Save 成 JPEG 后却变淡、发灰、甚至完全不见——大概率是编码器丢弃了 Alpha 通道,或压缩质量过低抹掉了半透明效果。
JPEG 不支持透明度,PNG 才保留 Color.FromArgb 设置的 Alpha。如果硬存为 JPEG,所有带透明度的文字会被强制合成到白底上,看起来像“消失了”或“变实了”。
- 要保留水印透明感,必须保存为
Png:用image.Save(path, ImageFormat.Png); - 若必须输出 JPEG,需提前将水印“合成到底色”:创建白色背景
Bitmap,先DrawImage原图,再DrawString水印,最后存 JPEG; - JPEG 质量设太低(如
EncoderParameter(Encoder.Quality, 50L))会让半透明边缘产生明显色块,建议不低于 85; - 别用
Image.Save(string)默认重载——它依赖文件扩展名推断格式,扩展名错(比如 .jpg 写成 .png)会导致编码器误判。
给 PDF 或视频加水印?System.Drawing 不适用,换方案
System.Drawing 只能处理位图,对 PDF、MP4、AVI 这类容器格式完全无效。试图用它打开 .pdf 文件会直接抛 ArgumentException: Parameter is not valid。
这不是代码写错了,是技术边界问题。强行用 FileStream 读 PDF 二进制再塞进 Bitmap 构造函数,只会得到解码失败异常。
- PDF 水印:用
iTextSharp(.NET Framework)或QuestPDF/PDFsharp(.NET 6+),通过操作页面内容流插入文字或矢量图形; - 视频水印:无法用 GDI+ 实时叠加,需调用 FFmpeg(命令行或
FFmpeg.AutoGen封装),用drawtext滤镜; - 网页预览水印:服务端生成带水印图只是权宜之计,真防下载得靠前端 Canvas 动态渲染 + 禁右键 + 片段截图干扰,但后者已超出 C# 范畴。
多线程批量加水印时 CPU 占满、GDI 资源耗尽?别共享 Graphics
有人把 Graphics 对象做成静态字段,在多个线程里反复 DrawString,结果程序跑几分钟就卡死,事件查看器里全是 Gdiplus: Out of memory 事件。
Graphics 对象不是线程安全的,且内部绑定单个设备上下文(DC)。并发调用会触发 GDI 句柄竞争,Windows 限制每个进程默认最多 10,000 个 GDI 对象,超限即失败。
- 每个水印任务必须独立创建
Bitmap → Graphics链路,用using确保及时释放; - 避免在
Parallel.ForEach外层缓存任何Graphics或Font实例; - 高频小图(如头像)可考虑对象池复用
Bitmap,但Graphics仍需 per-call 创建; - 更稳的做法是限制并发度:
ParallelOptions.MaxDegreeOfParallelism = Environment.ProcessorCount / 2,比硬扛更有效。
水印逻辑本身很简单,难的是在不同格式、线程模型、输出目标下保持稳定。最容易被忽略的其实是资源释放时机和格式语义——比如以为“存成 JPG 就行”,却没意识到 Alpha 通道根本不会被保存。











