介绍

企业经常需要一次性为成千上万的文件——合同、演示文稿、发票——添加品牌或保护标记。手动操作意味着要打开每个文档,插入徽标或保密声明,然后再次保存。这样不仅耗时,还会引入人为错误,并导致 重复水印 或漏掉文件的风险。

GroupDocs.Watermark for .NET 通过统一的 API 解决了这一问题,支持 PDF、DOCX、PPTX、XLSX 以及常见图像格式。示例项目提供了 四种文档类型(DOCX、PDF、XLSX、PPTX),因此每种流水线模式都在真实的文件格式上运行。在本教程中,我们将逐步演示完整的 批量水印流水线,包括:

  1. 加载许可证(或回退到评估模式)。
  2. 扫描文件夹并仅过滤库能够处理的格式。
  3. 为每个文档应用平铺的 文本 水印。
  4. 应用带有自定义不透明度和旋转角度的平铺 徽标 水印。
  5. 仅在不存在时 添加水印(幂等处理)。
  6. 查找并替换 过时的公司徽标为新徽标。

完成后,你将拥有一个可直接运行的解决方案,可嵌入任何 .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)——该方法是幂等的,可避免竞争条件。
  • 记录进度——每一步的控制台输出让你轻松看到哪些文件成功、哪些失败。
  • 根据品牌指南调节 OpacityRotateAngle;通常 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 环境中任何品牌或文档保护工作流的需求。

其他资源