소개
기업은 종종 수천 개의 파일(계약서, 프레젠테이션, 청구서 등)을 한 번에 브랜드화하거나 보호해야 합니다. 이를 수동으로 수행하면 각 문서를 열고 로고 또는 기밀성 고지를 삽입한 뒤 다시 저장해야 합니다. 이 과정은 시간이 많이 걸릴 뿐만 아니라 사람에 의한 오류가 발생하고 중복 워터마크 또는 누락된 파일 위험이 있습니다.
GroupDocs.Watermark for .NET 은 PDF, DOCX, PPTX, XLSX 및 일반 이미지 형식 전반에 걸쳐 작동하는 통합 API로 이 문제를 해결합니다. 샘플 프로젝트에는 네 가지 문서 유형(DOCX, PDF, XLSX, PPTX)이 포함되어 있어 모든 파이프라인 모드가 실제 형식에 대해 실행됩니다. 이번 튜토리얼에서는 다음과 같은 배치 워터마크 파이프라인을 단계별로 살펴봅니다.
- 라이선스를 로드합니다(또는 평가 모드로 대체).
- 폴더를 스캔하고 라이브러리가 처리할 수 있는 형식만 필터링합니다.
- 모든 문서에 타일형 텍스트 워터마크를 적용합니다.
- 사용자 지정 불투명도와 회전 각도를 가진 타일형 로고 워터마크를 적용합니다.
- 이미 존재하는 경우에만 워터마크를 추가합니다(멱등 처리).
- 구식 회사 로고를 새 로고로 찾아 교체합니다.
끝까지 진행하면 .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");
핵심 포인트: 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 로고가 교체되지 않음 | 로고가 콘텐츠 스트림 draw 연산자로 삽입돼 검색되지 않음 | 로고를 삽입할 때 PdfArtifactWatermarkOptions 로 추가하면 검색 가능한 아티팩트가 됩니다 |
Linux에서 System.Drawing.Common 예외 발생 |
네이티브 GDI+ 라이브러리 누락 | 대상 Linux 머신에 libgdiplus 를 설치하거나 .csproj 에 Unix 지원 옵션을 추가 (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />). |
결론
이제 완전하고 프로덕션 수준의 파이프라인을 갖추었습니다. 이를 통해 다음을 수행할 수 있습니다.
- 라이선스 적용
- 지원 문서 자동 감지
- 타일형 텍스트 또는 로고 워터마크 적용
- 중복 워터마크 없이 안전하게 여러 번 실행
- 전체 폴더에 걸친 구식 로고 교체
이 빌딩 블록들을 조합하면 .NET 환경에서 어떤 브랜드 보호 또는 문서 보호 워크플로에도 맞춤형 솔루션을 만들 수 있습니다.