在数字内容管理领域,图片水印是保护版权和声明来源的常见技术手段。最近我在一个企业文档管理系统项目中,需要实现灵活可配置的图片水印功能。经过技术选型,最终采用.NET Core 8.0 + SkiaSharp的方案,支持五种定位方式和透明度调节,单张图片处理耗时控制在50ms以内。
这个方案完美解决了客户对水印位置灵活性和性能的双重要求。下面我将完整分享实现过程,包括你可能遇到的字体渲染、DPI适配等实际问题。
在.NET生态中,常见的图像处理方案有:
最终选择SkiaSharp基于以下考量:
bash复制dotnet add package SkiaSharp --version 2.88.6
dotnet add package SkiaSharp.NativeAssets.Linux --version 2.88.6 # Linux部署需要
注意:生产环境部署时,Linux服务器需安装libfontconfig:
bash复制sudo apt-get install libfontconfig1
定义枚举表示五种位置:
csharp复制public enum WatermarkPosition
{
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Center
}
关键定位逻辑:
csharp复制SKPoint CalculatePosition(SKImageInfo imgInfo, SKRect textBounds,
WatermarkPosition position, float margin)
{
return position switch {
WatermarkPosition.TopLeft => new SKPoint(margin, margin),
WatermarkPosition.TopRight => new SKPoint(imgInfo.Width - textBounds.Width - margin, margin),
WatermarkPosition.BottomLeft => new SKPoint(margin, imgInfo.Height - textBounds.Height - margin),
WatermarkPosition.BottomRight => new SKPoint(imgInfo.Width - textBounds.Width - margin,
imgInfo.Height - textBounds.Height - margin),
WatermarkPosition.Center => new SKPoint(
(imgInfo.Width - textBounds.Width) / 2,
(imgInfo.Height - textBounds.Height) / 2),
_ => throw new ArgumentOutOfRangeException()
};
}
SkiaSharp使用SKColor的Alpha通道控制透明度:
csharp复制var color = new SKColor(255, 255, 255, (byte)(255 * opacity));
// opacity范围0-1.0
实测建议:
csharp复制public void AddWatermark(Stream input, Stream output,
string text, WatermarkOptions options)
{
using var original = SKBitmap.Decode(input);
using var surface = SKSurface.Create(new SKImageInfo(original.Width, original.Height));
var canvas = surface.Canvas;
// 绘制原图
canvas.DrawBitmap(original, 0, 0);
// 配置文字画笔
using var paint = new SKPaint {
Color = new SKColor(255, 255, 255, (byte)(255 * options.Opacity)),
TextSize = options.FontSize,
Typeface = SKTypeface.FromFamilyName(options.FontFamily),
IsAntialias = true
};
// 测量文字尺寸
var textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
// 计算位置
var pos = CalculatePosition(original.Info, textBounds,
options.Position, options.Margin);
// 绘制水印
canvas.DrawText(text, pos.X, pos.Y + textBounds.Height, paint);
// 输出图像
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Jpeg, 90);
data.SaveTo(output);
}
频繁创建SKTypeface会导致性能下降:
csharp复制// 在服务启动时缓存常用字体
private static readonly ConcurrentDictionary<string, SKTypeface> _fontCache = new();
public SKTypeface GetTypeface(string fontFamily)
{
return _fontCache.GetOrAdd(fontFamily, f =>
SKTypeface.FromFamilyName(fontFamily));
}
csharp复制var screenDensity = 1.0f; // 默认值
#if WINDOWS
screenDensity = Graphics.FromHwnd(IntPtr.Zero).DpiX / 96f;
#endif
// 使用时缩放字体大小
var scaledFontSize = options.FontSize * screenDensity;
通过SKTextBlob实现高效的多行绘制:
csharp复制var blob = SKTextBlob.Create(text,
new SKFont(GetTypeface(options.FontFamily), options.FontSize));
canvas.DrawText(blob, pos.X, pos.Y, paint);
典型错误:
code复制Unhandled exception. System.Exception: No fonts found
解决方案:
bash复制sudo apt-get install fonts-noto-cjk
dockerfile复制VOLUME /usr/share/fonts
必须正确释放Skia对象:
csharp复制// 错误示例 - 会泄漏内存
var image = SKImage.FromBitmap(bitmap);
// 正确做法
using var image = SKImage.FromBitmap(bitmap);
建议监控:
csharp复制Parallel.ForEach(files, file => {
using var input = File.OpenRead(file);
using var output = File.Create($"{file}_watermarked.jpg");
AddWatermark(input, output, options);
});
支持替换模板变量:
csharp复制var text = options.Text
.Replace("{date}", DateTime.Now.ToString("yyyy-MM-dd"))
.Replace("{user}", Environment.UserName);
csharp复制using var watermarkImage = SKBitmap.Decode(watermarkFile);
canvas.DrawBitmap(watermarkImage, pos.X, pos.Y, paint);
经过三个月的生产环境验证,这套方案日均处理超过50万张图片,CPU利用率保持在30%以下。最关键的收获是:一定要在Linux测试环境提前验证字体配置,这是我们踩过最深的坑。