Usted escribe un pequeño validador. Abre un PDF, busca al final, encuentra startxref, lee el desplazamiento y espera aterrizar en la palabra clave xref con una tabla de referencia cruzada de ancho fijo debajo de ella. A partir de esa tabla recopila los desplazamientos de los objetos, luego escanea hacia atrás en busca de la palabra clave trailer para conocer el /Root y el /Size. Funciona perfectamente en cada archivo que generó para probarlo. Luego llega un archivo producido por una versión actual de Word, o por una biblioteca que apunta a PDF 1.5, y el validador lo declara dañado. No hay ninguna palabra clave xref donde apunta el desplazamiento, ningún diccionario trailer por ningún lado, y la tabla de objetos que construyó el validador está casi vacía. El archivo es válido; el validador lo está leyendo a través de un lente de hace quince años.
Esta es la razón más común por la que una verificación de PDF a nivel de bytes escrita contra el diseño clásico falla en documentos modernos. La estructura de la que depende, la tabla de referencia cruzada en texto plano y la palabra clave trailer, se convirtieron en opcionales en PDF 1.5 y con frecuencia están ausentes. Dos características las reemplazaron: el flujo de referencia cruzada (cross-reference stream) y el flujo de objetos comprimidos (compressed object stream). Ambos se describen en ISO 32000-1, y un validador que no los conozca verá un archivo saludable como un cúmulo de objetos faltantes.
Qué cambió PDF 1.5 acerca del final del archivo
ISO 32000-1 §7.5.8 define el flujo de referencia cruzada, y §7.5.7 define el flujo de objetos de tipo /ObjStm. Juntos permiten que un escritor descarte las dos estructuras en las que se basa un analizador clásico. Un archivo PDF 1.5 puede terminar sin ninguna tabla xref. En su lugar, el objeto al que apunta startxref es un objeto de flujo común cuyo diccionario contiene /Type /XRef, y ese flujo almacena los datos de referencia cruzada en una forma binaria compacta. Tampoco hay una palabra clave trailer, porque el trailer es ahora el propio diccionario del flujo. Las claves que buscaba un analizador clásico, /Root, /Size e /ID, viven dentro de ese diccionario.
El segundo cambio mueve los objetos mismos. En lugar de escribir cada objeto indirecto en su propio desplazamiento de bytes, un escritor puede empaquetar muchos objetos pequeños (los diccionarios de página, los diccionarios de anotaciones, el árbol de estructura) en un único flujo de objetos y comprimir todo el contenedor con Flate. Los objetos individuales ya no tienen un desplazamiento de bytes en el archivo: tienen una posición dentro de un bloque comprimido. Un validador que escanee los bytes crudos en busca de 1 0 obj nunca los encontrará, porque ese texto solo existe después de la descompresión. Para un analizador clásico, la mitad del documento simplemente ha desaparecido.
Las claves del trailer están en texto plano, incluso en un archivo comprimido
La parte tranquilizadora es que leer el trailer de un flujo de referencia cruzada no requiere descomprimir nada. Un objeto de flujo se escribe como un diccionario seguido de la palabra clave stream y luego los bytes comprimidos. El diccionario está en texto plano. De modo que cuando startxref apunta a un flujo de referencia cruzada, los bytes inmediatamente posteriores al número de objeto se ven como un diccionario común, y /Root, /Size e /ID se ubican allí a la vista, antes de que comiencen la palabra clave stream y los datos Flate.
Esto significa que un validador puede conocer los tres datos que más necesita (dónde está el catálogo, cuántos objetos reclama el archivo y el identificador del archivo) analizando únicamente el diccionario del flujo. No tiene que descomprimir los datos de referencia cruzada y no tiene que interpretar las entradas binarias dentro de ellos. La tarea que supera a un analizador ingenuo no es leer el trailer; es encontrar los objetos. Esos son dos problemas separables, y resolver el primero es de bajo costo.
Flujos de objetos: un encabezado y luego un bloque Flate
Un flujo de objetos es un contenedor. Su diccionario contiene /Type /ObjStm, una entrada /N que indica la cantidad de objetos empaquetados en su interior, y una entrada /First que proporciona el desplazamiento de bytes, dentro de los datos descomprimidos, donde comienza el cuerpo del primer objeto. La carga útil comprimida, una vez descomprimida, comienza con un pequeño encabezado de /N pares de enteros. Cada par es un número de objeto y el desplazamiento del cuerpo de ese objeto en relación con /First. Después del encabezado vienen los cuerpos de los objetos mismos, concatenados.
Expandir uno es un proceso mecánico una vez que los bytes están descomprimidos. Se lee el diccionario para obtener /N y /First, se descomprime el flujo con un decodificador Flate, se recorren los /N pares iniciales para saber qué número de objeto vive en cada desplazamiento y luego se extrae cada cuerpo como si fuera un objeto indirecto común. La única dependencia real es el decodificador Flate, y usted ya tiene uno: Delphi incluye System.ZLib y Free Pascal incluye la unidad zstream, ambos de los cuales envuelven zlib y descomprimen un flujo Flate sin código de terceros. Una rutina que anexe cada objeto extraído a la tabla de objetos del validador hace que el resto de este, es decir, la sección que recorre /Root y verifica el árbol de páginas, se comporte exactamente como lo haría con un archivo clásico.
Lo que no es necesario implementar
Es fácil sobreestimar el trabajo. Leer las claves de trailer de un archivo comprimido no requiere decodificar las entradas binarias del flujo de referencia cruzada. El flujo de referencia cruzada de la sección §7.5.8 utiliza tres tipo de entradas, y la entrada de tipo 2, la que dice este objeto vive dentro del flujo de objetos N en el índice i
, es la que decodificaría para construir un mapa de desplazamiento completo. Necesita ese mapa para resolver objetos arbitrarios por número. No lo necesita para leer /Root, /Size e /ID, que están en el diccionario de texto plano, y no lo necesita para expandir flujos de objetos, porque cada /ObjStm anuncia su propio contenido a través de /N y /First.
Tampoco tiene que manejar las funciones de predicción PNG y TIFF que un flujo de referencia cruzada puede aplicar a través de su /DecodeParms solo para obtener las claves de trailer. Los predictores filtran las filas de referencia cruzada binaria para que se compriman mejor; no tienen nada que ver con el diccionario que precede al flujo. La actualización mínima para que un validador clásico reconozca los PDF modernos es, por lo tanto, pequeña: cuando startxref aterriza en un flujo en lugar de la palabra clave xref, analice el diccionario del flujo en busca de las claves de trailer y expanda cualquier objeto /ObjStm que encuentre para que su contenido entre en la tabla de objetos. Decodificar entradas de tipo 2 y predictores es una tarea independiente y más grande que puede posponer hasta que realmente necesite resolución de objetos aleatorios.
Por qué una comprobación de conformidad debe expandir los flujos primero
Esto deja de ser académico en el momento en que se ejecuta una comprobación de perfil. Un validador de PDF/A o PDF/X inspecciona objetos específicos: el catálogo del documento para un arreglo /OutputIntents, el flujo de /Metadata para un paquete XMP con el identificador correcto, cada descriptor de fuente para un archivo de fuente incrustado y el trailer para un /ID. En un archivo comprimido, la mayoría de esos objetos están dentro de flujos de objetos. Un validador que no ha expandido los flujos de objetos no puede ver las claves del catálogo, no puede encontrar los metadatos y no puede enumerar las fuentes. Reportará un documento perfectamente conforme como si le faltara su intención de salida, sus metadatos XMP y la mitad de su estructura, porque la evidencia que necesita todavía está en un bloque Flate que nunca descomprimió.
El orden importa. La expansión tiene que ocurrir antes de que se ejecuten las comprobaciones, no junto con ellas, porque cada comprobación asume que puede alcanzar un objeto por número. Si desea construir una comprobación de perfil directamente sobre un escaneo de bytes en bruto, heredará la ceguera del analizador clásico y producirá falsas violaciones precisamente en los archivos modernos que tienen más probabilidades de estar bien formados, ya que provienen de cadenas de herramientas lo suficientemente nuevas como para escribir flujos de referencia cruzada en primer lugar.
Permitir que PDFium haga el análisis por usted
El componente PDFium analiza los flujos de referencia cruzada y los flujos de objetos como parte de la carga de un documento, que es la forma práctica de evitar implementar manualmente el paso de descompresión y expansión. Cuando carga un archivo con el componente TPdf, los objetos empaquetados en contenedores /ObjStm ya están resueltos, y los puntos de entrada de validación ven el documento completamente expandido. ValidatePdfA devuelve un registro TPdfAValidationResult cuyo campo Conformance es un valor TPdfAConformance como pac1b o pacNone, cuyo campo Issues es un conjunto de los problemas específicos encontrados y cuyo método IsCompliant es verdadero solo cuando se detectó un nivel de conformidad y el conjunto de problemas está vacío. Debido a que los objetos se expandieron durante la carga, se encuentra un arreglo /OutputIntents o una fuente incrustada que vivía dentro de un flujo de objetos, en lugar de informarse como faltante.
uses
PDFium, FPdfPdfa;
function CheckPdfA(const FileName: string): TPdfAValidationResult;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // parses xref/object streams on load
Result := Pdf.ValidatePdfA; // sees the expanded object table
finally
Pdf.Free;
end;
end;
Lo mismo se aplica a ValidatePdfX, que devuelve un TPdfXValidationResult con la misma estructura. El punto de enrutar a través de PDFium es que la descompresión estructural descrita anteriormente ocurre una vez, de manera correcta, dentro del cargador, por lo que su código de validación nunca ve la diferencia entre un archivo clásico y uno completamente comprimido. Ambos llegan al validador como un conjunto resuelto de objetos.
var
Pdf: TPdf;
R : TPdfXValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Press_Ready.pdf';
Pdf.Active := True;
R := Pdf.ValidatePdfX;
if R.IsCompliant then
Writeln('PDF/X conformance: ', Ord(R.Conformance))
else
Writeln('Not conformant; issue count = ', SizeOf(R.Issues));
finally
Pdf.Free;
end;
end;
Si los bytes ya están en memoria en lugar de en el disco, la misma secuencia de cargar y luego validar funciona a través de la sobrecarga LoadDocument(const Data: TBytes), que toma el contenido del archivo crudo y analiza sus flujos de referencia cruzada y de objetos de la misma manera que lo hace la ruta del archivo. Lo que debe recordar para un validador escrito a mano es la regla estructural, no la API: lea las claves de trailer del diccionario del flujo en texto plano, expanda cada /ObjStm con un decodificador Flate antes de recorrer el documento y trate la decodificación de las entradas de referencia cruzada binarias como la tarea más grande y opcional que es.
Una vez que la estructura está expandida, un validador puede ejecutar el resto de un flujo de trabajo sobre ella. Para un arnés de preflight por línea de comandos que reporta la conformidad en una carpeta de entradas, consulte nuestro recorrido sobre cómo construir una CLI de informe de preflight por lotes. Cuando la validación es un filtro previo a la división de un documento grande, las técnicas de nuestra guía para dividir documentos PDF en varios archivos se complementan naturalmente con el patrón de carga y verificación que se muestra aquí. Ambos se basan en la superficie de carga y validación de PDFium Component para Delphi y C++Builder.