介绍
企业经常需要一次性为成千上万的文件——合同、演示文稿、发票——添加品牌或保护标记。手动操作意味着要打开每个文档,插入徽标或保密声明,然后再次保存。这样不仅耗时,还会引入人为错误,并导致 重复水印 或漏掉文件的风险。
GroupDocs.Watermark for .NET 通过统一的 API 解决了这一问题,支持 PDF、DOCX、PPTX、XLSX 以及常见图像格式。示例项目提供了 四种文档类型(DOCX、PDF、XLSX、PPTX),因此每种流水线模式都在真实的文件格式上运行。在本教程中,我们将逐步演示完整的 批量水印流水线,包括:
- 加载许可证(或回退到评估模式)。
- 扫描文件夹并仅过滤库能够处理的格式。
- 为每个文档应用平铺的 文本 水印。
- 应用带有自定义不透明度和旋转角度的平铺 徽标 水印。
- 仅在不存在时 添加水印(幂等处理)。
- 查找并替换 过时的公司徽标为新徽标。
完成后,你将拥有一个可直接运行的解决方案,可嵌入任何 .NET 项目。
为什么批量水印很重要
- 可扩展性 – 只需一个循环即可处理数十或数千个文件。
- 一致性 – 对每个文档应用相同的视觉风格,消除品牌漂移。
- 安全性 – 幂等逻辑防止在重新运行流水线时出现重复水印。
- 面向未来 – 徽标替换代码让你在不手动处理每个文件的情况下完成品牌更新。
前置条件
- .NET 6.0 或更高版本。
- GroupDocs.Watermark NuGet 包(
dotnet add package GroupDocs.Watermark)。 - 一个 许可证文件(临时或永久)。如果缺少文件,示例将在评估模式下运行。
- 磁盘上两个文件夹:
InputFolder– 存放源文档。OutputFolder– 写入加水印后的副本。
第一步 – 加载许可证
库需要许可证才能在没有评估限制的情况下运行。下面的代码片段尝试加载许可证文件,如果未找到则静默回退。
try
{
var license = new License();
license.SetLicense(LicensePath);
Console.WriteLine("License applied successfully.");
}
catch
{
Console.WriteLine("Warning: License not found. Running in evaluation mode.");
}
关键点: LicensePath 变量应指向你的 .lic 文件。如果文件缺失,代码仍会继续执行,这对快速测试非常有帮助。
第二步 – 发现受支持的文件
GroupDocs.Watermark 只能处理特定的扩展名。下面的辅助方法扫描文件夹,通过 FileType.GetSupportedFileTypes() 构建受支持扩展名的哈希集合,并仅返回匹配的文件。
if (!Directory.Exists(folderPath))
{
Console.WriteLine($"Folder not found: {folderPath}");
return new List<string>();
}
var supportedExtensions = FileType.GetSupportedFileTypes()
.Select(ft => ft.Extension.ToLowerInvariant())
.ToHashSet();
var supportedFiles = Directory.GetFiles(folderPath, "*.*", SearchOption.AllDirectories)
.Where(f => supportedExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.ToList();
Console.WriteLine($"Found {supportedFiles.Count} supported file(s) in '{folderPath}'.");
return supportedFiles;
关键点: 该方法保证后续的水印循环永远不会遇到不受支持的格式,否则会抛出运行时异常。
第三步 – 应用平铺文本水印
下面的代码创建一个红色、半透明的 “CONFIDENTIAL” 水印,旋转 ‑30°,并使用 TileOptions 在每页上平铺。
Directory.CreateDirectory(outputFolder);
int processed = 0, failed = 0;
foreach (var filePath in files)
{
try
{
using var watermarker = new Watermarker(filePath);
var watermark = new TextWatermark(watermarkText,
new Font("Arial", 19, FontStyle.Bold))
{
ForegroundColor = Color.Red,
Opacity = 0.3,
RotateAngle = -30,
TileOptions = new TileOptions
{
LineSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 12
},
WatermarkSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 10
}
}
};
watermarker.Add(watermark);
var outPath = Path.Combine(outputFolder, Path.GetFileName(filePath));
watermarker.Save(outPath);
processed++;
Console.WriteLine($"[OK] {Path.GetFileName(filePath)}");
}
catch (Exception ex)
{
failed++;
Console.WriteLine($"[FAIL] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"Text watermark: {processed} processed, {failed} failed");
关键点:
TileOptions生成砖块式图案,使水印难以在不影响底层内容的情况下被移除。- 由于
Watermarker抽象了文件格式,同一段代码可用于 PDF、Word、电子表格和图像。
第四步 – 应用平铺徽标水印
如果更倾向于使用视觉品牌标记,可将文本水印替换为图像。下面的代码先检查徽标文件是否存在,然后以 40% 不透明度和平铺‑30° 旋转方式添加。
if (!File.Exists(logoPath))
{
Console.WriteLine($"Logo not found: {logoPath}. Skipping image mode.");
return;
}
Directory.CreateDirectory(outputFolder);
int processed = 0, failed = 0;
foreach (var filePath in files)
{
try
{
using var watermarker = new Watermarker(filePath);
using var watermark = new ImageWatermark(logoPath)
{
Opacity = 0.4,
RotateAngle = -30,
TileOptions = new TileOptions
{
LineSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 15
},
WatermarkSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 12
}
}
};
watermarker.Add(watermark);
var outPath = Path.Combine(outputFolder, Path.GetFileName(filePath));
watermarker.Save(outPath);
processed++;
Console.WriteLine($"[OK] {Path.GetFileName(filePath)} - logo applied");
}
catch (Exception ex)
{
failed++;
Console.WriteLine($"[FAIL] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"Logo watermark: {processed} processed, {failed} failed");
关键点:
- 与文本相同的
TileOptions逻辑同样适用于图像,确保所有页面外观一致。 Opacity让底层内容保持可读,同时仍能展示品牌。
第五步 – 幂等水印(跳过已有标记)
多次运行流水线时不应在同一文档上叠加水印。下面的代码在添加新水印前搜索 完全相同 的水印文本。
Directory.CreateDirectory(outputFolder);
int applied = 0, skipped = 0, failed = 0;
foreach (var filePath in files)
{
try
{
using var watermarker = new Watermarker(filePath);
var criteria = new TextSearchCriteria(watermarkText, false);
var existing = watermarker.Search(criteria);
if (existing.Count > 0)
{
skipped++;
Console.WriteLine($"[SKIP] {Path.GetFileName(filePath)} – already contains watermark");
continue;
}
var watermark = new TextWatermark(watermarkText,
new Font("Arial", 19, FontStyle.Bold))
{
ForegroundColor = Color.Red,
Opacity = 0.3,
RotateAngle = -30,
TileOptions = new TileOptions
{
LineSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 12
},
WatermarkSpacing = new MeasureValue
{
MeasureType = TileMeasureType.Percent,
Value = 10
}
}
};
watermarker.Add(watermark);
var outPath = Path.Combine(outputFolder, Path.GetFileName(filePath));
watermarker.Save(outPath);
applied++;
Console.WriteLine($"[OK] {Path.GetFileName(filePath)} – watermark applied");
}
catch (Exception ex)
{
failed++;
Console.WriteLine($"[FAIL] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"Smart batch: {applied} applied, {skipped} skipped, {failed} failed");
关键点: TextSearchCriteria 的第二个参数设为 false(不区分大小写),确保仅在文档已经包含我们准备添加的 完全相同 水印时才跳过。
第六步 – 替换文件夹中的旧徽标
公司改品牌时,可能需要将所有旧徽标换成新徽标。下面的代码结合两种图像搜索策略——DCT‑hash 用于精确匹配,颜色直方图用于容差——然后覆盖每个匹配项的图像数据。
if (!File.Exists(oldLogoPath) || !File.Exists(newLogoPath))
{
Console.WriteLine("Old or new logo file missing – aborting replacement.");
return;
}
Directory.CreateDirectory(outputFolder);
byte[] newLogoData = File.ReadAllBytes(newLogoPath);
int replaced = 0, notFound = 0;
var settings = new WatermarkerSettings
{
SearchableObjects = new SearchableObjects
{
PdfSearchableObjects = PdfSearchableObjects.All
}
};
foreach (var filePath in files)
{
try
{
using var watermarker = new Watermarker(filePath, settings);
var dct = new ImageDctHashSearchCriteria(oldLogoPath) { MaxDifference = 0.4 };
var hist = new ImageColorHistogramSearchCriteria(oldLogoPath) { MaxDifference = 0.5 };
var criteria = dct.Or(hist);
var found = watermarker.Search(criteria);
if (found.Count == 0)
{
notFound++;
Console.WriteLine($"[--] {Path.GetFileName(filePath)} – old logo not found");
continue;
}
foreach (PossibleWatermark wm in found)
{
try
{
wm.ImageData = newLogoData;
}
catch
{
// Some watermark types cannot be overwritten – ignore.
}
}
var outPath = Path.Combine(outputFolder, Path.GetFileName(filePath));
watermarker.Save(outPath);
replaced++;
Console.WriteLine($"[OK] {Path.GetFileName(filePath)} – {found.Count} logo(s) replaced");
}
catch (Exception ex)
{
Console.WriteLine($"[FAIL] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"Logo replacement: {replaced} updated, {notFound} without old logo");
关键点:
WatermarkerSettings中的PdfSearchableObjects.All使搜索能够看到作为 PDF 工件存储的徽标。- 将 DCT‑hash 与颜色直方图相结合,可捕获 Office 中的矢量徽标以及 PDF 中的栅格化版本。
最佳实践与技巧
- 一次性创建输出文件夹(
Directory.CreateDirectory)——该方法是幂等的,可避免竞争条件。 - 记录进度——每一步的控制台输出让你轻松看到哪些文件成功、哪些失败。
- 根据品牌指南调节
Opacity与RotateAngle;通常 0.3–0.5 的不透明度即可兼顾可见性与不侵扰。 - 对任何重复任务使用幂等智能批处理(例如每晚的品牌更新)。
- 在全量运行前先在小样本上测试徽标替换,确保搜索条件调校正确。
常见问题排查
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| 没有文件被处理 | ScanFolderForSupportedFiles 返回了空列表 |
检查 InputFolder 路径,并确认文件夹中包含受支持的格式(PDF、DOCX、PPTX、XLSX、PNG、JPG 等) |
| 水印不可见 | 不透明度设置过低或颜色与背景融合 | 提高 Opacity(例如 0.5)或将 ForegroundColor 改为对比度更高的颜色 |
| PDF 中未找到徽标进行替换 | 徽标以内容流绘制操作方式添加(不可搜索) | 添加徽标时使用 PdfArtifactWatermarkOptions,使其成为可搜索的工件 |
在 Linux 上出现 System.Drawing.Common 异常 |
缺少本机 GDI+ 库 | 在目标 Linux 机器上安装 libgdiplus,或在 .csproj 中启用 Unix 支持 (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />) |
结论
现在你拥有一个 完整、可投入生产的流水线,能够:
- 为库加载许可证。
- 自动检测受支持的文档。
- 应用平铺文本或徽标水印。
- 安全地多次运行而不会产生重复水印。
- 在整个文件夹范围内替换旧的企业徽标。
这些构件可以自由组合,满足 .NET 环境中任何品牌或文档保护工作流的需求。