介紹
企業常常需要一次性為成千上萬的檔案(合約、簡報、發票等)加上品牌或保護標記。手動操作意味著必須逐一開啟文件、插入標誌或機密聲明,然後再儲存。這不僅耗時,還會產生人為錯誤,並帶來 重複浮水印 或遺漏檔案的風險。
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– 寫入加了浮水印的副本。
步驟 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)— 此方法具冪等性,可避免競爭條件。 - 記錄進度— 每個步驟的主控台輸出讓你輕鬆看出哪些檔案成功或失敗。
- 調整
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 環境下的品牌或文件保護工作流程。