소개

기업은 종종 수천 개의 파일(계약서, 프레젠테이션, 청구서 등)을 한 번에 브랜드화하거나 보호해야 합니다. 이를 수동으로 수행하면 각 문서를 열고 로고 또는 기밀성 고지를 삽입한 뒤 다시 저장해야 합니다. 이 과정은 시간이 많이 걸릴 뿐만 아니라 사람에 의한 오류가 발생하고 중복 워터마크 또는 누락된 파일 위험이 있습니다.

GroupDocs.Watermark for .NET 은 PDF, DOCX, PPTX, XLSX 및 일반 이미지 형식 전반에 걸쳐 작동하는 통합 API로 이 문제를 해결합니다. 샘플 프로젝트에는 네 가지 문서 유형(DOCX, PDF, XLSX, PPTX)이 포함되어 있어 모든 파이프라인 모드가 실제 형식에 대해 실행됩니다. 이번 튜토리얼에서는 다음과 같은 배치 워터마크 파이프라인을 단계별로 살펴봅니다.

  1. 라이선스를 로드합니다(또는 평가 모드로 대체).
  2. 폴더를 스캔하고 라이브러리가 처리할 수 있는 형식만 필터링합니다.
  3. 모든 문서에 타일형 텍스트 워터마크를 적용합니다.
  4. 사용자 지정 불투명도와 회전 각도를 가진 타일형 로고 워터마크를 적용합니다.
  5. 이미 존재하는 경우에만 워터마크를 추가합니다(멱등 처리).
  6. 구식 회사 로고를 새 로고로 찾아 교체합니다.

끝까지 진행하면 .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 은 벽돌 모양 패턴을 만들어 워터마크를 제거하기 어렵게 합니다.
  • Watermarker 가 형식을 추상화하므로 PDF, Word, 스프레드시트, 이미지 모두 동일하게 동작합니다.

텍스트 워터마크가 적용된 문서


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

핵심 포인트: TextSearchCriteriafalse(대소문자 구분 안 함)를 전달하면 우리가 추가하려는 정확한 워터마크가 이미 있는 문서만 건너뛰게 됩니다.


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

핵심 포인트

  • WatermarkerSettingsPdfSearchableObjects.All 을 지정하면 PDF 아티팩트로 저장된 로고도 검색 대상이 됩니다.
  • DCT‑hash와 색상 히스토그램 기준을 결합하면 Office 벡터 로고와 PDF 래스터 로고 모두를 포착할 수 있습니다.

구식 로고가 새 로고로 교체된 모습


모범 사례 및 팁

  • 출력 폴더는 한 번만 생성 (Directory.CreateDirectory) – 이 메서드는 멱등이며 레이스 컨디션을 방지합니다.
  • 진행 상황을 로그 – 각 단계의 콘솔 출력으로 어떤 파일이 성공했는지, 실패했는지 쉽게 확인할 수 있습니다.
  • OpacityRotateAngle을 브랜드 가이드에 맞게 조정 – 일반적으로 0.3–0.5 사이가 눈에 잘 띄면서도 방해되지 않는 값입니다.
  • 멱등 스마트 배치를 정기 작업에 활용(예: 야간 브랜드 업데이트).
  • 전체 저장소에 적용하기 전에 작은 샘플로 로고 교체 테스트 – 검색 기준이 올바르게 튜닝됐는지 확인하세요.

흔히 발생하는 문제 해결

증상 가능한 원인 해결 방법
파일이 전혀 처리되지 않음 ScanFolderForSupportedFiles 가 빈 리스트를 반환 InputFolder 경로와 해당 폴더에 지원 형식(PDF, DOCX, PPTX, XLSX, PNG, JPG 등)이 있는지 확인
워터마크가 보이지 않음 불투명도가 너무 낮거나 색상이 배경과 섞임 Opacity 값을 높이세요(예: 0.5) 또는 ForegroundColor 를 대비가 강한 색으로 변경
PDF 로고가 교체되지 않음 로고가 콘텐츠 스트림 draw 연산자로 삽입돼 검색되지 않음 로고를 삽입할 때 PdfArtifactWatermarkOptions 로 추가하면 검색 가능한 아티팩트가 됩니다
Linux에서 System.Drawing.Common 예외 발생 네이티브 GDI+ 라이브러리 누락 대상 Linux 머신에 libgdiplus 를 설치하거나 .csproj 에 Unix 지원 옵션을 추가 (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />).

결론

이제 완전하고 프로덕션 수준의 파이프라인을 갖추었습니다. 이를 통해 다음을 수행할 수 있습니다.

  • 라이선스 적용
  • 지원 문서 자동 감지
  • 타일형 텍스트 또는 로고 워터마크 적용
  • 중복 워터마크 없이 안전하게 여러 번 실행
  • 전체 폴더에 걸친 구식 로고 교체

이 빌딩 블록들을 조합하면 .NET 환경에서 어떤 브랜드 보호 또는 문서 보호 워크플로에도 맞춤형 솔루션을 만들 수 있습니다.

추가 자료