Wprowadzenie

Przedsiębiorstwa często muszą oznaczyć lub zabezpieczyć tysiące plików – umowy, prezentacje, faktury – w jednej operacji. Robienie tego ręcznie oznacza otwieranie każdego dokumentu, wstawianie logo lub informacji o poufności i ponowne zapisywanie. Proces nie tylko zajmuje dużo czasu, ale wprowadza ryzyko błędów ludzkich oraz możliwość zduplikowanych znaków wodnych lub pominiętych plików.

GroupDocs.Watermark for .NET rozwiązuje problem dzięki jednolitemu API działającemu na PDF, DOCX, PPTX, XLSX oraz popularnych formatach obrazów. Projekt przykładowy zawiera cztery typy dokumentów (DOCX, PDF, XLSX, PPTX), więc każdy tryb pipeline jest testowany na rzeczywistych formatach. W tym samouczku przeprowadzimy kompletny batch watermark pipeline, który:

  1. Ładuje licencję (lub przełącza się w tryb ewaluacyjny).
  2. Skanuje folder i filtruje tylko formaty obsługiwane przez bibliotekę.
  3. Nakłada kafelkowy znak wodny tekstowy na każdy dokument.
  4. Nakłada kafelkowy znak wodny logo z niestandardową przezroczystością i obrotem.
  5. Dodaje znak wodny tylko wtedy, gdy nie jest już obecny (przetwarzanie idempotentne).
  6. Znajduje i zamienia przestarzałe logo firmowe na nowe.

Po zakończeniu będziesz mieć gotowe rozwiązanie, które można wstawić do dowolnego projektu .NET.

Dlaczego batch watermarking jest ważny?

  • Skalowalność – przetwarzaj dziesiątki lub tysiące plików w jednej pętli.
  • Spójność – ten sam styl wizualny jest stosowany do każdego dokumentu, eliminując rozproszenie marki.
  • Bezpieczeństwo – logika idempotentna zapobiega podwójnym znakom wodnym przy ponownym uruchomieniu pipeline.
  • Przygotowanie na przyszłość – kod zamiany logo pozwala wprowadzić rebranding bez ręcznego edytowania każdego pliku.

Wymagania wstępne

  • .NET 6.0 lub nowszy.
  • Pakiet NuGet GroupDocs.Watermark (dotnet add package GroupDocs.Watermark).
  • Plik licencji (tymczasowy lub stały). Przykłady działają w trybie ewaluacyjnym, jeśli plik jest nieobecny.
  • Dwa foldery na dysku:
    • InputFolder – zawiera dokumenty źródłowe.
    • OutputFolder – miejsce, w którym zostaną zapisane wersje z znakami wodnymi.

Krok 1 – Ładowanie licencji

Biblioteka wymaga licencji, aby działać bez ograniczeń ewaluacyjnych. Poniższy fragment próbuje wczytać plik licencji i w cichym trybie przełącza się na tryb ewaluacyjny, jeśli plik nie zostanie znaleziony.

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

Kluczowy punkt: Zmienna LicensePath powinna wskazywać na Twój plik .lic. Jeśli plik jest nieobecny, kod kontynuuje działanie, co jest przydatne przy szybkich testach.


Krok 2 – Wykrywanie obsługiwanych plików

GroupDocs.Watermark może przetwarzać tylko określony zestaw rozszerzeń. Poniższy pomocnik skanuje folder, buduje zbiór obsługiwanych rozszerzeń przy pomocy FileType.GetSupportedFileTypes() i zwraca jedynie pliki, które pasują.

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;

Kluczowy punkt: Metoda zapewnia, że późniejsze pętle znakowania nie napotkają nieobsługiwanego formatu, co w przeciwnym razie spowodowałoby wyjątek w czasie wykonywania.


Krok 3 – Nakładanie kafelkowego znaku wodnego tekstowego

Poniższy kod tworzy czerwony, półprzezroczysty znak wodny “CONFIDENTIAL”, obraca go o ‑30° i kafelkowo rozmieszcza na każdej stronie przy użyciu 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");

Kluczowe punkty:

  • TileOptions tworzy wzór przypominający cegłę, co utrudnia usunięcie znaku wodnego bez uszkodzenia zawartości.
  • Ten sam fragment działa dla PDF‑ów, plików Word, arkuszy i obrazów, ponieważ Watermarker abstrahuje format.

Text watermark applied to a document


Jeśli wolisz wizualny znak marki, zamień znak tekstowy na obraz. Poniższy kod sprawdza, czy plik logo istnieje, a następnie kafelkowo nakłada go z 40 % przezroczystością i obrotem ‑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");

Kluczowe punkty:

  • Ta sama logika TileOptions użyta dla tekstu działa również dla obrazów, zapewniając spójny wygląd we wszystkich stronach.
  • Opacity pozwala zachować czytelność treści, jednocześnie wyświetlając markę.

Tiled logo watermark applied to a document


Krok 5 – Idempotentne znakowanie (pomijanie istniejących znaków)

Uruchamianie pipeline wielokrotnie nie powinno nakładać kolejnych znaków wodnych na siebie. Ten fragment wyszukuje dokładną instancję tekstu znaku wodnego przed dodaniem nowego.

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

Kluczowy punkt:TextSearchCriteria z wartością false dla rozróżniania wielkości liter zapewnia pomijanie jedynie dokumentów, które już zawierają dokładnie ten znak wodny, który zamierzamy dodać.


Krok 6 – Zamiana przestarzałego logo w całym folderze

Gdy firma przechodzi rebranding, może być konieczna wymiana każdego starego logo na nowe. Kod łączy dwie strategie wyszukiwania obrazów – DCT‑hash dla precyzji i histogram kolorów dla tolerancji – a następnie nadpisuje dane obrazu w każdym znalezionym miejscu.

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

Kluczowe punkty:

  • WatermarkerSettings z PdfSearchableObjects.All umożliwia wyszukiwanie logo, które jest zapisane jako artefakt PDF.
  • Połączenie kryteriów DCT‑hash i histogramu kolorów wykrywa zarówno dokładne wektory logo (Office), jak i ich rastrowe wersje (PDF).

Old logo replaced with new logo


Najlepsze praktyki i wskazówki

  • Utwórz folder wyjściowy raz (Directory.CreateDirectory) – metoda jest idempotentna i zapobiega wyścigom wątków.
  • Loguj postęp – wyjście konsoli w każdym kroku ułatwia szybkie zidentyfikowanie, które pliki zakończyły się sukcesem, a które nie.
  • Dostosuj Opacity i RotateAngle zgodnie z wytycznymi marki; wartość w przedziale 0.3–0.5 zazwyczaj jest wystarczająco widoczna, a jednocześnie nieinwazyjna.
  • Używaj idempotentnego smart batch przy wszelkich cyklicznych zadaniach (np. nocnych aktualizacjach brandingu).
  • Przetestuj zamianę logo na małej próbce przed uruchomieniem na całym repozytorium, aby upewnić się, że kryteria wyszukiwania są prawidłowo skalibrowane.

Rozwiązywanie typowych problemów

Objaw Prawdopodobna przyczyna Rozwiązanie
Żadne pliki nie są przetwarzane ScanFolderForSupportedFiles zwrócił pustą listę Sprawdź ścieżkę InputFolder oraz upewnij się, że folder zawiera obsługiwane formaty (PDF, DOCX, PPTX, XLSX, PNG, JPG itp.)
Znak wodny niewidoczny Zbyt niska wartość Opacity lub kolor zlewa się z tłem Zwiększ Opacity (np. do 0.5) lub zmień ForegroundColor na kontrastowy odcień
Logo w PDF nie zostało znalezione podczas zamiany Logo dodane jako operator rysowania w strumieniu treści (niewyszukiwalne) Dodawaj logo z opcją PdfArtifactWatermarkOptions, aby stało się wyszukiwalnym artefaktem
Wyjątek System.Drawing.Common na Linuksie Brak natywnych bibliotek GDI+ Zainstaluj libgdiplus na docelowej maszynie Linux lub włącz obsługę Unix w pliku .csproj (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />).

Zakończenie

Masz teraz kompletny, gotowy do produkcji pipeline, który potrafi:

  • Zainicjować licencję biblioteki.
  • Automatycznie wykrywać obsługiwane dokumenty.
  • Nakładać kafelkowe znaki wodne tekstowe lub logo.
  • Bezpiecznie działać wielokrotnie bez tworzenia duplikatów.
  • Zamienić stare logo firmowe w całym folderze.

Te elementy można dowolnie łączyć, aby dopasować je do każdego procesu brandingu lub ochrony dokumentów w .NET.

Dodatkowe zasoby