En un pipeline de reclamaciones de seguros en el que trabajé, un solo archivo entrante hizo perder medio día. El "contrato firmado" que subió un corredor era un PDF cifrado con contraseña de propietario que envolvía un formulario XFA: el extractor de texto posterior devolvió cadenas vacías, el indexador archivó la reclamación como documento en blanco y nadie lo notó hasta que llamó el asegurado. La falla no estaba en el extractor. El problema fue que ningún código había mirado realmente el archivo antes de enrutarlo. Todo equipo que acepta PDFs del mundo exterior termina construyendo lo mismo: una mesa de ingesta que inspecciona cada documento y decide a dónde se le permite ir. PDFium Component, una biblioteca de visor e inspección de documentos VCL/LCL con código fuente para Delphi, C++Builder y Lazarus, ofrece las llamadas de introspección para construir esa mesa; el resto de este artículo trata de qué llamadas responden qué preguntas y dónde pueden engañarlos.
Cinco preguntas antes de enrutar un archivo
Si quitan la grilla y la tira de miniaturas, la clasificación de ingesta se reduce a cinco preguntas:
- ¿Se puede abrir el archivo, y con qué contraseña?
- ¿Qué dice ser: título, autor, fecha de creación?
- ¿Trae contenido activo o riesgoso: JavaScript, un formulario XFA, archivos incrustados?
- ¿Hay texto extraíble, o es un escaneo que va rumbo a OCR?
- Con todo eso, ¿qué cola lo recibe: procesamiento directo, revisión manual o cuarentena?
Cada pregunta se mapea a una o dos llamadas de PDFium Component. Dos de esos mapeos tienen bordes filosos que explican la mayoría de los archivos mal enrutados que he depurado en producción: metadatos del documento que viven en dos lugares distintos, y cifrado que no impide que el documento se abra.
Abrir barato: form fill apagado, cero páginas renderizadas
La clasificación debe ser la apertura más barata posible. Definir FormFill := False antes de Active := True le dice al componente que omita por completo el entorno de form-fill, lo que acorta el tiempo de carga y, algo igual de importante con archivos de origen desconocido, evita que se inicialice cualquier JavaScript a nivel de documento. Ninguna de las propiedades de inspección usadas abajo exige renderizar una página, así que una pasada de triage no tiene que producir ni un solo bitmap.
procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := IncomingPath;
Pdf.FormFill := False; // no form environment, no JavaScript init
Pdf.Active := True; // failure is silent: Active simply stays False
if not Pdf.Active then
begin
Rec.OpenFailed := True; // damaged file or user-password lock
Exit; // the finally block still runs
end;
Rec.PageCount := Pdf.PageCount;
CollectIdentity(Pdf, IncomingPath, Rec);
CollectRiskSignals(Pdf, Rec);
finally
Pdf.Active := False;
Pdf.Free; // never leak the instance on a malformed file
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 absorbe el EPdfError interno y deja Active en False en vez de propagarlo. El código que espera una excepción leerá tranquilamente PageCount de un documento que nunca se abrió. Si el flujo de rechazo necesita el texto real de error del motor, lean el archivo en un arreglo de bytes y llamen a la sobrecarga de LoadDocument que recibe TBytes; esa ruta sí lanza EPdfError con el mensaje, incluido el caso de contraseña. El try..finally sigue ganándose su lugar: los servicios de intake corren sin atención durante semanas, y ninguna excepción posterior debe filtrar la instancia TPdf ni mantener un bloqueo que hará fallar el reintento.
El throughput rara vez se vuelve el cuello de botella. Con form fill desactivado y sin render, una apertura de triage está dominada por E/S, y un solo worker inspecciona cómodamente varios archivos por segundo desde disco local. Si el volumen de ingesta alguna vez supera a un worker, particionen el trabajo por archivo y no por comprobación: las cinco preguntas comparten una apertura, y dividirlas entre procesos multiplicaría el paso más caro en lugar de amortizarlo.
Los metadatos viven en dos lugares y no coinciden
ISO 32000-1 define dos hogares para los metadatos del documento: 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 Info, con MetaText[] para cualquier otra clave y DecodeDate para analizar 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 deprecar la mayoría de las claves del diccionario Info en PDF 2.0. El síntoma en una herramienta de ingesta es concreto: su mesa muestra un título vacío mientras Adobe Acrobat muestra uno, porque Acrobat recurrió a dc:title dentro del paquete XMP, que las propiedades del diccionario Info nunca tocan.
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
var Rec: TIntakeRecord);
begin
Rec.Title := Pdf.Title; // Info dictionary value
Rec.Author := Pdf.Author;
Rec.CreatedAt := Pdf.CreationDate; // raw PDF date string ("D:2026...")
// An empty Info title does not mean the document is untitled. The
// component does not expose the XMP packet, so probe the raw file
// bytes for the dc:title element before trusting the blank.
if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
Include(Rec.Flags, ifTitleInXmpOnly);
end;
Incluso la búsqueda cruda de subcadena anterior se gana su lugar: "hay metadatos, pero no donde miran las herramientas heredadas" es un dato relevante para el enrutamiento en cualquier pipeline de archivo que indexe por título o autor. Si el índice posterior lee solo el diccionario Info, los archivos marcados de esta forma se volverán silenciosamente imposibles de buscar.
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, necesaria para abrir el documento, de una contraseña de propietario que solo gobierna permisos como imprimir y copiar. Una gran parte de los documentos empresariales "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 se ofrezcan a respetar las banderas de permiso. Eso es política, no protección, y los estados de intake deben reflejar la diferencia.
Detectar cifrado después de una apertura exitosa requiere una llamada del motor más una señal de respaldo: FPDF_GetSecurityHandlerRevision(Pdf.Document) devuelve -1 para archivos sin protección y la revisión del manejador en los demás casos, y que Pdf.Permissions devuelva cualquier cosa distinta de la máscara de todos los bits en uno $FFFFFFFF es la señal corroborante. Para archivos realmente bloqueados con contraseña de usuario, asignen Password antes de establecer Active := True; si la apertura sigue fallando, enruten el archivo a un estado bloqueado que solicite credenciales al remitente por un canal seguro en lugar de reintentar a ciegas. Y resistan la tentación de tratar "cifrado" como cuarentena automática: en la mayoría de industrias intensivas en documentos, los archivos cifrados pero abribles son el caso normal, no el sospechoso.
Contenido activo: JavaScript, XFA y archivos incrustados
Tres hallazgos siempre deben llegar a la decisión de enrutamiento. Primero, JavaScript: el evento OnUnsupportedFeature reporta características estructurales como XFA o contenido 3D cuando el motor las encuentra, pero no detecta JavaScript; revisen JavaScriptActionCount en su lugar y traten un resultado distinto de cero como contenido activo. Segundo, XFA: cuando FormType devuelve ftXfaFull, las páginas visibles a menudo son poco más que un render de la plantilla XFA, y la extracción de texto convencional verá boilerplate en lugar de los valores llenados. Tercero, adjuntos: un PDF es un formato contenedor, y AttachmentCount les 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 is a per-page property; walk the pages to total
// it. Loading a page object renders nothing, so this stays cheap.
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 de ese bucle merecen atención. El nombre del adjunto viene desde dentro del documento, así que nunca lo reutilicen como ruta de salida sin sanearlo: un nombre incrustado como ..\..\start.exe es una traversal de ruta esperando una llamada de guardado descuidada. Y una lista de bloqueo por extensión es un disparador de revisión, no una garantía; su trabajo es forzar una decisión humana, no certificar que el archivo está limpio.
Convertir señales en estados de enrutamiento
Un modelo de estados funcional necesita menos estados de los que esperan la mayoría de equipos: ready, sin bloqueadores y con texto presente; review, la apertura funcionó pero algo requiere ojos, como formulario XFA, JavaScript, capa de texto vacía o título solo en XMP; blocked, se requiere contraseña de usuario; y damaged, la apertura falló. Registren la evidencia junto con el estado: hash del archivo, cantidad de páginas, banderas exactas, mensaje de error del motor para archivos dañados; la persona que cuestione una decisión de enrutamiento lo hará semanas después, contra un archivo que quizá ya fue reemplazado o modificado.
Cuando un operador sí necesita mirar un archivo en cuarentena, no se lo entreguen al visor predeterminado del shell. Renderícenlo dentro de un panel endurecido con scripting y manejo de enlaces desactivados, el enfoque descrito en construir una superficie de vista previa PDF segura en Delphi. Y si el intake alimenta un archivo con requisitos de conformidad, la pasada de triage es el lugar natural para programar una revisión más profunda; la validación preflight por lotes contra perfiles PDF/A y PDF/UA continúa exactamente donde esta inspección se detiene.
Preguntas frecuentes
¿Cómo verifico si un PDF está protegido por contraseña en Delphi?
Ábranlo con PDFium Component y consulten el manejador de seguridad: FPDF_GetSecurityHandlerRevision(Pdf.Document) devuelve -1 para archivos sin protección. Si Active queda en False sin contraseña, lo más probable es que el archivo use una contraseña de usuario: asignen Password e intenten de nuevo. Si se abre bien pero hay un manejador de seguridad presente, el archivo solo tiene protección con contraseña de propietario: es completamente legible, y las banderas de permiso en Permissions son consultivas.
¿Por qué la propiedad Title devuelve una cadena vacía cuando Acrobat muestra un título?
El título está almacenado solo en el paquete de metadatos XMP, no en el diccionario de información del documento que lee Title. El componente no expone el paquete XMP, así que busquen dc:title en los bytes crudos del archivo y marquen el archivo para pipelines que indexan con metadatos del diccionario Info.
¿PDFium Component puede detectar JavaScript dentro de un PDF?
Sí: revisen JavaScriptActionCount o enumeren las acciones a nivel de documento mediante JavaScriptActions. No dependan del evento OnUnsupportedFeature para esto; reporta características como XFA y 3D, pero no scripting.
La página del producto cubre licenciamiento, la API completa de inspección y los demos incluidos, entre ellos un inspector de documentos estilo intake: PDFium Component.