Artículo técnico

Construya una mesa de trabajo de revisión de recepción (intake) de PDF en Delphi con PDFium Component

Una mesa de trabajo (workbench) de revisión de recepción de PDF es un pequeño programa con un trabajo: mirar cada archivo antes de que se le permita tocarlo a cualquier cosa que esté aguas abajo (downstream). Para hacer ese trabajo, tiene que reunir un puñado de capacidades en una sola pasada. Abre el archivo (sin confiar en él), lee lo que el archivo afirma sobre sí mismo, busca contenido que engañaría a un extractor ingenuo o conllevaría un ataque, decide si hay algún texto extraíble en absoluto, y luego enruta el documento a una cola según lo que encontró. Sáltese la inspección y las fallas son silenciosas: un PDF cifrado con contraseña de propietario que envuelve un formulario XFA navega a través de un extractor de texto como cadenas vacías, se indexa como un documento en blanco, y nadie se da cuenta hasta que alguien más adelante busca un contenido que nunca fue leído. PDFium Component es un visor VCL/LCL en código fuente y una biblioteca de inspección para Delphi, C++Builder y Lazarus, y expone las llamadas de introspección que esta mesa de trabajo necesita. Las secciones siguientes recorren qué llamada responde qué pregunta, y los dos lugares donde la llamada obvia le da una respuesta confiadamente equivocada

Cinco preguntas para responder antes de que se enrute un archivo

Quite la cuadrícula y la tira de miniaturas, y la clasificación (triage) de recepción se reduce a cinco preguntas:

  • ¿Se puede abrir el archivo siquiera, y bajo qué contraseña?
  • ¿Qué afirma ser: título, autor, fecha de creación?
  • ¿Lleva contenido activo o riesgoso como JavaScript, un formulario XFA o archivos incrustados?
  • ¿Hay texto extraíble, o es un escaneo destinado al OCR?
  • Dado todo eso, ¿a qué cola va: procesamiento directo, revisión manual o cuarentena?

Cada pregunta se mapea sobre una o dos llamadas a PDFium Component. Dos de esos mapeos tienen bordes afilados que explican la mayoría de los archivos mal enrutados que he tenido que depurar en producción. Los metadatos de documentos viven en dos lugares diferentes que pueden no concordar, y el cifrado no detiene necesariamente la apertura de un documento

Abrir de forma económica: llenado de formularios desactivado, cero páginas renderizadas

La clasificación debería ser la apertura más económica posible. Configurar FormFill := False antes de Active := True le dice al componente que se salte por completo el entorno de llenado de formularios. Eso acorta el tiempo de carga, y (igual de importante para archivos de origen desconocido) evita que se inicialice cualquier JavaScript a nivel de documento. Ninguna de las propiedades de inspección usadas abajo requieren renderizar una página, por lo que una pasada de clasificación nunca tiene que producir un solo mapa de bits

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // sin entorno de formulario, sin inicio de JavaScript
    Pdf.Active := True;        // la falla es silenciosa: Active simplemente se queda en False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // archivo dañado o bloqueo por contraseña de usuario
      Exit;                    // el bloque finally todavía se ejecuta
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // nunca filtre la instancia en un archivo mal formado
  end;
end;

La comprobación después de la asignación no es opcional, y es una comprobación en lugar de un manejador de excepciones por una razón. Cuando el motor no puede cargar el archivo, el componente traga la excepción interna EPdfError y deja Active en False en lugar de propagarla. El código que espera una excepción felizmente leerá PageCount de un documento que nunca se abrió. Si el flujo de trabajo de rechazo necesita el texto de error real del motor, lea el archivo en una matriz de bytes y llame a la sobrecarga de LoadDocument que toma TBytes; esa ruta sí lanza EPdfError con el mensaje, incluido el caso de la contraseña. El try..finally todavía se gana su lugar. Los servicios de recepción se ejecutan desatendidos durante semanas, y ninguna excepción posterior puede filtrar la instancia TPdf o mantener un bloqueo con el que el paso de reintento tropezaría

El rendimiento rara vez se convierte en el cuello de botella. Con el llenado de formularios desactivado y sin renderizado, una apertura de clasificación está dominada por E/S, y un solo trabajador inspecciona cómodamente varios archivos por segundo desde un disco local. Si el volumen de recepción en algún momento supera a un trabajador, particione el trabajo por archivo en lugar de por comprobación. Las cinco preguntas comparten una apertura, y dividirlas en múltiples procesos multiplicaría el paso más costoso en lugar de amortizarlo

Los metadatos viven en dos lugares, y no concuerdan

ISO 32000-1 define dos hogares para los metadatos de documentos: el diccionario de información del documento (cláusula 14.3.3) y un paquete XMP adjunto al catálogo (cláusula 14.3.2). Las propiedades Title, Author, Subject y CreationDate leen el diccionario de información, con MetaText[] para cualquier otra clave y DecodeDate para analizar sintácticamente la cadena de fecha D:YYYYMMDD.... La trampa es que los productores modernos escriben cada vez más solo XMP, una dirección que ISO 32000-2 hace oficial al desaprobar la mayoría de las claves del diccionario de información en PDF 2.0. El síntoma en una herramienta de recepción es concreto. Su mesa de trabajo muestra un título vacío mientras que Adobe Acrobat muestra uno, porque Acrobat recurrió a dc:title dentro del paquete XMP, que las propiedades del diccionario de información nunca tocan

procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // valor del diccionario de Info
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // cadena de fecha original en PDF ("D:2026...")

  // Un título de Info vacío no significa que el documento no tenga título. El
  // componente no expone el paquete XMP, así que explore los bytes
  // originales del archivo buscando el elemento dc:title antes de confiar en el vacío.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Incluso la cruda exploración de subcadenas anterior vale la pena: "metadatos presentes, pero no donde las herramientas heredadas miran" es un dato relevante para el enrutamiento de cualquier tubería de archivo que indexe por título o autor. Si su índice descendente solo lee el diccionario de información, los archivos marcados de esta manera se volverán silenciosamente no buscables

Archivos cifrados que se abren de todos modos

Un documento cifrado no necesariamente falla al abrirse. El manejador de seguridad estándar (ISO 32000-1 cláusula 7.6.3) distingue una contraseña de usuario, requerida para abrir el documento, de una contraseña de propietario que meramente controla permisos como la impresión y la copia. Una gran parte de los documentos comerciales "protegidos" están cifrados con una contraseña de propietario y una contraseña de usuario vacía. Se abren sin preguntar, se descifran por completo y dependen de que los visores ofrezcan voluntariamente respetar las banderas de permisos. Eso es política, no protección, y sus estados de recepción deberían reflejar la diferencia

Detectar el cifrado después de una apertura exitosa requiere una llamada al motor más un plan de respaldo. FPDF_GetSecurityHandlerRevision(Pdf.Document) devuelve -1 para archivos desprotegidos y la revisión del manejador de lo contrario, y Pdf.Permissions devolviendo algo que no sea la máscara $FFFFFFFF (todos los bits activados) es la señal corroborante. Para archivos genuinamente bloqueados con contraseña de usuario, asigne Password antes de configurar Active := True; si la apertura sigue fallando, enrute el archivo a un estado bloqueado que solicite credenciales del remitente a través de un canal seguro en lugar de reintentar a ciegas. Y resista la tentación de tratar "cifrado" como una cuarentena automática. En la mayoría de las industrias con gran volumen de documentos, los archivos cifrados pero que se pueden abrir son el caso normal, no el sospechoso

Contenido activo: JavaScript, XFA y archivos incrustados

Tres hallazgos siempre deberían llegar a la decisión de enrutamiento. Primero, JavaScript: el evento OnUnsupportedFeature informa de características estructurales como XFA o contenido 3D a medida que el motor las encuentra, pero no detecta JavaScript. Verifique JavaScriptActionCount en su lugar y trate un resultado distinto de cero como contenido activo. Segundo, XFA: cuando FormType devuelve ftXfaFull, las páginas visibles son a menudo poco más que un renderizado de la plantilla XFA, y la extracción de texto convencional verá texto estándar en lugar de los valores completados. Tercero, archivos adjuntos: un PDF es un formato contenedor y AttachmentCount le dice si este lleva pasajeros

procedure CollectRiskSignals(Pdf: TPdf; var Rec: TIntakeRecord);
var
  i, PageNo: Integer;
  Ext: string;
begin
  Rec.IsEncrypted := Assigned(FPDF_GetSecurityHandlerRevision) and
    (FPDF_GetSecurityHandlerRevision(Pdf.Document) <> -1);
  Rec.HasForms := Pdf.FormType <> ftNone;
  Rec.IsXfa := Pdf.FormType = ftXfaFull;
  Rec.HasJavaScript := Pdf.JavaScriptActionCount > 0;

  // AnnotationCount es una propiedad por página; recorra las páginas para sumarla.
  // Cargar un objeto de página no renderiza nada, así que esto se mantiene barato.
  Rec.Annotations := 0;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    Inc(Rec.Annotations, Pdf.AnnotationCount);
  end;

  Rec.Attachments := Pdf.AttachmentCount;

  for i := 0 to Rec.Attachments - 1 do
  begin
    Ext := LowerCase(ExtractFileExt(string(Pdf.AttachmentName[i])));
    if (Ext = '.exe') or (Ext = '.js') or (Ext = '.vbs') or (Ext = '.dll') then
      Include(Rec.Flags, ifDangerousAttachment);
  end;
end;

Dos detalles en ese bucle merecen atención. El nombre del archivo adjunto viene de adentro del documento, así que nunca lo reutilice como una ruta de salida sin sanearlo primero; un nombre incrustado como ..\..\start.exe es un recorrido de ruta a la espera de una llamada de guardado descuidada. Y una lista de bloqueo de extensiones es un cable trampa, no una garantía. Su trabajo es forzar una decisión humana, no certificar el archivo como limpio

Convirtiendo señales en estados de enrutamiento

Un modelo de estado viable necesita menos estados de lo que la mayoría de los equipos espera: listo (sin bloqueadores, texto presente), revisar (la apertura tuvo éxito pero algo necesita ser visto, como un formulario XFA, JavaScript, una capa de texto vacía, o un título solo en XMP), bloqueado (se requiere contraseña de usuario) y dañado (la apertura falló). Registre la evidencia junto al estado. El hash del archivo, el número de páginas, las banderas exactas y el mensaje de error del motor para los archivos dañados importan, porque la persona que cuestione una decisión de enrutamiento lo hará semanas después, frente a un archivo que puede haber sido modificado o reemplazado desde entonces

Cuando un operador sí necesita mirar un archivo en cuarentena, no se lo entregue al visor predeterminado del sistema. Renderícelo dentro de un panel reforzado con la programación y el manejo de enlaces desactivados, el enfoque descrito en la construcción de una superficie segura de vista previa de PDF en Delphi. Y si su recepción alimenta un archivo con requerimientos de conformidad, la pasada de clasificación es el lugar natural para programar una revisión más profunda; la validación de comprobación previa por lotes frente a los perfiles PDF/A y PDF/UA retoma el trabajo exactamente donde se detiene esta inspección

La página de producto del componente cubre las licencias, la API completa de inspección y las demostraciones incluidas, incluido un inspector de documentos al estilo de recepción: PDFium Component