Introduction

企業はしばしば、契約書、プレゼンテーション、請求書など、数千件に及ぶファイルに対して一括でブランド付与や保護を行う必要があります。手作業で行う場合、各ドキュメントを開き、ロゴや機密性通知を挿入し、再度保存しなければなりません。プロセスは時間がかかるだけでなく、人為的ミスを招き、重複した透かしや見落としが発生するリスクがあります。

GroupDocs.Watermark for .NET は、PDF、DOCX、PPTX、XLSX および一般的な画像形式に対応した統一 API でこの問題を解決します。サンプルプロジェクトには 4 種類のドキュメント (DOCX、PDF、XLSX、PPTX) が同梱されており、すべてのパイプラインモードを実際のフォーマットで検証できます。このチュートリアルでは、以下の バッチ透かしパイプライン を順に解説します。

  1. ライセンスをロード(または評価モードにフォールバック)する。
  2. フォルダーをスキャンし、ライブラリが処理可能な形式だけをフィルタリングする。
  3. すべてのドキュメントにタイル状の テキスト 透かしを適用する。
  4. カスタム不透明度と回転角を持つタイル状の ロゴ 透かしを適用する。
  5. 既に透かしが存在しない場合にのみ 透かしを追加する(冪等処理)。
  6. 古い会社ロゴを新しいロゴに置換 する。

最後まで実行すれば、任意の .NET プロジェクトに組み込める実行可能なソリューションが完成します。

Why Batch Watermarking Matters

  • Scalability – 1 つのループで数十件から数千件のファイルを処理できる。
  • Consistency – すべてのドキュメントに同一のビジュアルスタイルが適用され、ブランドのブレがなくなる。
  • Safety – 冪等ロジックにより、パイプラインを再実行しても透かしが重複しない。
  • Future‑proofing – ロゴ置換コードにより、手作業でファイルを開くことなくリブランディングが可能。

Prerequisites

  • .NET 6.0 以上。
  • GroupDocs.Watermark NuGet パッケージ (dotnet add package GroupDocs.Watermark)。
  • ライセンス ファイル(一時的または永続的)。ファイルが無い場合は評価モードで動作します。
  • ディスク上の 2 つのフォルダー:
    • InputFolder – ソースドキュメントを格納。
    • OutputFolder – 透かしを付与したコピーを書き出す先。

Step 1 – Load the License

ライブラリは評価制限なしで実行するためにライセンスが必要です。以下のスニペットはライセンス ファイルのロードを試み、見つからなければ静かに評価モードへフォールバックします。

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 ファイルへのパスを指す必要があります。ファイルが無い場合でもコードは続行されるため、手早いテストに便利です。


Step 2 – Discover Supported Files

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;

重要ポイント: 後続の透かし処理ループで未対応フォーマットに遭遇することがなくなり、ランタイム例外を防げます。


Step 3 – Apply a Tiled Text Watermark

以下のコードは赤色・半透明の 「CONFIDENTIAL」 透かしを作成し、‑30° 回転させてページ全体にタイル配置します。

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、スプレッドシート、画像すべてで同一コードが機能します。

ドキュメントに適用されたテキスト透かし


Step 4 – Apply a Tiled Logo Watermark

テキスト透かしの代わりに画像ロゴを使用したい場合は、以下のコードでロゴファイルの存在を確認し、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 を調整することで、下位コンテンツの可読性を保ちつつブランドを表示できます。

ドキュメントに適用されたタイル状ロゴ透かし


Step 5 – Idempotent Watermarking (Skip Existing Marks)

パイプラインを複数回実行しても透かしが重複しないようにするには、追加前に透かしテキストが 正確に 存在するか検索します。

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 の第2引数に false を渡すことで大文字小文字を区別せず検索し、正確に同じ透かし がある場合にのみスキップします。


Step 6 – Replace an Outdated Logo Across the Folder

企業のリブランディング時に、古いロゴをすべて新しいロゴに差し替える必要があることがあります。以下のコードは、DCT‑ハッシュとカラー・ヒストグラムの 2 つの画像検索手法を組み合わせ、マッチした画像データを新ロゴに上書きします。

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‑ハッシュとカラー・ヒストグラムの組み合わせで、Office のベクターロゴと PDF のラスタライズ版の両方を捕捉できます。

古いロゴが新しいロゴに置換された様子


Best Practices & Tips

  • 出力フォルダーは一度だけ作成 (Directory.CreateDirectory) – このメソッドは冪等であり、競合状態を防げます。
  • 進捗をログ出力 – 各ステップのコンソール出力により、どのファイルが成功・失敗したかが一目で分かります。
  • OpacityRotateAngle をブランドガイドラインに合わせて調整; 0.3〜0.5 の範囲が視認性と侵入度のバランスとして一般的です。
  • 冪等スマートバッチ を定期的なジョブ(例: 夜間のブランド更新)に利用。
  • ロゴ置換は小規模サンプルでテスト してから全リポジトリに適用し、検索条件が適切に調整されていることを確認。

Troubleshooting Common Issues

症状 考えられる原因 対策
ファイルが一切処理されない ScanFolderForSupportedFiles が空リストを返した InputFolder のパスと、フォルダー内にサポート対象フォーマット (PDF, DOCX, PPTX, XLSX, PNG, JPG など) があるか確認
透かしが見えない 不透明度が低すぎる、または背景色と同化している Opacity を上げる (例: 0.5) または ForegroundColor をコントラストの高い色に変更
PDF のロゴが置換されない ロゴがコンテンツストリームの描画オペレーターとして埋め込まれている ロゴを追加する際に PdfArtifactWatermarkOptions を使用し、検索可能なアーティファクトにする
Linux で System.Drawing.Common の例外が発生 ネイティブ GDI+ ライブラリが欠如 ターゲット Linux マシンに libgdiplus をインストール、または .csproj に Unix サポートを有効化 (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />)

Conclusion

これで 完全な本番レベルのバッチ透かしパイプライン が完成しました。以下の機能を備えています。

  • ライセンスの適用
  • 対応ドキュメントの自動検出
  • タイル状テキストまたはロゴ透かしの適用
  • 冪等処理による重複透かし防止
  • フォルダー全体での古いロゴ置換

これらの部品を組み合わせることで、あらゆる .NET 環境でのブランディングや文書保護ワークフローに柔軟に対応できます。

Additional Resources