在.NET Core 8.0环境下使用SkiaSharp实现图片水印功能,是一个典型的图像处理需求场景。这个项目需要解决的核心问题包括:多位置水印定位、透明度控制以及高性能处理。作为跨平台的2D图形库,SkiaSharp比System.Drawing更适用于现代.NET应用开发,特别是在Linux容器化部署场景下。
我最近在一个电商后台管理系统项目中实际应用了这套方案,每天需要处理3000+商品图片的水印添加。通过SkiaSharp的GPU加速能力,处理速度比传统方式提升了40%,同时避免了System.Drawing在Linux下的字体渲染问题。下面将完整分享实现细节和踩坑经验。
首先创建.NET Core 8.0控制台或Web项目,通过NuGet添加必要的依赖:
bash复制dotnet add package SkiaSharp
dotnet add package SkiaSharp.NativeAssets.Linux # 跨平台支持
对于Web项目,建议在Program.cs中添加服务注册:
csharp复制builder.Services.AddSingleton<IImageWatermarkService, SkiaWatermarkService>();
中文字体处理是个常见坑点。推荐将字体文件作为嵌入式资源处理:
xml复制<ItemGroup>
<EmbeddedResource Include="Resources\msyh.ttf" />
</ItemGroup>
水印位置计算是核心难点,需要处理五种定位场景。以下是位置计算的数学建模:
csharp复制public static SKPoint CalculatePosition(SKImageInfo imageInfo, SKImageInfo watermarkInfo, WatermarkPosition position, float padding = 10f)
{
return position switch
{
WatermarkPosition.TopLeft => new SKPoint(padding, padding),
WatermarkPosition.TopRight => new SKPoint(imageInfo.Width - watermarkInfo.Width - padding, padding),
WatermarkPosition.BottomLeft => new SKPoint(padding, imageInfo.Height - watermarkInfo.Height - padding),
WatermarkPosition.BottomRight => new SKPoint(imageInfo.Width - watermarkInfo.Width - padding,
imageInfo.Height - watermarkInfo.Height - padding),
WatermarkPosition.Center => new SKPoint((imageInfo.Width - watermarkInfo.Width) / 2,
(imageInfo.Height - watermarkInfo.Height) / 2),
_ => throw new ArgumentOutOfRangeException()
};
}
重要提示:padding参数建议使用相对值(如图片宽度的2%),而非固定像素值,以适应不同尺寸图片。
SkiaSharp通过SKPaint的Color属性实现透明度控制。关键代码如下:
csharp复制var paint = new SKPaint {
Color = SKColor.Parse("#FFFFFF").WithAlpha((byte)(255 * opacity)), // opacity范围0-1
IsAntialias = true,
TextSize = fontSize,
Typeface = typeface
};
透明度计算时需要注意:
以下是完整的服务类实现:
csharp复制public class SkiaWatermarkService : IImageWatermarkService, IDisposable
{
private readonly SKTypeface _typeface;
public SkiaWatermarkService()
{
// 加载嵌入式字体资源
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("YourApp.Resources.msyh.ttf");
_typeface = SKTypeface.FromStream(stream);
}
public byte[] AddTextWatermark(byte[] imageData, string watermarkText,
WatermarkPosition position, float opacity = 0.5f, int fontSize = 32)
{
using var original = SKBitmap.Decode(imageData);
using var surface = SKSurface.Create(new SKImageInfo(original.Width, original.Height));
var canvas = surface.Canvas;
canvas.DrawBitmap(original, 0, 0);
using var paint = CreateTextPaint(opacity, fontSize);
var textBounds = new SKRect();
paint.MeasureText(watermarkText, ref textBounds);
var point = CalculatePosition(original.Info,
new SKImageInfo((int)textBounds.Width, (int)textBounds.Height),
position);
canvas.DrawText(watermarkText, point.X, point.Y + textBounds.Height, paint);
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
return data.ToArray();
}
private SKPaint CreateTextPaint(float opacity, int fontSize)
{
return new SKPaint {
Color = SKColors.White.WithAlpha((byte)(255 * opacity)),
IsAntialias = true,
TextSize = fontSize,
Typeface = _typeface,
Style = SKPaintStyle.Fill,
ImageFilter = SKImageFilter.CreateDropShadow(2, 2, 2, 2, SKColors.Black.WithAlpha((byte)(255 * opacity * 0.5)))
};
}
public void Dispose() => _typeface?.Dispose();
}
除了文字水印,还可以实现图片水印:
csharp复制public byte[] AddImageWatermark(byte[] imageData, byte[] watermarkData,
WatermarkPosition position, float opacity = 0.5f, float scale = 1.0f)
{
using var original = SKBitmap.Decode(imageData);
using var watermark = SKBitmap.Decode(watermarkData);
var scaledWidth = (int)(watermark.Width * scale);
var scaledHeight = (int)(watermark.Height * scale);
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 = SKColors.White.WithAlpha((byte)(255 * opacity))
};
var point = CalculatePosition(original.Info,
new SKImageInfo(scaledWidth, scaledHeight), position);
canvas.DrawBitmap(watermark,
new SKRect(0, 0, watermark.Width, watermark.Height),
new SKRect(point.X, point.Y, point.X + scaledWidth, point.Y + scaledHeight),
paint);
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
return data.ToArray();
}
通过循环可以实现平铺水印效果:
csharp复制for (int y = 0; y < original.Height; y += watermark.Height + spacing)
{
for (int x = 0; x < original.Width; x += watermark.Width + spacing)
{
canvas.DrawText(watermarkText, x, y + textBounds.Height, paint);
}
}
现象:中文字符显示为方框
解决方案:
dockerfile复制RUN apt-get update && apt-get install -y fonts-wqy-zenhei
通过BenchmarkDotNet测试发现:
优化建议:
Docker部署注意事项:
dockerfile复制FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
RUN apt-get update && apt-get install -y libfontconfig1
csharp复制SKFontManager.Default.FontFamilies; // 检查可用字体
建议使用Xunit编写测试用例:
csharp复制public class WatermarkTests
{
private readonly SkiaWatermarkService _service = new();
[Theory]
[InlineData(WatermarkPosition.TopLeft)]
[InlineData(WatermarkPosition.Center)]
public void ShouldAddWatermarkAtPosition(WatermarkPosition position)
{
var image = File.ReadAllBytes("test.jpg");
var result = _service.AddTextWatermark(image, "测试", position);
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.NotEqual(image.Length, result.Length);
using var bitmap = SKBitmap.Decode(result);
Assert.NotNull(bitmap);
}
[Fact]
public void ShouldHandleDifferentOpacity()
{
var image = File.ReadAllBytes("test.jpg");
var result1 = _service.AddTextWatermark(image, "测试", WatermarkPosition.TopLeft, 0.3f);
var result2 = _service.AddTextWatermark(image, "测试", WatermarkPosition.TopLeft, 0.7f);
Assert.NotEqual(result1.Length, result2.Length); // PNG压缩率不同
}
}
测试要点:
在电商图片处理场景中,我总结了以下最佳实践:
水印设计原则:
性能调优:
安全考虑:
监控指标:
这套方案经过我们生产环境验证,在2核4G的Linux容器上,平均每张2000x2000图片处理时间约120ms,内存消耗稳定在50MB以内。对于更高性能需求,可以考虑使用SkiaSharp的GPU加速模式或者分布式图片处理方案。