Artículo técnico

Convertir un PDF a PDF/A y reparar sus metadatos

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

Diagrama del pipeline de ConvertToPDFA en PDF Library for Delphi que elimina contenido prohibido, repara metadatos, declara la parte PDF/A y verifica antes de archivar con veredicto favorable
ConvertToPDFA elimina, completa, estampa y verifica en una sola llamada, y solo una comprobación superada convierte la declaración de archivado en algo seguro de archivar

¿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

Diagrama que muestra RepairDocumentMetadata poniendo de acuerdo el diccionario Info y el paquete XMP y restaurando la identificación de estándar PDF/A perdida en Delphi
RepairDocumentMetadata rellena cada mitad de metadatos en blanco a partir de la otra y restablece una identificación de estándar perdida para que los validadores vean la parte PDF/A prevista

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

ModoParte y nivelNotas
1PDF/A-1aPDF 1.4, etiquetado
2PDF/A-1bPDF 1.4
3PDF/A-2bPDF 1.7
4PDF/A-2aPDF 1.7, etiquetado
5, 6, 7PDF/A-3b, 3a, 3uFicheros incrustados de cualquier tipo permitidos
8PDF/A-2uPDF 1.7, texto Unicode requerido
9, 10, 11PDF/A-4, 4e, 4fPDF 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