Un archivo de facturas puede renderizar limpio, imprimir, incrustar cada fuente y llevar un output intent válido, y un validador PDF/A igualmente lo rechazará porque un diccionario de recursos sostiene 4.096 entradas. El techo es 4.095. PDF/A hereda una pequeña tabla de límites de implementación del PDF Reference, y un documento una unidad por encima de cualquiera de ellos falla la conformidad aunque ningún visor se queje
El otro fallo que aterriza en el mismo balde de «pero se ve bien» es una fuente TrueType simbólica que carga una entrada /Encoding. Los checkboxes, los códigos de barras y los dingbats salen bien en pantalla, y el validador igualmente marca el diccionario de la fuente. Ambos problemas viven en el grafo de objetos en lugar de en cualquier cosa que una persona pueda ver en la página, así que vale la pena verificarlos mecánicamente antes de que un documento entre al archivador
Los números que PDF/A hereda, y dónde se sienta el límite
PDFium Component verifica cuatro límites de contenedor en cada validación PDF/A: un token de nombre puede tener a lo más 127 bytes de longitud, un arreglo puede sostener a lo más 8.191 elementos, un diccionario puede sostener a lo más 4.095 entradas, y arreglos y diccionarios pueden anidar a lo más 28 niveles de profundidad. Las cuatro fronteras son inclusivas. Un nombre de 127 bytes pasa y un nombre de 128 bytes falla; un arreglo de exactamente 8.191 elementos pasa y uno más lo falla
Los límites de nombre, arreglo y diccionario vienen de la era en que solo se exigía a un lector conforme manejar estructuras hasta esos tamaños. Un visor de escritorio actual puede abrir un arreglo mucho más grande sin queja, pero un documento de archivado se supone que se abre en lectores que nadie ha probado todavía, incluidos los viejos que un sistema de records puede seguir corriendo. Por eso PDF/A conserva los techos históricos en lugar de relajarlos
Inclusivo contra exclusivo es donde las verificaciones hechas a mano suelen equivocarse. Un test escrito como Count >= 8191 en lugar de Count > 8191 rechaza documentos que cumplen, y un validador que rechaza documentos conformes tiende a creérsele de todas formas, así que el generador se «arregla» por un problema que jamás tuvo. La función ValidatePdfACompliance en la unidad FPdfPdfa cuenta estrictamente más allá del límite, y lee los bytes del archivo directamente, así que la verificación corre sin cargar la DLL PDFium
uses
System.Classes, FPdfPdfa;
procedure CheckContainerLimits(const FileName: string; Log: TStrings);
var
Src: TFileStream;
Res: TPdfAValidationResult;
begin
Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Res := ValidatePdfACompliance(Src); // escaneo de bytes, sin necesidad de la DLL PDFium
if pvaiNameOverLimit in Res.Issues then
Log.Add('A name token is longer than 127 bytes');
if pvaiArrayOverLimit in Res.Issues then
Log.Add('An array holds more than 8191 elements');
if pvaiDictOverLimit in Res.Issues then
Log.Add('A dictionary holds more than 4095 entries');
if pvaiNestingOverLimit in Res.Issues then
Log.Add('Arrays and dictionaries nest deeper than 28 levels');
if Res.IsCompliant then
Log.Add('No PDF/A findings');
finally
Src.Free;
end;
end;
Las cuatro banderas son miembros del conjunto Issues en TPdfAValidationResult, junto a cualquier otro hallazgo PDF/A, y IsCompliant devuelve True solo cuando se encontró una identificación PDF/A y el conjunto está vacío. Un archivo que no carga ninguna identificación PDF/A se verifica contra el conjunto de reglas estricto de PDF/A-1, así que los límites de contenedor igual le aplican
¿Qué generadores chocan con estos límites?
Casi siempre los que construyen estructura en un bucle. Un formulario con varios miles de campos produce un arreglo AcroForm /Fields, o un arreglo /Annots de página, que camina más allá de 8.191 entradas. Un motor de reportes que registra un recurso de fuente o imagen por objeto generado, y jamás reutiliza una entrada existente, hace crecer un sub-diccionario /Resources más allá de 4.095 claves. Un documento etiquetado cuyo árbol de estructura se produce recursando sobre un modelo de datos anidado puede pasar de 28 niveles sin que nadie mire
Los nombres largos tienen un origen distinto: datos empacados dentro de tokens de nombre. Un colorante de separación nombrado por una referencia de cliente, un grupo de contenido opcional nombrado por una ruta de archivo, una etiqueta de campo ensamblada desde seis niveles de jerarquía. Los nombres no cuestan nada de generar, y 127 bytes se acaban rápido en cuanto interviene una etiqueta UTF-8 con caracteres acentuados, porque cada uno de esos caracteres toma dos o tres bytes. Los 127 bytes se cuentan después del introducidor /, hasta el siguiente delimitador o espacio en blanco
El remedio es estructural todas las veces. Parta el arreglo, parta el diccionario, aplane la recursión, acorte el nombre. Inyectar marcadores PDF/A en el archivo no puede ayudar con ninguno de estos hallazgos, ya que el problema es la forma del grafo de objetos y los metadatos XMP no dicen nada al respecto
Cómo el escaneo cuenta elementos y entradas
La regla de conteo importa cuando intenta reproducir un hallazgo en un editor hexadecimal. Un arreglo o diccionario anidado cuenta como un elemento de su padre, que es como se enuncia el límite: un arreglo de 10 elementos de arreglos de 1.000 elementos está bien, porque ningún arreglo individual excede 8.191. Dentro de un diccionario solo se cuentan las claves, así que un valor que resulta ser un nombre no infla el total de entradas. Las cadenas hexadecimales como <FEFF0041> se saltan como tokens individuales, así que sus corchetes angulares jamás se confunden con anidamiento de diccionario
El escaneo cubre la estructura de objetos del archivo completo, incluidos los objetos almacenados dentro de object streams comprimidos, que se expanden antes de contar. Los cuerpos de los streams en sí, como el contenido de página y los datos de imagen, se borran primero, así que una corrida de bytes binarios que por casualidad contenga un / o un [ no puede disparar un hallazgo falso de nombre o arreglo
Hay un límite numérico adyacente que la misma validación reporta bajo su propia bandera. Cuando el archivo declara PDF/A-2 o una parte posterior, pvaiIntegerImplementationLimit se levanta para cualquier token entero fuera del rango con signo de 32 bits, o sea fuera de -2.147.483.648 a 2.147.483.647. Los generadores que escriben desplazamientos de bytes o contadores grandes como enteros planos son la fuente usual
¿Por qué una fuente TrueType simbólica debe soltar /Encoding?
ISO 19005-1 §6.3.7 admite solo el cmap integrado del programa de fuente como el mapeo de caracteres para una fuente TrueType simbólica. Una fuente simbólica mapea códigos a glifos a sus propias reglas; eso es lo que declara la bandera Symbolic. Agregue una entrada /Encoding y el archivo ahora da dos respuestas a la pregunta «qué glifo selecciona el byte 0x41», sin nada en el archivo que diga cuál gana. Los lectores resuelven el conflicto de manera distinta, y el mismo checkbox puede salir como una marca en un visor y como la letra A en otro
ValidatePdfACompliance encuentra cada diccionario de fuente con /Subtype /TrueType, verifica si tiene una clave /Encoding, y luego lee el bit 3 (valor 4) de la entrada /Flags en el descriptor de fuente. El descriptor se sigue tanto si está escrito en línea como si se referencia como objeto indirecto. Una fuente TrueType no simbólica que conserva /WinAnsiEncoding o /MacRomanEncoding no es marcada por esta verificación, porque para fuentes no simbólicas PDF/A espera exactamente esa codificación
// Res proviene de ValidatePdfACompliance como en el primer ejemplo
if pvaiSymbolicTrueTypeEncoding in Res.Issues then
Log.Add('A symbolic TrueType font carries /Encoding; PDF/A admits only ' +
'the font''s built-in cmap (ISO 19005-1 6.3.7)');
// Una regla de fuente vecina con disparador distinto: una fuente TrueType
// NO simbólica cuyo diccionario Encoding lleva /Differences
if pvaiTrueTypeDifferencesForbidden in Res.Issues then
Log.Add('A non-symbolic TrueType Encoding dictionary carries /Differences');
Las fuentes que tropiezan con esta regla son predecibles: Symbol, Wingdings, fuentes de códigos de barras, fuentes de iconos. Aparecen en documentos de negocio como checkboxes, estrellas de calificación y códigos de barras, y un paso de subconjunto que trata todas las fuentes TrueType de la misma manera les escribe una codificación también. Son además las fuentes que nadie mira cuando un validador dice «problema de fuente», porque las fuentes del texto del cuerpo se verifican primero
El error opuesto también aparece en la naturaleza: un productor pone la bandera Symbolic en una fuente de texto ordinaria que realmente está codificada en WinAnsi. La verificación PDF/A sigue la cláusula como está escrita y reporta esa fuente, y aquí la reparación pertenece al descriptor: corrija la bandera en el descriptor en lugar de borrar la codificación. Quitarle /Encoding a una fuente de texto mal etiquetada dejaría al texto dependiente del cmap que el subconjunto traiga de casualidad, que es un archivo peor que el que usted tenía. Mire el conjunto de glifos de la fuente antes de elegir qué mitad de la contradicción quitar
Leer los hallazgos en un reporte preflight
La unidad de reportes preflight viene con la edición Enterprise, mientras que ValidatePdfACompliance está en todas las ediciones. El reporte en la unidad FPdfPreflightReport ordena los cuatro límites de contenedor en la categoría Structure y el issue de codificación TrueType simbólica en la categoría Content. Esa división es deliberada. Los hallazgos de estructura usualmente vuelven a quien mantiene el generador, mientras que los hallazgos de contenido usualmente van a quien suministró las fuentes y las plantillas, y un reporte que los mezcla manda a ambas personas persiguiendo problemas que no son suyos
Cada issue lleva su nombre de enum como Code y una recomendación que nombra el remedio en términos concretos, por ejemplo «Split dictionaries so none carries more than 4095 entries». Note que la categoría Structure también sostiene otros dos hallazgos, una estructura etiquetada ausente en una reclamación de Level A y streams de referencia cruzada u objetos PDF 1.5 en un archivo PDF/A-1, así que filtre por código cuando solo quiera los cuatro límites
uses
System.SysUtils, System.Classes, PDFium, FPdfPreflightReport;
procedure ReportStructureFindings(const FileName: string; Log: TStrings);
var
Pdf: TPdf;
Report: TPdfPreflightReport;
Issues: TPdfPreflightIssues;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // esta ruta carga la DLL PDFium
if not Pdf.Active then
Exit;
Report := BuildPdfPreflightReport(Pdf, [ppsPdfA]);
Issues := Report.FilterByCategory('Structure').FlattenIssues;
for I := 0 to High(Issues) do
Log.Add(Issues[I].Code + ': ' + Issues[I].Recommendation);
Report.SaveJsonToFile(ChangeFileExt(FileName, '.preflight.json'));
finally
Pdf.Free;
end;
end;
Las dos rutas responden preguntas ligeramente distintas. BuildPdfPreflightReport pasa por TPdf.ValidatePdfA, que guarda el documento abierto a una copia temporal, corre el mismo escaneo de bytes sobre esa copia y luego agrega verificaciones que necesitan el motor PDFium, como fuentes que se usan en una página pero no están incrustadas. ValidatePdfACompliance juzga los bytes exactos en disco. Para límites de implementación, la ruta de bytes sobre el archivo original es la que hay que creer: reporta sobre el archivo que está a punto de archivar, y corre en un paso de build o un servicio bloqueado donde una DLL nativa no puede desplegarse. Use la ruta del reporte cuando también quiera las verificaciones de nivel de motor y la salida JSON, SARIF o JUnit para un pipeline
Por dónde seguir
Correr estas verificaciones sobre una carpeta, decidir qué hallazgos bloquean un release y convertir el resultado en un reporte que la gente leerá están cubiertos en el recorrido de validación preflight PDF/A en Delphi y en el CLI de reportes preflight por lotes. Qué parte de PDF/A y nivel de conformidad apuntar para empezar es el tema de las notas sobre conformidad de archivado PDF/A; esa elección decide, entre otras cosas, si el límite de enteros aplica
Los validadores mostrados aquí vienen con PDFium Component para Delphi, C++Builder y Lazarus, junto al renderizado, llenado de formularios y edición sobre el motor PDFium. La página del producto PDFium Component lista los estándares de conformidad soportados, las ediciones y las versiones de compiladores