Un filtro de recepción de archivos rechazó un lote de archivos "PDF/A-2b" que se abrían perfectamente en todos los visores disponibles. El proveedor juraba que eran conformes. No lo eran: cada uno de ellos contenía una acción de JavaScript oculta en el catálogo, el tipo de detalle que un análisis superficial pasa por alto y que un validador completo de PDF/A como veraPDF detecta de inmediato. El problema es que nadie quería integrar un entorno de Java en un servicio por lotes de Delphi simplemente para responder a una pregunta de sí o no por archivo. Esa es la brecha que cubre ValidatePdfACompliance en PDFium Component, y vale la pena comprender cómo llega a un veredicto sin necesidad de analizar por completo el flujo de contenido
Por qué el propio PDFium no puede resolver esto
Lo primero de lo que debemos ser conscientes: la DLL pdfium.dll incluida no tiene ninguna capacidad de PDF/A. No existe ConvertToPDFA, ni escritor de OutputIntent, ni API de XMP en su interfaz pública. Cada sección de PDF/A en esta biblioteca, tanto para la escritura como para la verificación, reside en Pascal puro dentro de FPdfPdfa.pas y funciona mediante análisis a nivel de bytes y actualización incremental. Por lo tanto, cuando llama al validador no está solicitando nada al renderizador de Chromium: está ejecutando un escáner de tokens en Pascal sobre los bytes estructurales del archivo
La API pública es intencionalmente pequeña. Una sola función lee un flujo de datos desde la posición 0 y devuelve un registro:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
IsCompliant define la regla que importa en un filtro: un archivo es válido solo cuando se detecta un nivel de conformidad real y el conjunto de problemas está vacío. Con un análisis que tenga éxito pero no encuentre la marca pdfaid se resuelve como pacNone, lo cual explícitamente no es una aprobación. Este es el mismo punto que el CLI de reporte de verificación previa por lotes plantea externamente: una lista de hallazgos vacía en un archivo no reconocido no es una garantía de conformidad
Vaciar los cuerpos de flujo antes de cualquier escaneo de tokens
Aquí se presenta el detalle de implementación más importante, y el que es más fácil de resolver de forma incorrecta si escribe su propio escáner. El detector encuentra infracciones buscando tokens de nombres delimitados (secuencias como /JavaScript, /LZWDecode, /BM). Si escanea los bytes del archivo sin procesar, los cuerpos de flujo binario integrados (imágenes comprimidas, perfiles ICC, programas de fuentes) contendrán aleatoriamente secuencias de bytes que se asemejan a esos tokens. Como resultado, reportará /AA o /3D como "detectados" simplemente porque tres bytes dentro de un JPEG coincidieron con esa escritura. Eso es una fuente constante de falsos positivos
La solución es PdfStructureBytes: recorre el archivo y vacía los bytes entre cada palabra clave stream y endstream convirtiéndolos en espacios, dejando la estructura del diccionario intacta. Solo después de esto se ejecuta el escaneo. Cada comprobación de token de nombre en el validador opera sobre esta copia limpia. Si debe conservar una sola idea de este artículo, que sea esta. El mismo criterio se refleja en el validador de PDF/UA, el cual mantiene su propia copia de la rutina debido a que ambos estándares evolucionan de forma independiente
Los 29 problemas y el significado de cada uno
TPdfAValidationIssue es un contrato documentado. Los valores ordinales están fijos porque las pruebas de DUnitX, las demostraciones y la capa de reportes dependen de ellos, de modo que los nuevos hallazgos solo se añaden al final. A partir de la versión v1.63.0 hay 29 miembros. Se agrupan en las siguientes familias:
- **Metadatos e identidad**: `pvaiMissingXmpMetadata`, `pvaiMissingPdfAIdentifier`, `pvaiMissingTrailerId` (ISO 19005-1 6.1.3), `pvaiMissingXmpDates`
- **Color y salida**: `pvaiMissingOutputIntent`, `pvaiMissingIccProfile` y `pvaiMixedDeviceColorSpaces` cuando aparecen tanto DeviceRGB como DeviceCMYK (6.2.3.3)
- **Prohibiciones estrictas para cada sección**: `pvaiEncryptionPresent` (un diccionario `/Encrypt` está prohibido por completo), `pvaiJavaScriptPresent`, `pvaiForbiddenAction`, `pvaiAdditionalActions`, `pvaiLzwUsed`, `pvaiXfaPresent`, `pvaiNeedAppearancesTrue`, `pvaiForbiddenAnnotation`
- **Fuentes**: `pvaiFontNotEmbedded` y la opción más estricta `pvaiUnembeddedFont`, además de `pvaiUnicodeMappingMissing` para una declaración de Nivel U sin `/ToUnicode`
- **Etiquetado**: `pvaiLevelAStructureMissing` cuando una declaración de conformidad=A carece de estructura etiquetada
Los seis miembros más recientes, añadidos en los ordinales 24 al 29, cubren los casos sutiles con los que suelen tropezar los revisores: pvaiTrappedTrue (a /Trapped /True en el diccionario Info, una declaración engañosa ya que el valor debe ser False o Unknown), pvaiForbiddenActionSubtype (Sound o Movie utilizados como acción, no solo como anotación), pvaiTransparentColorSpace (un modo de mezcla no Normal o un /CA//ca no igual a 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont y pvaiMixedDeviceColorSpaces
Filtrado por parte: A-1 es estricto, A-2 y A-3 se flexibilizan
PDF/A no es un único reglamento. Tres elementos que PDF/A-1 prohíbe están explícitamente permitidos a partir de PDF/A-2: transparencia (un grupo /Transparency o una /SMask activa, 6.4), contenido opcional (/OCProperties, 6.1.13) y archivos incrustados (/EmbeddedFiles o /EF, 6.1.11). A un validador simple que marque los tres elementos para cada archivo rechazará documentos PDF/A-2 perfectamente válidos en masa
Por lo tanto, el validador lee el número de parte de la marca pdfaid a través de PdfAPartOf y filtra esas comprobaciones bajo la condición PartNo = 1. Las comprobaciones de modo de mezcla y de canal alfa de anotación para los nuevos problemas de transparencia también son exclusivas de la parte 1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Vale la pena mencionar un valor predeterminado conservador: cuando no existe ninguna marca pdfaid, la parte se trata como 1, la más estricta. El motivo es que un archivo no identificado debe someterse a las reglas más estrictas en lugar de aprobarse sin controles. JavaScript, acciones prohibidas, LZW, XFA, NeedAppearances, anotaciones prohibidas y fuentes no incrustadas permanecen prohibidos para cada parte, por lo que esas comprobaciones nunca se omiten
Expandir flujos de objetos para que nada se oculte
PDF 1.5 introdujo el flujo de referencias cruzadas y el flujo de objetos (/Type /ObjStm), los cuales crean un punto ciego para un escáner de bytes simple. Un catálogo, un OutputIntent, un diccionario de acciones, cualquier elemento que no sea en sí mismo un flujo, puede comprimirse mediante Flate dentro de un ObjStm. Si escanea la estructura sin procesar no verá nada de esto, y reportará como correcto un archivo que dista de serlo
PdfExpandObjectStreams cierra esa brecha. Antes de ejecutar cualquier comprobación, el validador realiza Data := PdfExpandObjectStreams(Data). La rutina localiza cada ObjStm, lee su cabecera /N y /First para obtener los números y desplazamientos de los objetos contenidos, descomprime el cuerpo con PdfInflate (la biblioteca zlib de RTL, System.ZLib en Delphi y zstream en FPC) y añade cada objeto contenido como un elemento estándar N 0 obj ... endobj al final de una copia de los bytes. Las comprobaciones de tokens existentes encuentran entonces esos objetos sin cambiar su lógica
Dos restricciones hacen que esto sea limpio en lugar de inestable. Los objetos de flujo (los metadatos, el perfil ICC y los programas de fuentes) no pueden residir en un flujo de objetos (solo los diccionarios que no son de flujo pueden hacerlo), por lo que la expansión solo interactúa con diccionarios y los objetos añadidos no contienen la palabra clave stream que afectaría al paso de vaciado de cuerpos. Y debido a que el contenido añadido se ubica después de %%EOF, la búsqueda inversa desde startxref sigue localizando el bloque final (trailer) original. El trailer del flujo de referencias cruzadas en sí ya se manejaba antes, en la versión v1.49.3, leyendo Root, Size e ID directamente del diccionario plaintext de flujo de referencias cruzadas (xref-stream), un tema explorado en el artículo complementario sobre validar flujos de objetos y de referencias cruzadas; el trabajo en los flujos de objetos solo requería añadir el paso de descompresión (inflate), sin necesidad de decodificar entradas xref de tipo 2 ni de procesar un predictor PNG
Los límites reales de un comprobador a nivel de bytes
Esta es una herramienta de verificación previa, no un validador certificado, y los límites son reales. La incrustación de fuentes es una heurística de conteo, y resolverla correctamente requirió una corrección que vale la pena conocer. La comprobación original utilizaba PdfCountName('/FontDescriptor'), pero cada fuente aporta dos tokens /FontDescriptor (una referencia desde el diccionario de la fuente y un /Type en el propio objeto descriptor), por lo que el conteo era 2N frente a N programas incrustados y la prueba siempre resultaba verdadera. La solución es PdfCountDescriptorRefs, que cuenta únicamente la forma de referencia /FontDescriptor N G R (una por fuente) y genera pvaiUnembeddedFont solo cuando los programas incrustados son realmente menos:
K := PdfCountDescriptorRefs(Struct); // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Incluso corregido, es aproximado: un documento mixto donde cada descriptor contenga algún archivo de fuente (FontFile) puede permitir que una fuente individual no conforme pase desapercibida. Expandir los flujos de objetos también tiene un efecto secundario conocido: expone los recursos predeterminados standard-14 que contiene un /DR de AcroForm, como /Helv, y la heurística los reporta como no incrustados, aunque veraPDF los aprueba porque nunca se utilizan realmente para el renderizado. Las comprobaciones a nivel de operadores de flujos de contenido (6.2.10) quedan fuera de alcance por completo, ya que requerirían un análisis completo del contenido en lugar de un escaneo de bytes. Utilice el validador como un filtro rápido y sin dependencias que detecta las infracciones que la inyección de marcadores no puede solucionar, y reserve un validador completo para la transformación final
Esta es la mitad de verificación de la historia. La parte complementaria de escritura, donde SaveAsPdfA inyecta el perfil XMP, OutputIntent y el perfil sRGB ICC, y degrada correctamente una solicitud de Nivel A que no tenga estructura etiquetada, se basa en el mismo mecanismo a nivel de bytes. Ambas partes se distribuyen en el PDFium Component para Delphi, un único paquete VCL sobre una implementación PDF/A en Pascal puro sin entornos de ejecución externos que instalar