Introduction
Las empresas a menudo necesitan marcar o proteger miles de archivos – contratos, presentaciones, facturas – en una única operación. Hacer esto manualmente implica abrir cada documento, insertar un logotipo o un aviso de confidencialidad y volver a guardarlo. No solo el proceso consume tiempo, también introduce errores humanos y deja el riesgo de marcas de agua duplicadas o archivos omitidos.
GroupDocs.Watermark for .NET resuelve el problema con una API unificada que funciona con PDF, DOCX, PPTX, XLSX y formatos de imagen comunes. El proyecto de ejemplo incluye cuatro tipos de documentos (DOCX, PDF, XLSX, PPTX) para que cada modo de canalización se ejecute contra formatos del mundo real. En este tutorial recorreremos una pipeline de marcas de agua por lotes completa que:
- Carga una licencia (o recurre al modo de evaluación).
- Escanea una carpeta y filtra solo los formatos que la biblioteca puede manejar.
- Aplica una marca de agua de texto en mosaico a cada documento.
- Aplica una marca de agua de logotipo en mosaico con opacidad y rotación personalizadas.
- Añade una marca de agua solo cuando no está ya presente (procesamiento idempotente).
- Busca y reemplaza un logotipo de empresa desactualizado por uno nuevo.
Al final tendrás una solución lista para ejecutar que puede integrarse en cualquier proyecto .NET.
Why Batch Watermarking Matters
- Escalabilidad – Procesa decenas o miles de archivos con un solo bucle.
- Consistencia – El mismo estilo visual se aplica a cada documento, eliminando la deriva de marca.
- Seguridad – La lógica idempotente evita marcas de agua duplicadas cuando la pipeline se vuelve a ejecutar.
- Preparación para el futuro – El código de reemplazo de logotipos permite lanzar una nueva identidad corporativa sin tocar cada archivo manualmente.
Prerequisites
- .NET 6.0 o posterior.
- Paquete NuGet GroupDocs.Watermark (
dotnet add package GroupDocs.Watermark). - Un archivo de licencia (temporal o permanente). Los ejemplos funcionan en modo de evaluación si el archivo falta.
- Dos carpetas en disco:
InputFolder– contiene los documentos de origen.OutputFolder– donde se escribirán las copias con marca de agua.
Step 1 – Load the License
La biblioteca requiere una licencia para ejecutarse sin límites de evaluación. El fragmento a continuación intenta cargar un archivo de licencia y, si no lo encuentra, continúa silenciosamente.
try
{
var license = new License();
license.SetLicense(LicensePath);
Console.WriteLine("License applied successfully.");
}
catch
{
Console.WriteLine("Warning: License not found. Running in evaluation mode.");
}
Punto clave: La variable LicensePath debe apuntar a tu archivo .lic. Si el archivo falta, el código continúa, lo que es útil para pruebas rápidas.
Step 2 – Discover Supported Files
GroupDocs.Watermark solo puede procesar un conjunto específico de extensiones. El ayudante a continuación escanea una carpeta, crea un conjunto hash de extensiones compatibles mediante FileType.GetSupportedFileTypes() y devuelve solo los archivos que coinciden.
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;
Punto clave: El método garantiza que los bucles de marcación posteriores nunca encuentren un formato no compatible, lo que de otro modo provocaría una excepción en tiempo de ejecución.
Step 3 – Apply a Tiled Text Watermark
El siguiente código crea una marca de agua roja y semitransparente “CONFIDENTIAL”, la rota ‑30° y la repite en todas las páginas mediante 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");
Puntos clave:
TileOptionscrea un patrón tipo ladrillo, lo que hace que la marca de agua sea difícil de eliminar sin afectar el contenido subyacente.- El mismo fragmento funciona para PDFs, archivos Word, hojas de cálculo e imágenes porque
Watermarkerabstrae el formato.
Step 4 – Apply a Tiled Logo Watermark
Si prefieres una marca visual, sustituye la marca de agua de texto por una imagen. El código a continuación verifica que el archivo del logotipo exista y luego lo repite con un 40 % de opacidad y una rotación de ‑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");
Puntos clave:
- La misma lógica de
TileOptionsusada para texto funciona para imágenes, proporcionando una apariencia coherente en todas las páginas. Opacitypermite que el contenido subyacente siga siendo legible mientras se muestra la marca.
Step 5 – Idempotent Watermarking (Skip Existing Marks)
Ejecutar la pipeline varias veces no debe apilar marcas de agua una sobre otra. Este fragmento busca una instancia exacta del texto de la marca de agua antes de añadir una nueva.
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");
Punto clave: TextSearchCriteria con false para la sensibilidad a mayúsculas/minúsculas asegura que solo se omitan los documentos que ya contienen la exacta marca de agua que se pretende añadir.
Step 6 – Replace an Outdated Logo Across the Folder
Cuando una empresa cambia de identidad, puede ser necesario intercambiar cada logotipo antiguo por el nuevo. El código combina dos estrategias de búsqueda de imágenes – DCT‑hash para precisión y histograma de colores para tolerancia – y luego sobrescribe los datos de imagen de cada coincidencia.
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");
Puntos clave:
WatermarkerSettingsconPdfSearchableObjects.Allpermite que la búsqueda vea los logotipos almacenados como artefactos PDF.- Combinar criterios DCT‑hash y de histograma de colores captura tanto logotipos vectoriales exactos (Office) como versiones rasterizadas (PDF).
Best Practices & Tips
- Crea la carpeta de salida una sola vez (
Directory.CreateDirectory) – el método es idempotente y evita condiciones de carrera. - Registra el progreso – la salida en consola en cada paso facilita ver qué archivos se procesaron correctamente o fallaron.
- Ajusta
OpacityyRotateAnglesegún las directrices de la marca; un valor entre 0.3‑0.5 suele ser suficiente para ser visible sin ser intrusivo. - Utiliza el lote inteligente idempotente para cualquier trabajo recurrente (por ejemplo, actualizaciones nocturnas de marca).
- Prueba el reemplazo de logotipos en una muestra pequeña antes de ejecutarlo en todo el repositorio para asegurarte de que los criterios de búsqueda están bien afinados.
Troubleshooting Common Issues
| Síntoma | Causa probable | Solución |
|---|---|---|
| No se procesan archivos | ScanFolderForSupportedFiles devolvió una lista vacía |
Verifica la ruta de InputFolder y que la carpeta contenga formatos compatibles (PDF, DOCX, PPTX, XLSX, PNG, JPG, etc.) |
| La marca de agua no es visible | Opacidad demasiado baja o el color se mezcla con el fondo | Incrementa Opacity (p. ej., 0.5) o cambia ForegroundColor a un tono contrastante |
| Los logotipos PDF no se encuentran durante el reemplazo | Los logotipos se añadieron como operadores de flujo de contenido (no buscables) | Al insertar logotipos, usa PdfArtifactWatermarkOptions para que se conviertan en artefactos buscables |
Excepción System.Drawing.Common en Linux |
Falta la biblioteca nativa GDI+ | Instala libgdiplus en la máquina Linux objetivo o habilita el soporte Unix en el .csproj (<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />). |
Conclusion
Ahora dispones de una pipeline completa y lista para producción que puede:
- Licenciar la biblioteca.
- Detectar documentos compatibles automáticamente.
- Aplicar marcas de agua de texto o logotipo en mosaico.
- Ejecutarse de forma segura varias veces sin crear duplicados.
- Reemplazar un logotipo corporativo antiguo en toda una carpeta.
Estos bloques de construcción pueden combinarse y adaptarse a cualquier flujo de trabajo de protección o branding de documentos en .NET.