Введение

Предприятия часто нуждаются в брендировании или защите тысяч файлов — контрактов, презентаций, счетов — одним действием. Делать это вручную означает открывать каждый документ, вставлять логотип или уведомление о конфиденциальности и сохранять его снова. Это не только отнимает много времени, но и вводит человеческий фактор и риск дублирования водяных знаков или пропуска файлов.

GroupDocs.Watermark for .NET решает проблему с помощью единого API, который работает с PDF, DOCX, PPTX, XLSX и распространёнными форматами изображений. Примерный проект поставляется с четырьмя типами документов (DOCX, PDF, XLSX, PPTX), поэтому каждый режим конвейера работает с реальными форматами. В этом руководстве мы пройдём полный конвейер пакетного наложения водяных знаков, который:

  1. Загружает лицензию (или переходит в режим оценки).
  2. Сканирует папку и отбирает только форматы, поддерживаемые библиотекой.
  3. Применяет чередующийся текстовый водяной знак ко всем документам.
  4. Применяет чередующийся логотип‑водяной знак с пользовательской непрозрачностью и вращением.
  5. Добавляет водяной знак только если его ещё нет (идемпотентная обработка).
  6. Находит и заменяет устаревший логотип компании новым.

К концу вы получите готовое решение, которое можно сразу использовать в любом .NET‑проекте.

Почему важна пакетная обработка водяных знаков

  • Масштабируемость — обрабатывайте десятки или тысячи файлов одним циклом.
  • Последовательность — один и тот же визуальный стиль применяется ко всем документам, устраняя отклонения бренда.
  • Безопасность — идемпотентная логика предотвращает дублирование водяных знаков при повторном запуске конвейера.
  • Будущее — код замены логотипа позволяет провести ребрендинг без ручного изменения каждого файла.

Предварительные требования

  • .NET 6.0 или новее.
  • NuGet‑пакет GroupDocs.Watermark (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 может обрабатывать только определённый набор расширений. Ниже helper сканирует папку, формирует набор поддерживаемых расширений через 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, чтобы они стали поисковыми артефактами
Исключение System.Drawing.Common в Linux Отсутствуют нативные библиотеки GDI+ Установите libgdiplus на целевой Linux‑сервер или включите поддержку Unix в .csproj (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />).

Заключение

Теперь у вас есть полный, готовый к производству конвейер, который может:

  • Лицензировать библиотеку.
  • Автоматически определять поддерживаемые документы.
  • Применять чередующиеся текстовые или логотипные водяные знаки.
  • Безопасно работать многократно без создания дубликатов.
  • Заменять старый корпоративный логотип во всей папке.

Эти строительные блоки можно комбинировать и адаптировать под любой процесс брендирования или защиты документов в .NET.

Дополнительные ресурсы