Toma un archivo cuyo diccionario de información dice /Title (Annual Maintenance Report) mientras que su paquete XMP no tiene ningún dc:title. Los visores muestran el título sin queja, pero un validador PDF/A rechaza el archivo porque las dos copias de metadatos discrepan. Un segundo archivo tiene igual de buena salud pero no tiene entrada pdfaid:part en su XMP, y el validador responde con docenas de errores: juzga el archivo como un PDF ordinario y reporta todas las reglas de la parte reclamada como suspendidas, todo por una línea ausente
Los dos problemas necesitan cantidades de trabajo muy distintas. Reconciliar metadatos es un arreglo estrecho que puedes correr sobre cualquier archivo. Convertir un PDF cualquiera en uno archivable significa quitar lo que la parte objetivo prohíbe, añadir lo que exige, declarar la reclamación y luego validar los bytes que realmente salieron. Construimos la conversión alrededor de ese último paso a propósito. ConvertToPDFA, que viene con la edición Enterprise (TPDFlibEnterprise) junto con GetPDFAConversionReport, guarda el documento convertido en memoria, corre la comprobación PDF/A sobre esos bytes y devuelve 1 solo cuando esa comprobación pasa
¿Por qué un PDF que se abre bien suspende una comprobación PDF/A?
Los dos fallos vienen de reglas de metadatos. Un PDF registra su título, autor, productor y fechas en el diccionario de información del documento, y PDF/A también exige los mismos datos en un paquete de metadatos XMP; ISO 19005-1 §6.7.3 pide que ambos sean equivalentes, y PDF/A-2 y PDF/A-3 mantienen la regla. Entre los archivos que fallan por esta regla, el patrón habitual es un productor que llenó el diccionario de información y nunca escribió la mitad XMP, así que el validador encuentra un título en un sitio y nada en el otro
La identificación ausente es más silenciosa. Las entradas pdfaid:part y pdfaid:conformance son lo que marca un archivo como PDF/A, y un archivo puede estar perfectamente bien formado sin ellas. El caso que más vemos es una aplicación que suministra su propio paquete XMP, reemplazando el que generó el escritor de PDF; el paquete personalizado lleva los campos de la aplicación y ninguna entrada pdfaid, así que la lista de errores no apunta ni de cerca a la causa real
RepairDocumentMetadata maneja ambos casos y devuelve el número de entradas que cambió. Está disponible en todas las ediciones, igual que SetPDFAMode y SanitizeDocument. La reparación recorre Title, Author, Subject, Keywords, Creator, Producer, CreationDate y ModDate. Donde solo el XMP lleva un valor, el diccionario de información se rellena a partir de él, así que nada de lo ya registrado se tira. Donde ambos llevan valores y difieren, gana el diccionario de información. Tomamos esa decisión porque el diccionario de información es la mitad que los productores más suelen rellenar, lo que la hace el registro más probable de lo que el autor pretendía
Si tu XMP es la copia autoritativa (digamos que fue curada en un sistema de gestión documental), empuja los valores que quieres al diccionario de información con SetInformation y solo después corre la reparación. Hacerlo al revés sobreescribirá tu título XMP curado con lo que fuera que escribiera el productor original
La reparación de la identificación necesita una cosa de ti: el documento tiene que saber a qué parte apunta. Un archivo recién cargado no tiene ningún modo PDF/A fijado, así que llama a SetPDFAMode antes de la reparación. Con un modo en marcha, RepairDocumentMetadata escribe las entradas pdfaid correspondientes, añade pdfaid:rev con el año 2020 para los modos de la parte 4, y fija dc:format a application/pdf, que los validadores también comprueban
uses
SysUtils, PDFlibrary;
procedure RepairIncoming(const InName, OutName: string);
var
Lib: TPDFlib;
Fixed: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(InName, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [InName]);
// El título curado entra primero en el diccionario de información,
// porque esa copia gana cuando las dos mitades discrepan
Lib.SetInformation(2, 'Annual Maintenance Report 2025');
Lib.SetPDFAMode(3); // 3 = PDF/A-2b, mira la tabla de abajo
Fixed := Lib.RepairDocumentMetadata;
Writeln(Format('%d metadata entries brought into agreement', [Fixed]));
Lib.SaveToFile(OutName);
finally
Lib.Free;
end;
end;
Elegir la parte objetivo y su número de modo
Elige la parte a partir de lo que el documento tiene que llevar, y solo después mira el número. SetPDFAMode y ConvertToPDFA comparten un esquema de numeración, y no corre en el orden que los nombres de las partes sugieren
| Modo | Parte y nivel | Notas |
|---|---|---|
| 1 | PDF/A-1a | PDF 1.4, etiquetado |
| 2 | PDF/A-1b | PDF 1.4 |
| 3 | PDF/A-2b | PDF 1.7 |
| 4 | PDF/A-2a | PDF 1.7, etiquetado |
| 5, 6, 7 | PDF/A-3b, 3a, 3u | Ficheros incrustados de cualquier tipo permitidos |
| 8 | PDF/A-2u | PDF 1.7, texto Unicode requerido |
| 9, 10, 11 | PDF/A-4, 4e, 4f | PDF 2.0; 4e añade 3D y multimedia enriquecida, 4f permite ficheros incrustados |
El modo 3 es PDF/A-2b y el modo 4 es PDF/A-2a. Esa inversión caza a quienes cuentan desde 1a y asumen que el nivel a siempre va primero. PDF/A-2u aparece en el 8, después de los modos de la parte 3
La elección con consecuencias de verdad va de los adjuntos. Las partes 1 y 2 prohíben ficheros incrustados, así que ConvertToPDFA los elimina para los modos 1 a 4 y el modo 8. Convierte una factura Factur-X o ZUGFeRD al modo 3 y la factura XML estructurada desaparece de la salida, junto con la razón de ser del archivo. Para cualquier cosa que lleve un adjunto, apunta al modo 5 (PDF/A-3b), que además es la parte sobre la que se definen esos formatos de factura, o al modo 11 si necesitas PDF 2.0. Cada adjunto en un archivo de parte 3 o parte 4 debe declarar cómo se relaciona con el documento mediante /AFRelationship, y PDF Library for Delphi escribe esa entrada para cada fichero incrustado que guarda en esos modos
La parte 4 se identifica de forma distinta. No tiene letra de conformidad para el PDF/A-4 llano, usa E o F para las dos extensiones, y exige pdfaid:rev con el año en que se publicó su parte. ConvertToPDFA sube el archivo a PDF 2.0 para los modos 9 a 11, porque ISO 19005-4 §6.1.2 construye la parte sobre PDF 2.0, y la comprobación juzga esos archivos frente a PDF 2.0 en lugar de 1.7
Qué cambia ConvertToPDFA en el archivo
ConvertToPDFA hace una secuencia fija de reparaciones, anota cada una en un informe de texto plano, guarda el resultado en memoria y valida esos bytes. Una comprobación fallida devuelve 0, y también un modo fuera de 1 a 11, que devuelve antes de escribir informe alguno. La secuencia merece conocerse, porque cada paso es algo que la salida perderá o ganará:
- Un documento cifrado cuya contraseña se desconoce detiene la conversión enseguida con una línea
blocked:; nada de lo que hay dentro se puede leer, y menos aún reparar - El JavaScript de nivel documento y la acción de apertura que lo ejecuta se eliminan, junto con cada entrada de acciones adicionales y los tipos de acción que ninguna parte permite, entre ellos Launch, Sound, Movie, ResetForm, ImportData, Hide, SetOCGState, Rendition y Trans. Los enlaces GoTo y URI se quedan, así que la navegación sigue funcionando
- Los ficheros incrustados se eliminan para las partes que los prohíben, como se describió arriba
- Las anotaciones ocultas o puestas para no mostrarse nunca se borran. PDF/A exige que las anotaciones sean visibles e imprimibles, y revelarlas cambiaría el aspecto de la página, así que elegimos la eliminación
- Las apariencias de los campos de formulario se regeneran antes de limpiar el flag
/NeedAppearances, así que los valores rellenados siguen visibles - Las fuentes que se usan pero no están incrustadas se incrustan desde las fuentes instaladas en la máquina
- Los objetos a los que ya no referencia nada se sueltan, y se añade un output intent sRGB si el archivo no tiene ninguno
- Por último se declaran la parte y el nivel en el XMP, y se comprueban los bytes
En un servidor, el paso de fuentes es el que más probablemente atasca una conversión. Un documento que usa una fuente que no existe en ningún sitio de la máquina de conversión no puede repararse desde dentro del archivo, y el informe lo dirá. Instala en la máquina de conversión las fuentes que tus documentos usan de verdad; el recorrido sobre incrustar fuentes faltantes en un PDF existente cubre ese paso por separado
¿Cómo debe un trabajo por lotes manejar una conversión que falla?
Trata el valor de retorno como una puerta y el informe como la orden de trabajo. Archiva los archivos que devuelven 1, y encamina el resto a una persona con el informe adjunto. No guardes la salida de una conversión fallida en el archivador solo porque parezca más limpia que la entrada. Ese archivo lleva ahora una reclamación PDF/A que la comprobación se negó a confirmar, que es exactamente la situación que el paso de verificación existe para evitar
El informe de GetPDFAConversionReport es una línea por acción, cada una empezando con un verbo: removed:, regenerated:, repaired:, embedded:, added:, stated:, más blocked: si algo detuvo la ejecución, y una línea final result:. La comprobación detrás del veredicto se para en el primer fallo, así que cuando un archivo falla, la conversión corre la comprobación otra vez entera y el informe muestra todos los problemas de una vez. Encaminar por los prefijos lleva unas pocas líneas
uses
Classes, SysUtils, PDFlibrary, PDFlibEnterprise;
function ArchiveOne(const InName, ArchiveDir, ReviewDir: string): Boolean;
var
Lib: TPDFlibEnterprise;
Report: TStringList;
I: Integer;
Blocked: Boolean;
begin
Result := False;
Lib := TPDFlibEnterprise.Create;
Report := TStringList.Create;
try
if Lib.LoadFromFile(InName, '') <> 1 then
Exit;
Result := Lib.ConvertToPDFA(5) = 1; // 5 = PDF/A-3b, conserva los adjuntos
Report.Text := Lib.GetPDFAConversionReport;
if Result then
Lib.SaveToFile(IncludeTrailingPathDelimiter(ArchiveDir) + ExtractFileName(InName))
else
begin
Blocked := False;
for I := 0 to Report.Count - 1 do
if Pos('blocked:', Report[I]) = 1 then
Blocked := True;
// Deja el original intacto; solo el informe viaja con él
Report.SaveToFile(IncludeTrailingPathDelimiter(ReviewDir) +
ChangeFileExt(ExtractFileName(InName), '.pdfa-report.txt'));
if Blocked then
Writeln(InName, ': needs a password or a rebuilt source file');
end;
finally
Report.Free;
Lib.Free;
end;
end;
Una línea blocked: normalmente significa que el arreglo está fuera del PDF: una contraseña, un fichero de origen distinto, o una reejecución de lo que produjo el documento. Los problemas de validación llanos tras una conversión completa son más a menudo contenido que lleva significado y no puede borrarse sin más, y esos son los casos donde una persona tiene que decidir
Leer la marca que un archivo ya porta
Antes de convertir, comprueba lo que el documento ya declara sobre sí mismo. Los archivos que llegan de escáneres, suites ofimáticas y otras herramientas PDF a menudo llevan ya una marca PDF/A, y reconvertir un archivo PDF/A-3 válido a la parte 2 le costaría sus adjuntos para nada
La trampa es que XMP permite escribir la misma propiedad de dos maneras. Algunos productores escriben la parte como elemento hijo, <pdfaid:part>2</pdfaid:part>; otros la escriben como atributo en rdf:Description, pdfaid:part="2". Ambas son XMP corriente y válido. Un detector que busca solo una forma decide que los archivos de la otra familia de productores no llevan marca, y un comprobador construido así acaba juzgando un archivo PDF/A-2 válido contra la parte 1 y reportándolo como sin marca y de versión de PDF demasiado alta, exactamente lo contrario de la verdad. El validador de PDF Library for Delphi lee las dos formas. Si husmeas la marca tú mismo, haz lo mismo:
function DeclaredPDFAPart(Lib: TPDFlib): string;
const
ElemTag = '<pdfaid:part>';
AttrDQ = 'pdfaid:part="';
AttrSQ = 'pdfaid:part=''';
var
Packet: string;
P: Integer;
begin
Result := '';
Packet := Lib.GetDocumentMetadata;
P := Pos(ElemTag, Packet); // forma elemento
if P > 0 then
Exit(Copy(Packet, P + Length(ElemTag), 1));
P := Pos(AttrDQ, Packet); // forma atributo, comillas dobles
if P = 0 then
P := Pos(AttrSQ, Packet); // forma atributo, comillas simples
if P > 0 then
Result := Copy(Packet, P + Length(AttrDQ), 1);
end;
Un resultado vacío significa que no hay marca en ninguna de las dos formas. Un dígito te dice qué parte pretendía el productor, y ese número es el que comparar con tu objetivo antes de decidir si convertir en absoluto
Saneamiento de scripts antes de que el archivo entre en el archivador
La conversión archivable y el saneamiento de seguridad suelen correr juntos, porque el contenido que una política de seguridad quiere fuera se solapa mucho con lo que PDF/A prohíbe. SanitizeDocument toma una máscara de bits de flags SANITIZE_* y devuelve cuántas eliminaciones aplicó. ConvertToPDFA en persona la llama con SANITIZE_JAVASCRIPT or SANITIZE_OPEN_ACTION como su primer paso
Un detalle importa a los validadores. Los scripts de nivel documento viven en un árbol de nombres /JavaScript bajo el diccionario /Names del catálogo, y cuando se elimina el último script el árbol ahora vacío se elimina con él. Un árbol vacío todavía anuncia que el documento tiene scripts, y algunos comprobadores reaccionan a la clave en sí sin contar lo que hay debajo
Sea cual sea la herramienta que saneó, verifica el archivo guardado con una carga fresca en lugar de fiarte del estado en memoria. Reabrirlo es barato:
var
Lib, Check: TPDFlib;
begin
Lib := TPDFlib.Create;
Check := TPDFlib.Create;
try
Lib.LoadFromFile('upload.pdf', '');
Lib.SanitizeDocument(SANITIZE_JAVASCRIPT or SANITIZE_OPEN_ACTION);
Lib.SaveToFile('upload-clean.pdf');
Check.LoadFromFile('upload-clean.pdf', '');
if Check.GlobalJavaScriptCount > 0 then
raise Exception.Create('Document-level scripts survived the save');
finally
Check.Free;
Lib.Free;
end;
end;
Para los pasos vecinos en un pipeline de archivado, mira las guías de preflight PDF/A y PDF/UA, redacción real y eliminación de contenido y esquemas de extensión XMP de PDF/A-3 para Factur-X, que retoma el lado de los metadatos cuando un documento archivado también lleva datos estructurados de factura
Toda la conversión, reparación de metadatos y validación mostradas aquí corren dentro de tu propio proceso Delphi, C++Builder o Lazarus, sin ningún conversor externo ni validador de línea de comandos en la cadena. La página de producto de PDF Library for Delphi lista las partes PDF/A, las ediciones y las plataformas objetivo