PDFlibPas asocia un fichero incrustado a una página concreta en lugar de al documento en su conjunto, escribiendo un array /AF en el diccionario de la página mientras el propio contenido sigue registrado en el árbol de nombres EmbeddedFiles del documento. Ese reparto es lo que describe el ISO 32000-2 §14.13, y es lo que permite a un lector responder la pregunta que un adjunto a nivel de documento no puede: a qué página pertenecen estos datos
Los casos de uso son más específicos que los adjuntos genéricos. Un informe topográfico donde cada página lleva la serie bruta de mediciones que hay detrás de su gráfico. Un lote escaneado donde cada página conserva el resultado del OCR que produjo su capa de texto. Un juego de planos donde cada hoja lleva la extracción CAD desde la que se renderizó. En cada caso, una lista de adjuntos a nivel de documento sería un montón de ficheros cuyos nombres codifican números de página, que es una convención, no una estructura
Un contenido, dos sitios desde los que se referencia
El punto estructural importante es que la asociación a nivel de página no crea una segunda copia de nada. El fichero se incrusta una vez y se registra en el árbol de nombres EmbeddedFiles igual que un adjunto a nivel de documento, usando la misma maquinaria de especificación de fichero. Lo que cambia es dónde se escriben la referencia y su clave de relación: en el diccionario de la página en vez de en el catálogo del documento
Se siguen dos consecuencias. Primera: un lector que solo conoce los adjuntos a nivel de documento sigue encontrando el contenido, porque está en el árbol de nombres donde ese lector busca. Segunda: limpiar la asociación de página elimina el enlace, no el fichero. ClearPageAssociatedFiles desvincula la página de sus ficheros asociados y deja los contenidos accesibles a través del árbol de nombres, que es el comportamiento conservador: una operación que dice limpiar la asociación no debería destruir silenciosamente datos que otra parte del documento puede referenciar
Esa función tiene una condición de éxito deliberadamente estrecha que conviene conocer. Solo informa de éxito cuando la página realmente llevaba una clave /AF. Una página que nunca tuvo asociaciones devuelve fallo en lugar de una confirmación optimista, así que quien la llama no puede confundir un no-op con una limpieza completada
var
Lib: TPDFlib;
Idx, I: Integer;
begin
Lib := TPDFlib.Create(nil);
try
Lib.LoadFromFile('survey-report.pdf');
// Adjunta la serie de mediciones que produjo el gráfico de la página 3
Idx := Lib.AddPageAssociatedFileFromFile(3,
'series-03.csv', // fichero en disco
'measurements.csv', // nombre visible dentro del PDF
'text/csv', // tipo MIME
'Raw measurement series for figure 3',
'Data'); // AFRelationship, ISO 32000-2 14.13
if Idx < 0 then
raise Exception.Create('page association refused');
for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
Writeln('page 3 associated file, embedded index ',
Lib.GetPageAssociatedFileEmbeddedIndex(3, I));
Lib.SaveToFile('survey-report-with-data.pdf');
finally
Lib.Free;
end;
end;
La cadena de relación no es texto libre en la práctica. El ISO 32000-2 define un vocabulario, Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema y Unspecified, y los consumidores se apoyan en él. Data para los números que hay detrás de un gráfico, Source para el documento del que se generó una página, Alternative para una representación equivalente. Escoja del vocabulario aunque nada de su pipeline lo lea todavía, porque la siguiente herramienta de la cadena podría hacerlo
¿Por qué la misma búsqueda necesita FollowRef en ambas direcciones?
Porque seguir referencias responde a dos preguntas distintas, y el código tiene que saber cuál está haciendo. Una búsqueda por clave que sigue referencias indirectas devuelve el objeto al que la referencia apunta. Una búsqueda que no las sigue devuelve la propia referencia. Ambas son correctas, y usar la equivocada produce un mal comportamiento silencioso en lugar de un error
Leer un fichero asociado demuestra la primera dirección. Para obtener el número de objeto del stream incrustado que hay detrás de las claves /EF y /F de la especificación de fichero, la búsqueda no debe seguir, porque seguir resuelve la referencia en el objeto stream y el número de objeto desaparece. La regla se generaliza: cualquier ruta de código que necesite la identidad de un objeto en lugar de su contenido tiene que tomar la referencia cruda
El contenido opcional muestra la dirección contraria, y costó más de encontrar. El diccionario de propiedades del contenido opcional se escribe en el catálogo como objeto indirecto, así que el código que lo lee de vuelta sin seguir obtiene una referencia en lugar de un diccionario. La comprobación de tipo sobre ese valor falla entonces, y la rama de respaldo natural, si no hay configuración, crear una, se ejecuta y sobrescribe la configuración que ya estaba ahí. Nada lanza una excepción. Las capas descritas en los grupos de contenido opcional y las capas simplemente pierden su estado de visibilidad por defecto
La lección se generaliza más allá de ambos casos. Cuando una búsqueda puede devolver o una referencia o el objeto, una comprobación de tipo a secas no es manejo de errores: es una rama que tarde o temprano se tomará por la razón equivocada. Decida explícitamente qué necesita cada punto de llamada, y prefiera la API pública que responde directamente a la pregunta, como una propiedad de recuento de contenido opcional, antes que meterse en un accessor protegido del diccionario del catálogo
// Los adjuntos a nivel de documento y las asociaciones a nivel de página
// coexisten; un fichero incrustado también puede marcarse como asociado
if Lib.IsEmbeddedFileAssociated(0) = 0 then
Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');
Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files : ',
Lib.GetPageAssociatedFileCount(3));
// Limpiar desvincula la página; el contenido sigue en el árbol de nombres
if Lib.ClearPageAssociatedFiles(3) > 0 then
Writeln('page 3 associations removed, payloads still reachable');
Qué hacen los modos de conformidad con los adjuntos
Los perfiles de archivo restringen qué se puede incrustar, y la restricción se aplica en el punto de entrada en lugar de en el momento de guardar. PDF/A-1 prohíbe por completo los ficheros incrustados, PDF/A-2 solo permite documentos PDF/A incrustados, y PDF/A-3 es el perfil que abrió la incrustación a tipos de fichero arbitrarios, que es precisamente por lo que los formatos de factura híbridos se construyen sobre él
PDFlibPas rechaza el adjunto cuando el modo de conformidad activo no lo permite, en la llamada, no cientos de operaciones después durante la salida. Es una elección deliberada sobre dónde es más barato actuar ante un error: un rechazo en el punto de llamada nombra el fichero que estaba añadiendo, mientras que un rechazo al guardar nombra un documento y le deja a usted averiguar cuál de cuarenta adjuntos lo causó
También por esto los ficheros asociados aparecen tantas veces en la facturación electrónica. Una factura híbrida es un PDF que lee un humano con un contenido XML legible por máquina adjunto y marcado con la relación correcta, y tanto el perfil del contenedor como la clave de relación son parte de la especificación, no convenciones. Esa construcción se cubre en la construcción de facturas híbridas Factur-X y ZUGFeRD, con la parte de metadatos en el esquema de extensión XMP del PDF/A-3
¿Cuándo conviene la asociación por página en lugar de por documento?
Cuando un consumidor necesita saber a qué página pertenecen los datos, y solo entonces. Los adjuntos a nivel de documento son más simples, están más extendidos entre los visores y bastan siempre que el contenido describa el documento entero: un XML de factura, un manifiesto de firma, un archivo fuente. Acuda a la asociación a nivel de página cuando el contenido esté genuinamente acotado a la página y la identidad de la página forme parte de su significado
El soporte es la restricción práctica. Los ficheros asociados a nivel de página son una construcción de PDF 2.0, y el soporte en visores es más escaso que el de los adjuntos a nivel de documento. Como el contenido está en el árbol de nombres de cualquier forma, un visor que ignora /AF en las páginas sigue mostrando el fichero en su lista de adjuntos, así que la degradación es elegante. Pero si el enlace de página es esencial para su consumidor y no solo un metadato útil, verifique el lector al que realmente apunta en lugar de suponer
Los ficheros asociados a nivel de página, los adjuntos a nivel de documento y las compuertas de perfil de archivo que gobiernan a ambos se envían en la biblioteca PDF Delphi PDFlibPas. Si además repara ficheros antiguos en la entrada, el trabajo de metadatos y conformidad de la conversión a PDF/A con reparación de metadatos es lo que decide, en primer lugar, qué rutas de adjuntos tiene disponibles