1. 项目概述
在.NET Core 8.0环境下使用SkiaSharp实现图片水印功能,是一个兼具实用性和技术深度的开发需求。作为一名长期从事.NET开发的工程师,我发现图片水印功能在内容管理系统、电商平台、社交应用等场景中几乎是标配功能。传统的水印实现方式要么功能单一,要么性能不佳,而SkiaSharp这个跨平台的2D图形库为我们提供了完美的解决方案。
SkiaSharp是Google Skia图形库的.NET绑定,它继承了Skia的高性能和跨平台特性,特别适合处理图像相关的操作。与System.Drawing相比,SkiaSharp不仅支持.NET Core/5/6/7/8,还能在Linux/macOS上完美运行,避免了System.Drawing在非Windows平台上的兼容性问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与基础配置
2.1 创建.NET Core 8.0项目
首先,我们需要创建一个新的.NET Core 8.0项目。打开终端或命令提示符,执行以下命令:
bash复制dotnet new console -n ImageWatermarkDemo
cd ImageWatermarkDemo
2.2 添加SkiaSharp依赖
SkiaSharp有几个不同的NuGet包,我们需要根据需求选择合适的包。对于基本的水印功能,安装以下两个包即可:
bash复制dotnet add package SkiaSharp
dotnet add package SkiaSharp.NativeAssets.Linux # 如果需要在Linux上运行
注意:如果你计划在Windows、Linux和macOS多平台上运行,建议同时安装所有平台的NativeAssets包,这样可以确保在不同环境下都能正常工作。
2.3 基础代码结构
创建一个简单的类来封装水印功能:
csharp复制using SkiaSharp;
using System.IO;
public class ImageWatermarker
{
private readonly SKBitmap _originalImage;
public ImageWatermarker(Stream imageStream)
{
_originalImage = SKBitmap.Decode(imageStream);
}
// 其他方法将在后续章节实现
}
3. 核心水印功能实现
3.1 水印位置计算
水印位置是功能的核心之一。我们需要实现五种标准位置:左上角、右上角、左下角、右下角和居中。以下是位置计算的数学原理:
csharp复制public enum WatermarkPosition
{
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Center
}
private SKPoint CalculatePosition(SKSize watermarkSize, WatermarkPosition position)
{
var imageWidth = _originalImage.Width;
var imageHeight = _originalImage.Height;
return position switch
{
WatermarkPosition.TopLeft => new SKPoint(10, 10), // 留10像素边距
WatermarkPosition.TopRight => new SKPoint(imageWidth - watermarkSize.Width - 10, 10),
WatermarkPosition.BottomLeft => new SKPoint(10, imageHeight - watermarkSize.Height - 10),
WatermarkPosition.BottomRight => new SKPoint(
imageWidth - watermarkSize.Width - 10,
imageHeight - watermarkSize.Height - 10),
WatermarkPosition.Center => new SKPoint(
(imageWidth - watermarkSize.Width) / 2,
(imageHeight - watermarkSize.Height) / 2),
_ => throw new ArgumentOutOfRangeException(nameof(position))
};
}
3.2 透明度处理
透明度是水印效果的另一个关键因素。SkiaSharp使用SKColor结构表示颜色,其中包含Alpha通道(透明度)。我们可以这样处理透明度:
csharp复制private SKColor ApplyTransparency(SKColor originalColor, float opacity)
{
// opacity范围0-1,1表示完全不透明
byte alpha = (byte)(originalColor.Alpha * opacity);
return new SKColor(originalColor.Red, originalColor.Green, originalColor.Blue, alpha);
}
3.3 完整水印添加方法
结合位置和透明度,下面是完整的添加水印方法:
csharp复制public void AddTextWatermark(string text, WatermarkPosition position, float opacity,
string fontFamily = "Arial", float fontSize = 24, SKColor? color = null)
{
color ??= SKColors.Black;
using var surface = SKSurface.Create(new SKImageInfo(_originalImage.Width, _originalImage.Height));
using var canvas = surface.Canvas;
// 绘制原始图像
canvas.DrawBitmap(_originalImage, 0, 0);
// 设置水印文本样式
var paint = new SKPaint
{
Color = ApplyTransparency(color.Value, opacity),
TextSize = fontSize,
Typeface = SKTypeface.FromFamilyName(fontFamily),
IsAntialias = true
};
// 计算文本尺寸和位置
var textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
var textSize = new SKSize(textBounds.Width, textBounds.Height);
var textLocation = CalculatePosition(textSize, position);
// 绘制水印文本
canvas.DrawText(text, textLocation.X, textLocation.Y + textBounds.Height, paint);
// 更新原始图像
_originalImage = surface.Snapshot().ToBitmap();
}
4. 高级功能扩展
4.1 图片水印支持
除了文字水印,我们经常需要添加Logo图片作为水印。实现方法如下:
csharp复制public void AddImageWatermark(Stream watermarkStream, WatermarkPosition position,
float opacity, float scale = 1.0f)
{
using var watermarkBitmap = SKBitmap.Decode(watermarkStream);
// 计算缩放后的尺寸
var scaledWidth = (int)(watermarkBitmap.Width * scale);
var scaledHeight = (int)(watermarkBitmap.Height * scale);
// 创建缩放后的水印图像
using var scaledWatermark = new SKBitmap(scaledWidth, scaledHeight);
using (var canvas = new SKCanvas(scaledWatermark))
{
canvas.SetMatrix(SKMatrix.CreateScale(scale, scale));
canvas.DrawBitmap(watermarkBitmap, 0, 0);
}
// 计算位置
var watermarkSize = new SKSize(scaledWidth, scaledHeight);
var watermarkLocation = CalculatePosition(watermarkSize, position);
// 绘制到原图
using var surface = SKSurface.Create(new SKImageInfo(_originalImage.Width, _originalImage.Height));
using var canvas = surface.Canvas;
canvas.DrawBitmap(_originalImage, 0, 0);
// 设置透明度
using var paint = new SKPaint { Color = SKColors.White.WithAlpha((byte)(255 * opacity)) };
canvas.DrawBitmap(scaledWatermark, watermarkLocation.X, watermarkLocation.Y, paint);
_originalImage = surface.Snapshot().ToBitmap();
}
4.2 多重水印与平铺水印
有时候我们需要在图片上添加多个水印或者平铺水印:
csharp复制public void AddTiledWatermark(string text, float opacity, int spacing = 100,
float fontSize = 24, SKColor? color = null)
{
color ??= SKColors.Black;
using var surface = SKSurface.Create(new SKImageInfo(_originalImage.Width, _originalImage.Height));
using var canvas = surface.Canvas;
canvas.DrawBitmap(_originalImage, 0, 0);
var paint = new SKPaint
{
Color = ApplyTransparency(color.Value, opacity),
TextSize = fontSize,
Typeface = SKTypeface.FromFamilyName("Arial"),
IsAntialias = true
};
var textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
var textWidth = textBounds.Width;
var textHeight = textBounds.Height;
// 计算平铺的行列数
int cols = (int)Math.Ceiling(_originalImage.Width / (textWidth + spacing)) + 1;
int rows = (int)Math.Ceiling(_originalImage.Height / (textHeight + spacing)) + 1;
// 绘制平铺水印
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
float x = col * (textWidth + spacing);
float y = row * (textHeight + spacing);
// 交错排列,使视觉效果更好
if (row % 2 == 0)
x += textWidth / 2;
canvas.DrawText(text, x, y + textHeight, paint);
}
}
_originalImage = surface.Snapshot().ToBitmap();
}
4.3 旋转水印效果
为了增加水印的视觉效果和防去除能力,可以添加旋转功能:
csharp复制public void AddRotatedWatermark(string text, WatermarkPosition position,
float opacity, float angle, float fontSize = 24)
{
using var surface = SKSurface.Create(new SKImageInfo(_originalImage.Width, _originalImage.Height));
using var canvas = surface.Canvas;
canvas.DrawBitmap(_originalImage, 0, 0);
var paint = new SKPaint
{
Color = ApplyTransparency(SKColors.Black, opacity),
TextSize = fontSize,
Typeface = SKTypeface.FromFamilyName("Arial"),
IsAntialias = true
};
var textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
// 计算旋转中心点
var center = CalculatePosition(new SKSize(textBounds.Width, textBounds.Height), position);
center.Y += textBounds.Height / 2; // 调整Y坐标
// 保存当前画布状态
canvas.Save();
// 旋转画布
canvas.RotateDegrees(angle, center.X, center.Y);
// 绘制文本
canvas.DrawText(text, center.X - textBounds.Width / 2, center.Y + textBounds.Height / 2, paint);
// 恢复画布状态
canvas.Restore();
_originalImage = surface.Snapshot().ToBitmap();
}
5. 性能优化与最佳实践
5.1 对象重用与资源释放
SkiaSharp中的许多对象都实现了IDisposable接口,正确处理这些资源对性能至关重要:
csharp复制public class ImageWatermarker : IDisposable
{
private SKBitmap _originalImage;
private bool _disposed;
// ... 其他代码 ...
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_originalImage?.Dispose();
}
_disposed = true;
}
}
~ImageWatermarker()
{
Dispose(false);
}
}
5.2 批量处理优化
当需要处理大量图片时,可以重用SKPaint和其他对象来提高性能:
csharp复制public void ProcessBatch(IEnumerable<string> imagePaths, Action<SKBitmap> processAction)
{
// 创建可重用的对象
using var paint = new SKPaint();
// 配置paint...
foreach (var path in imagePaths)
{
using var stream = File.OpenRead(path);
using var bitmap = SKBitmap.Decode(stream);
// 应用处理
processAction(bitmap);
// 保存处理后的图片
using var outputStream = File.Create(Path.ChangeExtension(path, ".watermarked.jpg"));
bitmap.Encode(outputStream, SKEncodedImageFormat.Jpeg, 90);
}
}
5.3 异步处理支持
对于大型图片或批量处理,使用异步方法可以提升响应性:
csharp复制public async Task<Stream> AddWatermarkAsync(Stream imageStream, string watermarkText,
WatermarkPosition position, float opacity)
{
return await Task.Run(() =>
{
using var bitmap = SKBitmap.Decode(imageStream);
using var surface = SKSurface.Create(new SKImageInfo(bitmap.Width, bitmap.Height));
using var canvas = surface.Canvas;
canvas.DrawBitmap(bitmap, 0, 0);
// 添加水印...
var resultImage = surface.Snapshot();
var resultStream = new MemoryStream();
resultImage.Encode(resultStream, SKEncodedImageFormat.Png, 100);
resultStream.Position = 0;
return resultStream;
});
}
6. 常见问题与解决方案
6.1 字体渲染问题
在不同平台上,字体可用性可能不同。解决方案:
- 嵌入字体文件:
csharp复制var typeface = SKTypeface.FromFile("path/to/font.ttf");
- 使用系统默认字体:
csharp复制var typeface = SKTypeface.FromFamilyName(null, SKFontStyle.Normal);
- 提供回退机制:
csharp复制var preferredFonts = new[] { "Arial", "Helvetica", "sans-serif" };
SKTypeface typeface = null;
foreach (var font in preferredFonts)
{
typeface = SKTypeface.FromFamilyName(font);
if (typeface != null)
break;
}
6.2 内存泄漏排查
SkiaSharp对象如果没有正确释放会导致内存泄漏。使用以下模式确保资源释放:
csharp复制using (var bitmap = SKBitmap.Decode(stream))
using (var surface = SKSurface.Create(...))
using (var canvas = surface.Canvas)
using (var paint = new SKPaint())
{
// 绘图操作
}
6.3 跨平台兼容性问题
虽然SkiaSharp是跨平台的,但仍有一些注意事项:
- Linux上需要安装必要的依赖:
bash复制sudo apt-get install libfontconfig1 libfreetype6
- macOS上可能需要设置字体目录:
csharp复制SKFontManager.Default.FontProviders.Insert(0,
new SKFontManager(SKFontManager.Default.FontFamilies,
new[] { "/Library/Fonts", "/System/Library/Fonts" }));
- 图片编解码器支持可能因平台而异,建议测试目标平台的支持情况。
7. 实际应用示例
7.1 Web API集成示例
创建一个ASP.NET Core Web API来提供水印服务:
csharp复制[ApiController]
[Route("api/watermark")]
public class WatermarkController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> AddWatermark(
[FromForm] IFormFile image,
[FromForm] string watermarkText,
[FromForm] WatermarkPosition position = WatermarkPosition.BottomRight,
[FromForm] float opacity = 0.7f)
{
if (image == null || image.Length == 0)
return BadRequest("No image uploaded");
using var memoryStream = new MemoryStream();
await image.CopyToAsync(memoryStream);
memoryStream.Position = 0;
using var watermarker = new ImageWatermarker(memoryStream);
watermarker.AddTextWatermark(watermarkText, position, opacity);
var resultStream = new MemoryStream();
watermarker.Save(resultStream, SKEncodedImageFormat.Jpeg, 90);
resultStream.Position = 0;
return File(resultStream, "image/jpeg", $"watermarked_{image.FileName}");
}
}
7.2 控制台批量处理示例
批量处理文件夹中的所有图片:
csharp复制public static void BatchProcessFolder(string inputFolder, string outputFolder,
string watermarkText)
{
if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder);
var imageFiles = Directory.GetFiles(inputFolder, "*.jpg")
.Concat(Directory.GetFiles(inputFolder, "*.png"));
foreach (var file in imageFiles)
{
using var stream = File.OpenRead(file);
using var watermarker = new ImageWatermarker(stream);
watermarker.AddTextWatermark(watermarkText,
WatermarkPosition.BottomRight, 0.5f);
var outputPath = Path.Combine(outputFolder, Path.GetFileName(file));
using var outputStream = File.Create(outputPath);
watermarker.Save(outputStream, SKEncodedImageFormat.Jpeg, 85);
}
}
7.3 高级配置示例
支持更多自定义选项的完整示例:
csharp复制public void AddCustomWatermark(
string text,
WatermarkPosition position,
float opacity,
string fontFamily = "Arial",
float fontSize = 24,
SKColor? color = null,
bool shadow = false,
float rotation = 0,
float padding = 10)
{
color ??= SKColors.Black;
using var surface = SKSurface.Create(new SKImageInfo(_originalImage.Width, _originalImage.Height));
using var canvas = surface.Canvas;
canvas.DrawBitmap(_originalImage, 0, 0);
// 主文本绘制
var paint = new SKPaint
{
Color = ApplyTransparency(color.Value, opacity),
TextSize = fontSize,
Typeface = SKTypeface.FromFamilyName(fontFamily),
IsAntialias = true,
TextAlign = SKTextAlign.Center
};
// 阴影效果
if (shadow)
{
var shadowPaint = new SKPaint
{
Color = ApplyTransparency(SKColors.Black, opacity * 0.7f),
TextSize = fontSize,
Typeface = paint.Typeface,
IsAntialias = true,
TextAlign = SKTextAlign.Center
};
// 绘制阴影
// ...
}
// 计算文本尺寸和位置
var textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
var textSize = new SKSize(textBounds.Width, textBounds.Height);
var textLocation = CalculatePosition(textSize, position, padding);
// 旋转处理
if (rotation != 0)
{
canvas.Save();
canvas.RotateDegrees(rotation,
textLocation.X + textBounds.Width / 2,
textLocation.Y + textBounds.Height / 2);
}
// 绘制文本
canvas.DrawText(text,
textLocation.X + textBounds.Width / 2,
textLocation.Y + textBounds.Height,
paint);
if (rotation != 0)
canvas.Restore();
_originalImage = surface.Snapshot().ToBitmap();
}
8. 测试与验证
8.1 单元测试示例
使用xUnit编写单元测试验证水印功能:
csharp复制public class WatermarkerTests
{
[Fact]
public void ShouldAddTopLeftWatermark()
{
// 准备测试图片
using var bitmap = new SKBitmap(400, 300);
using var stream = new MemoryStream();
bitmap.Encode(stream, SKEncodedImageFormat.Png, 100);
stream.Position = 0;
// 添加水印
using var watermarker = new ImageWatermarker(stream);
watermarker.AddTextWatermark("TEST", WatermarkPosition.TopLeft, 0.5f);
// 验证水印位置
using var result = watermarker.ToSKBitmap();
using var canvas = new SKCanvas(result);
var paint = new SKPaint { Color = SKColors.Red };
canvas.DrawRect(5, 5, 50, 30, paint); // 标记预期水印区域
// 保存测试结果用于人工验证
using var output = File.Create("test_top_left.png");
result.Encode(output, SKEncodedImageFormat.Png, 100);
}
[Theory]
[InlineData(0.1f)]
[InlineData(0.5f)]
[InlineData(0.9f)]
public void ShouldApplyCorrectOpacity(float opacity)
{
// 测试不同透明度值
// ...
}
}
8.2 性能测试
测试不同大小图片的水印处理时间:
csharp复制[Fact]
public void PerformanceTest()
{
var sizes = new[] { (800, 600), (1920, 1080), (3840, 2160) };
var results = new Dictionary<string, long>();
foreach (var (width, height) in sizes)
{
// 创建测试图片
using var bitmap = new SKBitmap(width, height);
using var stream = new MemoryStream();
bitmap.Encode(stream, SKEncodedImageFormat.Png, 100);
stream.Position = 0;
// 计时
var stopwatch = Stopwatch.StartNew();
using var watermarker = new ImageWatermarker(stream);
watermarker.AddTextWatermark("PERF TEST", WatermarkPosition.Center, 0.5f);
stopwatch.Stop();
results[$"{width}x{height}"] = stopwatch.ElapsedMilliseconds;
}
// 输出结果
foreach (var kvp in results)
{
Console.WriteLine($"Size: {kvp.Key}, Time: {kvp.Value}ms");
}
}
8.3 视觉验证工具
创建一个简单的WinForms应用来交互式测试水印效果:
csharp复制public class WatermarkTesterForm : Form
{
private SKBitmap _originalImage;
private PictureBox _pictureBox;
private ComboBox _positionCombo;
private TrackBar _opacityTrack;
public WatermarkTesterForm()
{
// 初始化UI控件
// ...
// 加载图片按钮
var loadButton = new Button { Text = "Load Image" };
loadButton.Click += (s, e) =>
{
using var dialog = new OpenFileDialog();
if (dialog.ShowDialog() == DialogResult.OK)
{
using var stream = File.OpenRead(dialog.FileName);
_originalImage = SKBitmap.Decode(stream);
UpdatePreview();
}
};
// 更新预览
void UpdatePreview()
{
if (_originalImage == null) return;
using var copy = _originalImage.Copy();
using var watermarker = new ImageWatermarker(copy);
var position = (WatermarkPosition)_positionCombo.SelectedIndex;
var opacity = _opacityTrack.Value / 100f;
watermarker.AddTextWatermark("Sample", position, opacity);
_pictureBox.Image = watermarker.ToBitmap();
}
// 控件事件绑定
_positionCombo.SelectedIndexChanged += (s, e) => UpdatePreview();
_opacityTrack.ValueChanged += (s, e) => UpdatePreview();
}
}
