Inleiding

Bedrijven moeten vaak duizenden bestanden – contracten, presentaties, facturen – in één keer voorzien van een merk of bescherming. Handmatig betekent dit elk document openen, een logo of vertrouwelijkheidsverklaring invoegen en opnieuw opslaan. Niet alleen is dit tijdrovend, het introduceert menselijke fouten en brengt het risico op dubbele watermerken of gemiste bestanden met zich mee.

GroupDocs.Watermark for .NET lost dit probleem op met een eenduidige API die werkt met PDF, DOCX, PPTX, XLSX en gangbare afbeeldingsformaten. Het voorbeeldproject wordt geleverd met vier documenttypen (DOCX, PDF, XLSX, PPTX) zodat elke pipeline‑modus wordt getest op real‑world formaten. In deze tutorial lopen we een volledige batch‑watermerk‑pipeline door die:

  1. Een licentie laadt (of terugvalt op evaluatiemodus).
  2. Een map scant en alleen de formaten filtert die de bibliotheek aankan.
  3. Een getegeld tekst‑watermerk toepast op elk document.
  4. Een getegeld logo‑watermerk toepast met aangepaste doorzichtigheid en rotatie.
  5. Een watermerk alleen toevoegt wanneer het nog niet aanwezig is (idempotente verwerking).
  6. Zoekt en vervangt een verouderd bedrijfslogo door een nieuw logo.

Aan het einde heb je een kant‑en‑klaar‑oplossing die in elk .NET‑project kan worden geplaatst.

Waarom batch-watermerken belangrijk is

  • Schaalbaarheid – Verwerk tientallen of duizenden bestanden met één enkele lus.
  • Consistentie – Dezelfde visuele stijl wordt op elk document toegepast, waardoor merkverschuiving wordt voorkomen.
  • Veiligheid – Idempotente logica voorkomt dubbele watermerken wanneer de pipeline opnieuw wordt uitgevoerd.
  • Toekomstbestendigheid – De logo‑vervangingscode stelt je in staat een re‑brand door te voeren zonder elk bestand handmatig aan te passen.

Vereisten

  • .NET 6.0 of hoger.
  • GroupDocs.Watermark NuGet‑pakket (dotnet add package GroupDocs.Watermark).
  • Een licentiebestand (tijdelijk of permanent). De voorbeelden werken in evaluatiemodus als het bestand ontbreekt.
  • Twee mappen op schijf:
    • InputFolder – bevat de bron‑documenten.
    • OutputFolder – waar de watergemarkeerde kopieën worden weggeschreven.

Stap 1 – Laad de licentie

De bibliotheek vereist een licentie om zonder evaluatie‑beperkingen te draaien. Het fragment hieronder probeert een licentiebestand te laden en valt stilletjes terug als het bestand niet wordt gevonden.

try
{
    var license = new License();
    license.SetLicense(LicensePath);
    Console.WriteLine("License applied successfully.");
}
catch
{
    Console.WriteLine("Warning: License not found. Running in evaluation mode.");
}

Belangrijk punt: De variabele LicensePath moet naar jouw .lic‑bestand wijzen. Als het bestand ontbreekt, gaat de code door, wat handig is voor snelle tests.


Stap 2 – Ontdek ondersteunde bestanden

GroupDocs.Watermark kan slechts een specifieke set extensies verwerken. De helper hieronder scant een map, bouwt een hash‑set van ondersteunde extensies via FileType.GetSupportedFileTypes() en retourneert alleen de bestanden die overeenkomen.

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;

Belangrijk punt: De methode garandeert dat de latere watermerk‑lussen nooit een niet‑ondersteund formaat tegenkomen, wat anders een runtime‑exception zou veroorzaken.


Stap 3 – Pas een getegelde tekstwatermerk toe

De volgende code maakt een rood, half‑transparant “CONFIDENTIAL”‑watermerk, roteert het ‑30° en tegelt het over elke pagina met 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");

Belangrijke punten:

  • TileOptions creëert een baksteen‑achtig patroon, waardoor het watermerk moeilijk te verwijderen is zonder de onderliggende inhoud te beïnvloeden.
  • Hetzelfde fragment werkt voor PDF’s, Word‑bestanden, spreadsheets en afbeeldingen omdat Watermarker het formaat abstracteert.

Text watermark applied to a document


Stap 4 – Pas een getegelde logo‑watermerk toe

Als je de voorkeur geeft aan een visueel merkteken, vervang dan het tekst‑watermerk door een afbeelding. De code hieronder controleert of het logo‑bestand bestaat, en tegelt het vervolgens met 40 % doorzichtigheid en een rotatie van ‑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");

Belangrijke punten:

  • Dezelfde TileOptions‑logica die voor tekst wordt gebruikt, werkt ook voor afbeeldingen, waardoor een consistente uitstraling ontstaat over alle pagina’s.
  • Opacity laat de onderliggende inhoud leesbaar, terwijl het merk toch zichtbaar blijft.

Tiled logo watermark applied to a document


Stap 5 – Idempotente watermerken (sla bestaande merken over)

Het uitvoeren van de pipeline meerdere keren mag geen watermerken stapelen. Dit fragment zoekt naar een exacte instantie van de watermerk‑tekst voordat een nieuw exemplaar wordt toegevoegd.

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");

Belangrijk punt:TextSearchCriteria met false voor hoofdlettergevoeligheid zorgt ervoor dat we alleen documenten overslaan die al het exacte watermerk bevatten dat we willen toevoegen.


Stap 6 – Vervang een verouderd logo in de map

Wanneer een bedrijf een re‑brand ondergaat, moet je mogelijk elk oud logo vervangen door het nieuwe. De code combineert twee afbeeldings‑zoekstrategieën – DCT‑hash voor precisie en kleur‑histogram voor tolerantie – en overschrijft vervolgens de afbeeldingsdata van elke vondst.

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");

Belangrijke punten:

  • WatermarkerSettings met PdfSearchableObjects.All maakt het zoeken mogelijk naar logo’s die als PDF‑artefacten zijn opgeslagen.
  • Het combineren van DCT‑hash‑ en kleur‑histogramcriteria vangt zowel exacte vector‑logo’s (Office) als gerasterde versies (PDF) op.

Old logo replaced with new logo


Best practices & tips

  • Maak de output‑map één keer aan (Directory.CreateDirectory) – de methode is idempotent en voorkomt race‑conditions.
  • Log de voortgang – de console‑output in elke stap maakt het eenvoudig te zien welke bestanden geslaagd of mislukt zijn.
  • Stem Opacity en RotateAngle af op de merkrichtlijnen; een waarde tussen 0.3–0.5 is meestal voldoende om zichtbaar maar niet opdringerig te zijn.
  • Gebruik de idempotente slimme batch voor terugkerende taken (bijv. nachtelijke branding‑updates).
  • Test logo‑vervanging eerst op een kleine steekproef voordat je de volledige repository doorloopt, zodat je zeker weet dat de zoekcriteria correct zijn afgesteld.

Veelvoorkomende problemen oplossen

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" />).

Conclusie

Je beschikt nu over een volledige, productie‑klare pipeline die kan:

  • De bibliotheek licentiëren.
  • Documenten automatisch detecteren die ondersteund worden.
  • Getegelde tekst‑ of logo‑watermerken toepassen.
  • Veilig meerdere keren draaien zonder duplicaten te creëren.
  • Een oud bedrijfslogo vervangen in een volledige map.

Deze bouwstenen kunnen naar wens worden gecombineerd om elke branding‑ of documentbeschermingsworkflow in .NET te ondersteunen.

Aanvullende bronnen