Un PDF que llega a una frontera de producción — una cola de impresión, un sistema de archivado, un portal de carga para clientes — debería auditarse antes de que cualquier cosa lo renderice. El archivo podría traer una acción Launch conectada para iniciar un programa externo, imágenes demasiado burdas para sobrevivir a la impresión, un diccionario de cifrado que prohíbe justo el trabajo de impresión para el que se envió, o una etiqueta PDF/A que no cumple. Inspeccionar un documento contra reglas como estas antes de que entre a un flujo de trabajo se llama preflight, y la API C de PDFium le da a Delphi todo lo necesario para implementar las comprobaciones directamente, sin renderizar una sola página
Este artículo construye las comprobaciones en sí: cuatro clases de auditoría, cada una una rutina pequeña que agrega hallazgos a una lista de resultados compartida. Elementos interactivos, métricas de recursos, estado de seguridad y marcadores de estándares reciben código funcional, incluida la aritmética. Si lo que necesitas es la maquinaria alrededor de las comprobaciones — bucles por lotes sobre carpetas, archivos de reporte JSON y HTML, aislamiento por archivo — el PDFium Component incluye un motor de preflight listo para usar, y el artículo sobre la CLI de preflight por lotes cubre esa fontanería. Los dos comparten deliberadamente un mismo vocabulario de códigos de salida, así que un auditor escrito aquí encaja directamente debajo de ese controlador por lotes
El registro de hallazgos y el contrato de códigos de salida
Cada comprobación escribe en un único tipo de registro plano, porque la alternativa, que cada comprobación imprima su propia prosa, no puede contarse, filtrarse ni someterse a umbrales después. Cuatro campos bastan
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // clave estable para máquinas, p. ej. 'ACT-LAUNCH'
Page: Integer; // base 1; 0 significa nivel de documento
Message: string; // para humanos; puede reformularse entre versiones
end;
TFindings = TList<TPreflightFinding>;
procedure Add(Findings: TFindings; Severity: TFindingSeverity;
const Code: string; Page: Integer; const Msg: string);
var
F: TPreflightFinding;
begin
F.Severity := Severity;
F.Code := Code;
F.Page := Page;
F.Message := Msg;
Findings.Add(F);
end;
Las herramientas posteriores se basan en Code, nunca en el texto de Message, que puede cambiar libremente. El código de salida del proceso sigue el mismo contrato de tres valores que el artículo por lotes: 0 significa que el archivo no produjo hallazgos, 1 significa que existen hallazgos y 2 significa que la auditoría en sí no pudo ejecutarse porque el archivo no se pudo analizar o exige una contraseña. Mantener el código 2 separado importa. Una carpeta de escaneos corruptos es un escáner roto río arriba, no un colapso repentino de cumplimiento, y mezclar los dos manda a alguien a perseguir el problema equivocado
Elementos interactivos: scripts, destinos de Launch, enlaces externos
PDFium clasifica cada acción que encuentra por un tipo entero, y vale la pena fijar con precisión las constantes de fpdf_doc.h, porque valores mal copiados dejan a un escáner ciego en silencio. La enumeración real es PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 y PDFACTION_EMBEDDEDGOTO = 5. Observa lo que falta: no hay un miembro JavaScript. Los scripts a nivel de documento no son acciones de enlace y nunca aparecen a través de FPDFAction_GetType; se enumeran con una familia de llamadas aparte. Un auditor que compara tipos de acción contra una constante JavaScript imaginaria compila, se ejecuta y no encuentra nada, para siempre
const
PDFACTION_GOTO = 1; // salto dentro del documento: inofensivo
PDFACTION_REMOTEGOTO = 2; // salto a otro archivo local
PDFACTION_URI = 3; // abre una URL externa
PDFACTION_LAUNCH = 4; // inicia un programa externo
PDFACTION_EMBEDDEDGOTO = 5; // salto a un archivo incrustado
function ActionTarget(Doc: FPDF_DOCUMENT; Action: FPDF_ACTION;
AType: ULONG): string;
var
Buf: array[0..2047] of AnsiChar;
begin
FillChar(Buf, SizeOf(Buf), 0);
if AType = PDFACTION_URI then
FPDFAction_GetURIPath(Doc, Action, @Buf, SizeOf(Buf))
else
FPDFAction_GetFilePath(Action, @Buf, SizeOf(Buf));
Result := string(UTF8String(PAnsiChar(@Buf)));
end;
procedure AuditPageActions(Doc: FPDF_DOCUMENT; Page: FPDF_PAGE;
PageNo: Integer; Findings: TFindings);
var
StartPos: Integer;
Link: FPDF_LINK;
Action: FPDF_ACTION;
AType: ULONG;
begin
StartPos := 0;
while FPDFLink_Enumerate(Page, @StartPos, @Link) <> 0 do
begin
Action := FPDFLink_GetAction(Link);
if Action = nil then
Continue; // enlace solo con destino, nada que marcar
AType := FPDFAction_GetType(Action);
case AType of
PDFACTION_LAUNCH:
Add(Findings, fsError, 'ACT-LAUNCH', PageNo,
'Launch action targets "' + ActionTarget(Doc, Action, AType) + '"');
PDFACTION_URI:
Add(Findings, fsWarning, 'ACT-URI', PageNo,
'link opens ' + ActionTarget(Doc, Action, AType));
PDFACTION_REMOTEGOTO, PDFACTION_EMBEDDEDGOTO:
Add(Findings, fsWarning, 'ACT-XFILE', PageNo,
'cross-file destination "' + ActionTarget(Doc, Action, AType) + '"');
end; // PDFACTION_GOTO guarda silencio a propósito
end;
end;
procedure AuditDocumentBehaviors(Doc: FPDF_DOCUMENT; Findings: TFindings);
var
N: Integer;
begin
N := FPDFDoc_GetJavaScriptActionCount(Doc);
if N > 0 then
Add(Findings, fsError, 'JS-DOC', 0,
Format('%d document-level JavaScript action(s) run on open', [N]));
N := FPDFDoc_GetAttachmentCount(Doc);
if N > 0 then
Add(Findings, fsWarning, 'ATT-EMB', 0,
Format('%d embedded file attachment(s)', [N]));
end;
La división de severidad codifica la política. Una acción Launch es un error porque iniciar un programa arbitrario es lo más peligroso que puede hacer un clic en un PDF, y ninguna factura lo necesita. Las URI externas son advertencias: comunes en documentos legítimos, pero un revisor debería ver el destino sin hacer clic, ya que el texto visible del enlace y el destino real no tienen por qué coincidir. Los saltos GoTo dentro del documento son estructura, no comportamiento, y quedan fuera del reporte por completo — un preflight que grita lobo en cada entrada del índice entrena a la gente a ignorarlo. Para leer los cuerpos de los scripts detrás del conteo de JavaScript, y para los niveles MDP de firmas y la detección de XFA, el artículo sobre auditoría de riesgos de seguridad recorre la misma superficie a través del envoltorio de objetos del componente
Métricas de recursos: DPI efectivo de las imágenes
Una imagen dentro de un PDF no tiene DPI propio. Tiene píxeles, y la página coloca esos píxeles en un rectángulo medido en puntos, donde 72 puntos hacen una pulgada. La resolución solo existe como la razón entre ambos, y por eso la misma foto de 600 por 400 se ve nítida como miniatura y borrosa como imagen principal a página completa. La auditoría necesita, por tanto, ambos números para cada imagen: las dimensiones en píxeles de origen desde los metadatos de la imagen, y el rectángulo colocado desde los límites del objeto
procedure AuditPageImages(Page: FPDF_PAGE; PageNo: Integer;
Findings: TFindings);
var
I, ObjCount: Integer;
Obj: FPDF_PAGEOBJECT;
Meta: FPDF_IMAGEOBJ_METADATA;
L, B, R, T: Single;
WidthPt, HeightPt, DpiX, DpiY, EffDpi: Double;
begin
ObjCount := FPDFPage_CountObjects(Page);
for I := 0 to ObjCount - 1 do
begin
Obj := FPDFPage_GetObject(Page, I);
if FPDFPageObj_GetType(Obj) <> FPDF_PAGEOBJ_IMAGE then
Continue;
if FPDFImageObj_GetImageMetadata(Obj, Page, @Meta) = 0 then
Continue;
if FPDFPageObj_GetBounds(Obj, @L, @B, @R, @T) = 0 then
Continue;
WidthPt := R - L; // tamaño colocado en la página, en puntos
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 puntos = 1 pulgada, así que pulgadas colocadas = puntos / 72, y
// DPI efectivo = píxeles de origen / pulgadas colocadas.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // el peor eje decide la calidad de impresión
if EffDpi < 150.0 then
Add(Findings, fsWarning, 'IMG-LOWRES', PageNo,
Format('image %dx%d px placed at %.1fx%.1f pt = %.0f DPI effective',
[Meta.Width, Meta.Height, WidthPt, HeightPt, EffDpi]))
else if EffDpi > 600.0 then
Add(Findings, fsInfo, 'IMG-BLOAT', PageNo,
Format('image is %.0f DPI at placed size; resampling would ' +
'shrink the file with no visible loss', [EffDpi]));
end;
end;
Los umbrales son política, no física: 150 DPI es un piso por debajo del cual la impresión de oficina se pixela visiblemente, 300 es el objetivo comercial habitual, y todo lo que supere 600 no compra calidad visible pero infla el tamaño del archivo, y por eso se reporta como hinchazón informativa en lugar de como defecto. Una advertencia honesta: FPDFPageObj_GetBounds devuelve la caja alineada a los ejes, así que para una imagen colocada con rotación la cifra calculada subestima la densidad real. La estructura FPDF_IMAGEOBJ_METADATA también trae los campos horizontal_dpi y vertical_dpi, que PDFium deriva de la matriz de transformación completa, y comparar ambos resultados es una forma barata de detectar colocaciones rotadas. La misma aritmética de puntos a píxeles impulsa el renderizado en la dirección opuesta, cubierta en el artículo sobre exportación a JPEG
Estado de seguridad: cifrado y bits de permisos
El cifrado de PDF define dos contraseñas con trabajos distintos. La contraseña de usuario controla el descifrado: sin ella el archivo no se abre en absoluto, y FPDF_LoadDocument devuelve nil mientras FPDF_GetLastError reporta FPDF_ERR_PASSWORD. La contraseña de propietario controla los permisos: un archivo protegido solo por contraseña de propietario se abre sin credenciales pero lleva bits de restricción que un lector conforme debe respetar. El intento de carga es, por tanto, la primera sonda de seguridad, y la distinción decide el código de salida — un archivo con contraseña de usuario no es auditable (código 2), mientras que un archivo con contraseña de propietario se audita con normalidad y simplemente acumula hallazgos
const
FPDF_ERR_PASSWORD = 4;
function AuditSecurity(const FileName: string;
Findings: TFindings): FPDF_DOCUMENT;
var
Perms: ULONG;
Revision: Integer;
begin
Result := FPDF_LoadDocument(PAnsiChar(AnsiString(FileName)), nil);
if Result = nil then
begin
if FPDF_GetLastError() = FPDF_ERR_PASSWORD then
Add(Findings, fsError, 'SEC-USERPW', 0,
'user (open) password required; audit cannot proceed')
else
Add(Findings, fsError, 'DOC-BROKEN', 0, 'file failed to parse');
Exit;
end;
Revision := FPDF_GetSecurityHandlerRevision(Result);
if Revision >= 0 then // -1 significa que el archivo no está cifrado
begin
// Abierto con contraseña vacía pero cifrado: solo contraseña de propietario.
// Cualquiera puede leerlo, pero los bits de permisos restringen lo que
// un lector conforme le permite hacer. Los archivos sin cifrar reportan
// todos los bits activos, y por eso la puerta de la revisión va primero.
Perms := FPDF_GetDocPermissions(Result);
Add(Findings, fsInfo, 'SEC-ENC', 0,
Format('encrypted, security handler revision %d', [Revision]));
if (Perms and 4) = 0 then // bit 3: imprimir
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'printing is not permitted');
if (Perms and 16) = 0 then // bit 5: copiar / extraer contenido
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'content extraction is not permitted');
if (Perms and 2048) = 0 then // bit 12: impresión en alta resolución
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'only low-resolution printing is permitted');
end;
end;
Las máscaras salen de la Tabla 22 de ISO 32000-1, que numera los bits desde 1: el bit 3 del valor /P es la máscara 4, el bit 5 es 16, el bit 12 es 2048. Si un hallazgo dado importa es una decisión de enrutamiento. Una imprenta debería rechazar un archivo SEC-NOPRINT en la recepción, donde quien lo envía recibe un mensaje claro, y no en el RIP tres horas antes de una fecha límite. Un sistema de archivado debería tratar el propio SEC-ENC como bloqueante, ya que el cifrado y la preservación a largo plazo no se mezclan — un punto que la comprobación de estándares está a punto de formalizar
Marcadores de estándares: leer una declaración PDF/A
Un archivo declara su conformidad PDF/A en su paquete de metadatos XMP, mediante la propiedad pdfaid:part (1 a 4) y pdfaid:conformance (la letra del nivel, como b para fidelidad visual o a para etiquetado estructural completo). La API C de PDFium no ofrece un accesor XMP; FPDF_GetMetaText lee solo el diccionario Info, que no es donde vive la identificación. La salida de emergencia es una regla del propio estándar: ISO 19005 exige que el flujo de metadatos XMP se almacene sin comprimir, precisamente para que las herramientas puedan encontrarlo sin un analizador PDF completo. Un escaneo de bytes en crudo es, por tanto, un detector de declaraciones legítimo — y un archivo cuya declaración se esconde dentro de un flujo comprimido ya violó el estándar que declara
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // vacío = no hay declaración PDF/A
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // esquema de identificación XMP
if P = 0 then
Exit;
// Maneja tanto <pdfaid:part>2</pdfaid:part> como pdfaid:part="2":
// toma el primer dígito después del nombre de la propiedad.
Limit := Min(P + 32, Length(S));
Inc(P, Length('pdfaid:part'));
while (P <= Limit) and not (S[P] in ['1'..'4']) do
Inc(P);
if P <= Limit then
Result := 'PDF/A-' + Char(S[P]);
end;
El hallazgo que esto produce es deliberadamente informativo, porque una declaración es una afirmación, no una propiedad del archivo. La entrada XMP es una línea de XML que cualquier productor puede escribir, incluido uno roto; la conformidad es que el archivo satisfaga de verdad cientos de reglas sobre fuentes incrustadas, color independiente del dispositivo y características prohibidas. Detectar la declaración te dice qué archivos enrutar a una validación real, y nada más. El motor de preflight integrado del componente realiza esa validación en los perfiles PDF/A, PDF/UA y PDF/X, y el artículo sobre la CLI por lotes muestra cómo conectarlo a una canalización con reportes que un auditor pueda abrir después
Una ejecución contra un archivo problemático
El controlador encadena las comprobaciones: primero la seguridad, porque decide si la auditoría se ejecuta siquiera, luego los comportamientos a nivel de documento y la declaración de estándares, y después un bucle por páginas para acciones e imágenes
function AuditFile(const FileName: string; Findings: TFindings): Integer;
var
Doc: FPDF_DOCUMENT;
Page: FPDF_PAGE;
I: Integer;
Claim: string;
begin
Doc := AuditSecurity(FileName, Findings);
if Doc = nil then
Exit(2); // fallo de auditoría, no un veredicto
try
AuditDocumentBehaviors(Doc, Findings);
Claim := PdfAClaim(FileName);
if Claim <> '' then
Add(Findings, fsInfo, 'STD-PDFA', 0,
Claim + ' conformance claimed (declaration only, not validated)');
for I := 0 to FPDF_GetPageCount(Doc) - 1 do
begin
Page := FPDF_LoadPage(Doc, I);
if Page = nil then
begin
Add(Findings, fsError, 'PAGE-BROKEN', I + 1, 'page failed to parse');
Continue;
end;
try
AuditPageActions(Doc, Page, I + 1, Findings);
AuditPageImages(Page, I + 1, Findings);
finally
FPDF_ClosePage(Page);
end;
end;
finally
FPDF_CloseDocument(Doc);
end;
if Findings.Count > 0 then
Result := 1
else
Result := 0;
end;
Contra un folleto que volvió de una agencia externa, la salida se ve así
> preflight_audit brochure_final.pdf
brochure_final.pdf: 5 finding(s)
[ERROR] ACT-LAUNCH page 3 Launch action targets "..\tools\setup.exe"
[ERROR] JS-DOC doc 2 document-level JavaScript action(s) run on open
[WARNING] IMG-LOWRES page 7 image 412x287 px placed at 396.0x275.8 pt = 75 DPI effective
[WARNING] SEC-NOPRINT doc printing is not permitted
[INFO] STD-PDFA doc PDF/A-2 conformance claimed (declaration only, not validated)
exit code 1
Cada línea es accionable por sí sola, pero la combinación es el veredicto real. Este archivo declara PDF/A-2 mientras carga un diccionario de cifrado y JavaScript activo, y PDF/A prohíbe ambos de plano — así que la declaración es demostrablemente falsa antes de que corra cualquier validador profundo. Ese es el tipo de contradicción que una lista plana de hallazgos saca a la luz y que un aprobado/reprobado booleano esconde
Lo que esta auditoría no puede decirte
La honestidad sobre el alcance es lo que mantiene la confianza en una herramienta de preflight. Todo lo anterior lee lo que el archivo declara sobre sí mismo: PDFium analiza la estructura, y esta auditoría la inventaría. No realiza validación PDF/A — sin comprobaciones de cobertura de glifos contra fuentes incrustadas, sin análisis de espacios de color contra intenciones de salida, ninguna de las reglas a nivel de cláusula que separan una declaración de la conformidad; para eso necesitas un validador dedicado como el motor de preflight del componente o veraPDF. Los bits de permisos son declaraciones que los lectores conformes respetan, no muros criptográficos, así que SEC-NOPRINT describe intención y no aplicación. El escaneo de acciones cubre anotaciones de enlace y scripts a nivel de documento; los scripts enterrados en diccionarios de eventos de campos de formulario necesitan además las API de formularios. Y una comprobación de firmas, si extiendes la auditoría con una, reporta intención declarada, no criptografía verificada — la validación de la cadena de certificados es un trabajo aparte. Una auditoría de preflight es la entrevista de ingreso, no el juicio: su trabajo es hacer que la decisión de enrutamiento sea informada, rápida y repetible
Nota: las API de objetos de documento, página, anotación e imagen usadas a lo largo de esta auditoría, junto con un envoltorio Delphi de alto nivel y un motor de preflight completo de validación de estándares, se incluyen con el PDFium Component