介紹

企業常常需要一次性為成千上萬的檔案(合約、簡報、發票等)加上品牌或保護標記。手動操作意味著必須逐一開啟文件、插入標誌或機密聲明,然後再儲存。這不僅耗時,還會產生人為錯誤,並帶來 重複浮水印 或遺漏檔案的風險。

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 – 寫入加了浮水印的副本。

步驟 1 – 載入授權

程式庫需要授權才能在無評估限制的情況下執行。以下程式碼嘗試載入授權檔,若找不到則靜默回退。

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 檔案。若檔案遺失,程式仍會繼續執行,方便快速測試。


步驟 2 – 掃描支援的檔案

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;

重點: 此方法保證後續的浮水印迴圈不會遇到不支援的格式,避免執行時例外。


步驟 3 – 套用平鋪文字浮水印

以下程式碼建立紅色、半透明的 「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 產生磚牆式圖案,使浮水印難以在不影響底層內容的情況下被移除。
  • 同一段程式碼同時適用於 PDF、Word、試算表與影像,因為 Watermarker 抽象化了格式。

文字浮水印已套用於文件


步驟 4 – 套用平鋪圖示浮水印

如果想使用視覺品牌標記,只需將文字浮水印換成影像。以下程式碼先檢查圖示檔是否存在,然後以 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 讓底層內容仍可閱讀,同時顯示品牌標誌。

平鋪圖示浮水印已套用於文件


步驟 5 – 冪等浮水印(跳過已存在的標記)

多次執行管線時不應在同一文件上堆疊浮水印。此程式碼在加入新浮水印前,先搜尋是否已存在 完全相同 的文字浮水印。

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,確保只跳過已包含 完全相同 浮水印的文件。


步驟 6 – 取代資料夾內的舊標誌

公司改版時,可能需要將所有舊標誌換成新標誌。以下程式碼結合兩種影像搜尋策略——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 環境下的品牌或文件保護工作流程。

其他資源