Introduction
شرکتها اغلب نیاز دارند تا هزاران فایل—قراردادها، ارائهها، فاکتورها—را در یک عملیات یکپارچه برند یا محافظت کنند. انجام این کار بهصورت دستی به این معنی است که هر سند را باز کنید، لوگو یا اعلان محرمانگی را وارد کنید و دوباره ذخیره کنید. نه تنها این فرآیند زمانبر است، بلکه خطای انسانی را وارد میکند و خطر آبنشانهای تکراری یا فایلهای فراموششده را بهوجود میآورد.
GroupDocs.Watermark for .NET این مشکل را با یک API یکپارچه که بر روی PDF، DOCX، PPTX، XLSX و فرمتهای رایج تصویر کار میکند، حل میکند. پروژه نمونه با چهار نوع سند (DOCX، PDF، XLSX، PPTX) ارائه میشود تا هر حالت لولهکشی (pipeline) بر روی فرمتهای واقعی اجرا شود. در این آموزش، یک لولهکشی آبنشان دستهای کامل را مرور میکنیم که:
- یک لایسنس را بارگذاری میکند (یا در صورت عدم وجود به حالت ارزیابی میرود).
- یک پوشه را اسکن میکند و فقط فرمتهایی را که کتابخانه میتواند پردازش کند فیلتر مینماید.
- یک آبنشان متنی کاشیشده را به هر سند اعمال میکند.
- یک آبنشان لوگو کاشیشده با شفافیت و چرخش سفارشی اعمال میکند.
- فقط زمانی که آبنشان قبلاً وجود نداشته باشد، آن را اضافه میکند (پردازش ایندومنت).
- یافتن و جایگزینی یک لوگوی قدیمی شرکت با لوگوی جدید.
در پایان، یک راهحل آماده‑به‑اجرا خواهید داشت که میتواند در هر پروژه .NET قرار گیرد.
Why Batch Watermarking Matters
- Scalability – Process dozens or thousands of files with a single loop.
- Consistency – The same visual style is applied to every document, eliminating brand drift.
- Safety – Idempotent logic prevents duplicate watermarks when the pipeline is re‑run.
- Future‑proofing – Logo‑replacement code lets you roll out a re‑brand without touching each file manually.
Prerequisites
- .NET 6.0 or later.
- GroupDocs.Watermark NuGet package (
dotnet add package GroupDocs.Watermark). - A license file (temporary or permanent). The examples work in evaluation mode if the file is missing.
- Two folders on disk:
InputFolder– contains the source documents.OutputFolder– جایی که نسخههای دارای واترمارک ذخیره میشوند.
Step 1 – Load the License
The library requires a license to run without evaluation limits. The snippet below tries to load a license file and falls back silently if the file is not found.
try
{
var license = new License();
license.SetLicense(LicensePath);
Console.WriteLine("License applied successfully.");
}
catch
{
Console.WriteLine("Warning: License not found. Running in evaluation mode.");
}
Key point: The LicensePath variable should point to your .lic file. If the file is missing the code continues, which is helpful for quick testing.
Step 2 – Discover Supported Files
GroupDocs.Watermark can only process a specific set of extensions. The helper below scans a folder, builds a hash set of supported extensions via FileType.GetSupportedFileTypes(), and returns only the files that match.
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;
Key point: The method guarantees that the later watermarking loops never encounter an unsupported format, which would otherwise throw a runtime exception.
Step 3 – Apply a Tiled Text Watermark
The following code creates a red, semi‑transparent “CONFIDENTIAL” watermark, rotates it ‑30°, and tiles it across every page using 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");
Key points:
TileOptionscreates a brick‑like pattern, making the watermark hard to remove without affecting the underlying content.- The same snippet works for PDFs, Word files, spreadsheets and images because
Watermarkerabstracts the format.
Step 4 – Apply a Tiled Logo Watermark
If you prefer a visual brand mark, replace the text watermark with an image. The code below checks that the logo file exists, then tiles it with 40 % opacity and a ‑30° rotation.
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");
Key points:
- The same
TileOptionslogic used for text works for images, giving a consistent look across all pages. Opacitylets the underlying content remain readable while still displaying the brand.
Step 5 – Idempotent Watermarking (Skip Existing Marks)
Running the pipeline multiple times should not stack watermarks on top of each other. This snippet searches for an exact instance of the watermark text before adding a new one.
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");
Key point: TextSearchCriteria with false for case‑sensitivity ensures we only skip documents that already contain the exact watermark we intend to add.
Step 6 – Replace an Outdated Logo Across the Folder
When a company re‑brands, you may need to swap every old logo with the new one. The code combines two image‑search strategies – DCT‑hash for precision and colour‑histogram for tolerance – then overwrites the image data of each match.
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");
Key points:
WatermarkerSettingswithPdfSearchableObjects.Allenables the search to see logos that are stored as PDF artifacts.- Combining DCT‑hash and colour‑histogram criteria catches both exact vector logos (Office) and rasterised versions (PDF).
Best Practices & Tips
- Create the output folder once (
Directory.CreateDirectory) – the method is idempotent and avoids race conditions. - Log progress – the console output in each step makes it easy to see which files succeeded or failed.
- Tune
OpacityandRotateAngleper brand guidelines; a value between 0.3–0.5 is usually enough to be visible but not intrusive. - Use the idempotent smart batch for any recurring job (e.g., nightly branding updates).
- Test logo replacement on a small sample before running across the entire repository to ensure the search criteria are correctly tuned.
Troubleshooting Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| No files are processed | ScanFolderForSupportedFiles returned an empty list |
Verify InputFolder path and that the folder contains supported formats (PDF, DOCX, PPTX, XLSX, PNG, JPG, etc.) |
| Watermark not visible | Opacity set too low or colour blends with background | Increase Opacity (e.g., 0.5) or switch ForegroundColor to a contrasting hue |
| PDF logos not found during replacement | Logos added as content‑stream draw operators (not searchable) | When seeding logos, add them with PdfArtifactWatermarkOptions so they become searchable artifacts |
Exception System.Drawing.Common on Linux |
Missing native GDI+ libraries | Install libgdiplus on the target Linux machine or enable Unix support in the .csproj (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />). |
Conclusion
You now have a complete, production‑ready pipeline that can:
- License the library.
- Detect supported documents automatically.
- Apply tiled text or logo watermarks.
- Run safely multiple times without creating duplicates.
- Replace an old corporate logo across an entire folder.
These building blocks can be mixed and matched to fit any branding or document‑protection workflow in .NET.