Introducción

Un colega envía dos revisiones de un contrato y te pide que las compares. Las colocas en tu servicio de comparación, el resultado vuelve y todo parece normal. Lo que no viste es que uno de esos documentos contenía una imagen vinculada que apuntaba a una URL, y tu servidor contactó ese host en el momento en que se abrió el archivo. Nada en la salida indica que eso ocurrió.

Esto no es un defecto, es lo que significa cargar un documento fielmente. Un archivo OOXML puede referenciar una imagen que reside en un servidor web en lugar de estar dentro del paquete, y tanto Word como cualquier biblioteca que cargue el documento resuelven correctamente esa referencia. GroupDocs.Comparison para .NET expone dos propiedades en LoadOptions que te permiten decidir si lo hace: SkipExternalResources y WhitelistedResources.

Entre ellas ofrecen tres configuraciones, y este artículo compara las tres: el predeterminado permisivo, bloquear todo, y bloquear todo excepto referencias nombradas. Al final sabrás cuál elegir según el origen del documento y los dos errores que hacen que estas configuraciones parezcan no funcionar.

💡 Ejemplo completo funcional: block-external-resources-on-document-load-dotnet - un proyecto de consola ejecutable que sirve las imágenes referenciadas él mismo y registra cada solicitud, para que puedas observar cómo cada configuración entra en vigor.

Dónde se Ocultan las Referencias Externas

Antes de elegir una configuración, vale la pena saber qué es lo que estás eligiendo. Un archivo .docx contiene referencias externas en dos lugares distintos, y son fáciles de pasar por alto porque ninguno es visible en el texto del documento.

La primera es una relación en word/_rels/document.xml.rels que lleva TargetMode="External" y una URL absoluta. La imagen aparece en el cuerpo como un dibujo que apunta a la relación por ID, de modo que la URL nunca aparece cerca del contenido que afecta.

La segunda es un código de campo INCLUDEPICTURE en el cuerpo del documento, que contiene su URL dentro de una instrucción de campo. Word la resuelve cuando la página se renderiza; una biblioteca de comparación la resuelve cuando el documento se carga.

Ambos mecanismos respetan las dos opciones de carga discutidas a continuación, lo cual es importante porque un documento puede usar una, la otra o ambas. Una referencia que detectaste en el archivo de relaciones no prueba que no exista una segunda en un código de campo.

Enfoque 1: El Predeterminado - Referencias Resueltas

SkipExternalResources tiene como valor predeterminado false, por lo que un documento cargado sin configuración tiene sus referencias remotas resueltas:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = false
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

Esto brinda la mayor fidelidad: los documentos comparados contienen todo lo que referencian, exactamente como Word los renderizaría. Para documentos que tu propia aplicación o plantillas generaron, donde cada URL de referencia apunta a infraestructura que tú controlas, es la elección correcta, y una imagen vinculada ausente podría hacer que la comparación sea activamente engañosa.

El costo es que cada referencia es contactada, sea quien sea quien la haya puesto. También hay un costo de tiempo que no tiene que ver con la confianza: una URL de referencia que ya no se resuelve hace que la carga espere todo el intento de conexión, en cada comparación.

Enfoque 2: Bloquear Cada Recurso Externo

Una propiedad desactiva la resolución de referencias remotas para ese documento:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = true
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

No se emite ninguna solicitud. Las imágenes referenciadas están ausentes del resultado y, esto es lo que vale la pena aclarar, nada más cambia. La configuración determina qué se carga, no cómo se encuentran las diferencias, por lo que los cambios textuales y estructurales entre los dos documentos se detectan exactamente como antes. Lo único que pierdes es la capacidad de detectar un cambio dentro de una imagen referenciada, que nunca se cargó.

Esta es la configuración que debes tratar como tu línea base para documentos que no creaste: cargas de usuarios en una aplicación web, archivos recibidos por correo electrónico, cualquier cosa comparada en un agente de compilación donde una solicitud saliente rara vez se desea. Es todo o nada, sin embargo: una imagen vinculada que realmente querías se bloquea junto con el resto, y el resultado simplemente la carece sin anunciarlo.

Enfoque 3: Bloquear Todo Excepto Referencias Nombradas

La tercera configuración es la que recompensa una lectura cuidadosa. WhitelistedResources recibe una List<string> y solo se consulta cuando SkipExternalResources es true:

LoadOptions loadOptions = new LoadOptions
{
    SkipExternalResources = true,
    WhitelistedResources = new List<string> { "includepicture-field.png" }
};

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

Las entradas son fragmentos de URL, no nombres de archivo. Cada una se compara con la URL de referencia, y una coincidencia en cualquier parte de ella permite ese recurso. Eso es lo que hace que la lista blanca sea portátil: "includepicture-field.png" permite la imagen sin importar el esquema, host o ruta que la precedan, de modo que la misma lista funciona en desarrollo y producción sin reescrituras.

La misma propiedad funciona en sentido inverso. Un fragmento corto o genérico — logo.png, o peor, .png — puede coincidir con referencias que nunca pretendiste permitir. Elige un fragmento lo suficientemente específico como para identificar el único recurso que deseas.

En el ejemplo de referencia, esta configuración recupera la imagen incluida en la lista blanca y deja bloqueada una segunda imagen referenciada, que ninguna entrada cubre. El registro de solicitudes muestra tres peticiones donde el predeterminado permisivo produjo cinco, y solo nombra el archivo en la lista blanca.

¿Qué Configuración Debería Usar?

Alinea la configuración con el origen del documento. Los documentos que tu propia aplicación o plantillas generaron pueden mantener el predeterminado, porque cada URL de referencia apunta a infraestructura que ya operas. Cualquier cosa que provenga del exterior — cargas de usuarios, adjuntos de correo, archivos de terceros — justifica SkipExternalResources = true. Añade un fragmento de WhitelistedResources estrecho solo cuando una referencia confiable realmente necesita resolverse.

Comparando los Tres

Preocupación Default Skip all Skip + whitelist
Propiedades a establecer 0 1 2
Solicitudes salientes todas las referencias ninguna solo las de la lista blanca
Control por referencia no no sí
URLs rotas aumentan el tiempo de carga sí no solo las de la lista blanca
Mejor para documentos que tú generaste documentos de cualquier otro origen plantillas confiables entre contenido no confiable

La decisión sigue la procedencia del documento más que el rendimiento. Los documentos que tus sistemas generaron pueden mantener el predeterminado. Los documentos externos justifican el bloqueo. Usa la lista blanca en el punto donde una referencia específica realmente tiene que resolverse — por ejemplo, una plantilla corporativa que extrae su imagen de encabezado de una URL interna, entre informes cuyos autores pegaron imágenes de donde quisieron.

Los Dos Errores

Ambos producen el mismo síntoma: configuras la opción y parece no hacer nada.

Una lista blanca sin el interruptor. WhitelistedResources solo se consulta cuando SkipExternalResources es true. Si la estableces sola, no hace nada en absoluto — no hay bloqueo del que pueda hacer una excepción. Si una lista blanca parece ignorada, verifica esto primero.

Opciones solo en la fuente. Este es el más sutil. Las opciones de carga describen cómo se carga un documento. El constructor de Comparer recibe las opciones para la fuente; cada llamada a Add() recibe las opciones para ese objetivo:

using (Comparer comparer = new Comparer(sourcePath, loadOptions))
{
    comparer.Add(targetPath, loadOptions);
    comparer.Compare(outputPath);
}

Si las pasas al constructor y olvidas la llamada a Add(), la fuente queda protegida mientras cada objetivo sigue obteniendo sus referencias. La comparación tiene éxito, el resultado parece plausible, y la mitad de tus documentos siguen contactando la red. Cuando fuente y objetivo necesitan manejos diferentes, pasa instancias de LoadOptions separadas — esa es precisamente la razón por la que la API las acepta por documento.

Verificando que Realmente Funciona

Un recurso bloqueado deja casi ningún rastro. El documento de salida carece de una imagen, lo que se parece mucho a un documento que nunca la tuvo. Leer el archivo resultante, por tanto, es una forma pobre de confirmar que la configuración tuvo efecto.

Observa el lado del servidor en su lugar. El ejemplo de referencia adopta este enfoque deliberadamente: inicia un pequeño listener HTTP en un puerto de loopback libre, escribe sus documentos de demostración apuntando a ese puerto y registra cada solicitud que recibe, imprimiendo el recuento por comparación. Cinco solicitudes, luego cero, luego tres. Un rastreo de red contra tus fuentes de documentos reales te brinda la misma confianza.

Conclusión

Tres configuraciones, una regla de decisión: permite que los documentos que tú generaste mantengan el predeterminado, establece SkipExternalResources = true para todo lo demás, y usa una lista blanca con un fragmento de URL estrecho solo donde una referencia confiable específica aún necesita resolverse.

Luego revisa los dos factores que anulan silenciosamente el trabajo — una lista blanca sin SkipExternalResources = true, y opciones pasadas al constructor de Comparer pero no a cada llamada a Add() — y verifica desde el lado del servidor en lugar de hacerlo a partir del archivo de salida.

Recursos Adicionales