Un archivo PDF que llega a un límite de producción (una cola de impresión, un archivo, un portal de carga para clientes) debe auditarse antes de que se proceda a renderizarlo. Es posible que el archivo incluya una acción de inicio configurada para iniciar un programa externo, que contenga imágenes demasiado grandes para resistir la impresión, que posea un diccionario de encriptación que prohíba el trabajo de impresión en sí para el que se envió, o tal vez una etiqueta de PDF/A que no cumple con las condiciones. Inspeccionar un documento a través de reglas como las mencionadas antes de que pase a un flujo de trabajo se denomina revisión de preimpresión (preflighting), y la API de PDFium C le brinda al Delphi todo lo que necesita para implementar esas verificaciones de forma directa y sin tener que renderizar una sola página
Este artículo formula las propias verificaciones: cuatro clases de auditoría, cada una de ellas como una pequeña rutina que añade los hallazgos en una lista compartida de resultados. Tanto los elementos interactivos, como la métrica de recursos, el estado de seguridad y los marcadores de estándares obtienen código funcional de trabajo, incluyendo la aritmética. Si lo que precisa es la maquinaria que rodea a las verificaciones (bucles de carpetas en lotes, archivos de informe JSON y HTML, y asilamiento por archivo), el componente de PDFium se suministra con un motor de preimpresión listo para usarse, y el artículo sobre el CLI de preimpresión por lotes se encarga de abordar dicha estructura. Ambas secciones comparten intencionalmente un solo vocabulario de códigos de salida, de modo que el auditor que se escribe aquí encaje directamente bajo el controlador de dicho lote
El registro de los hallazgos y el contrato de los códigos de salida
Cada verificación se escribe dentro de un único tipo de registro plano; esto debido a que la otra alternativa, que es que cada verificación imprima su propia prosa, no puede ser contabilizada, ni filtrada, ni sometida a umbrales después. Basta y sobra con cuatro campos
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // stable machine key, e.g. 'ACT-LAUNCH'
Page: Integer; // 1-based; 0 means document level
Message: string; // for humans; free to reword between releases
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 de las fases posteriores toman a Code como clave, pero nunca al texto en Message, que se puede cambiar libremente. El código de salida del proceso se rige por el mismo contrato de tres valores mencionado en el artículo de procesamiento por lotes: 0 significa que el archivo no produjo ningún hallazgo, 1 se traduce en que sí hubo hallazgos y 2 significa que la auditoría en sí no logró ejecutarse porque el archivo falló en el proceso de análisis sintáctico o porque requiere una contraseña. Es muy importante que se mantenga el código 2 por separado. Una carpeta con escaneos dañados no representa otra cosa que un escáner averiado en una fase anterior, y no un problema súbito de colapso de cumplimiento, por lo que juntarlos o integrarlos lleva a la persona en cuestión a abalanzarse sobre el problema equivocado
Elementos interactivos: scripts, objetivos de inicio, enlaces externos
PDFium clasifica mediante un tipo de valor entero todas y cada una de las acciones que encuentra; vale la pena que se definan con precisión las constantes de fpdf_doc.h, porque copiar mal un valor provoca que el escáner se ciegue en silencio. La enumeración real es la siguiente: PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 y PDFACTION_EMBEDDEDGOTO = 5. Observe con atención lo que se encuentra ausente: no hay ningún miembro de JavaScript. Los scripts al nivel del documento no son acciones de enlace y por tanto no se exhiben a través de FPDFAction_GetType; por el contrario, los numera otra familia de llamadas distinta. Un auditor que pone a prueba a los tipos de acciones enfrentándolos a una constante ficticia de JavaScript en un plano imaginario termina con compilar, ejecutar y encontrar nada por toda la eternidad
const
PDFACTION_GOTO = 1; // in-document jump: harmless
PDFACTION_REMOTEGOTO = 2; // jump into another local file
PDFACTION_URI = 3; // opens an external URL
PDFACTION_LAUNCH = 4; // starts an external program
PDFACTION_EMBEDDEDGOTO = 5; // jump into an embedded file
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; // destination-only link, nothing to flag
AType := FPDFAction_GetType(Action);
case AType of
PDFACTION_LAUNCH:
Add(Findings, fsError, 'ACT-LAUNCH', PageNo,
'La acción de inicio apunta a "' + ActionTarget(Doc, Action, AType) + '"');
PDFACTION_URI:
Add(Findings, fsWarning, 'ACT-URI', PageNo,
'el enlace abre a ' + ActionTarget(Doc, Action, AType));
PDFACTION_REMOTEGOTO, PDFACTION_EMBEDDEDGOTO:
Add(Findings, fsWarning, 'ACT-XFILE', PageNo,
'destino entre archivos "' + ActionTarget(Doc, Action, AType) + '"');
end; // PDFACTION_GOTO stays silent by design
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 acciones de JavaScript a nivel de documento se ejecutan al abrirse', [N]));
N := FPDFDoc_GetAttachmentCount(Doc);
if N > 0 then
Add(Findings, fsWarning, 'ATT-EMB', 0,
Format('%d archivos adjuntos integrados', [N]));
end;
La división que marca la severidad u hostilidad del problema tiene como origen las políticas de codificación en particular. La variable de acción de tipo Launch se interpreta como un error en un alto nivel en razón de que dar por comenzado el uso de cualquier programa arbitrario es la acción con el nivel de peligro más elevado de todas que la pulsación de un botón u enlace de un simple PDF pueda hacer, en sintonía con que ninguna factura jamás precisa de ese accionar. En adición, las variables relativas al parámetro URI externo sirven el modo de advertencias: su aparición es moneda corriente de los casos para documentos con el aval de lo legítimo, pero de todos modos se da que lo ideal se reduce a que cada uno de los revisores debería echar un ojo del destino pormenorizado en lugar de recurrir al clic, de la mano de que los textos o el mensaje visible sumado a ese enlace además de el objetivo final nunca coinciden con frecuencia. Todo salto de formato de clase GoTo para los casos dentro del nivel interno del formato del documento son más una facción propia a la fase formativa que atañe al documento y en un sentido adverso a su propio accionar, de suerte que se ven fuera de toda instancia afín a figurar en las revisiones y los informes, porque de lo contrario se terminaría con que las revisiones que siempre dan la nota de aviso se vuelven una total molestia al punto de acostumbrar y educar a los miembros a hacer a las mismas a un lado en general. En lo alusivo al grado por dar por leído a todo ese conglomerado o matriz de cada script y a toda variable escondida detrás del listado de los JavaScript, así como todo al nivel o grado en función con el MDP de todas y cada una de las marcas de los documentos, lo que a su vez se aúna y le acompaña en conjunción con los procesos respectivos para hallar a la XFA, este artículo de auditoría a cada instancia de los peligros concernientes al aspecto de los riesgos de protección se encarga de ilustrar a este mismísimo aspecto con el uso íntegro respectivo de envoltura en clase objetiva del mismísimo componente
Métrica de recursos: matemáticas efectivas de PPP de imagen
Toda imagen integrada y encerrada bajo el marco formal de un archivo del PDF no se dispone a un PPP por sí misma de forma alguna. Por otro lado es dueña de su variable matriz de píxeles, a modo de los que recae la misión de parte de la misma página de encuadrarlos en forma de un rectangular medido en puntos de un modo tal que unos 72 dan lugar así por fin a una pulgada total. Resulta a causa de lo mismo que la medida en calidad de la resolución se haya presente no más que en su función en estado del índice en paridad, lo cual viene a explicar los porqué tras los casos donde es normal dar con una gráfica fotográfica de una resolución a priori de un estimado 600 por su contraparte de 400 que recae en un formato y resultado prístino cuando se la enmarca bajo la forma peculiar a las de las imágenes miniatura pero que a fin se destruye su nitidez y visual total de verse forzada en estado heroico a gran formato. Es gracias a que es el caso de un suceso con ese talante que a los fines para la respectiva y peculiar labor por revisar, recae toda urgencia para que el auditor cuente con ambas cifras para cada fotografía y cada ejemplo u matriz del documento en la mano: su total en dimensión de su mismo de píxel matriz provenientes del respectivo archivo meta original sumados en igual instancia y a par con el rectángulo base de cara a los bordes atañentes y relativos que corresponden por el mismo de clase objetiva
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; // placed size on the page, in points
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 points = 1 inch, so placed inches = points / 72, and
// effective DPI = source pixels / placed inches.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // the worse axis decides print quality
if EffDpi < 150.0 then
Add(Findings, fsWarning, 'IMG-LOWRES', PageNo,
Format('imagen de %dx%d px ubicada en %.1fx%.1f pt = efectiva %.0f ppp',
[Meta.Width, Meta.Height, WidthPt, HeightPt, EffDpi]))
else if EffDpi > 600.0 then
Add(Findings, fsInfo, 'IMG-BLOAT', PageNo,
Format('la imagen es de %.0f ppp en su tamaño ubicado; volver a muestrearla encogería el tamaño del archivo sin que haya pérdida visible alguna', [EffDpi]));
end;
end;
Todos estos umbrales son a fin de cuentas producto por normas propias y no más de físicas: en relación que se parte del 150 en ppp es a fin de su suelo inferior frente el que si baja cae en los grados de todo lo observable mediante de la pixelada y más su fin para la general en fin para 300 a una clase atañente que general en fin que más en total por la que subyacente o mayor a los 600 nada para su en general un bien el cual de una que en adición sea más pesado a su propio un nivel el cual un para el caso del a fin en su vez su o un. O en su vez de un para su en el que de en un al fin un a de su FPDFPageObj_GetBounds por un de a al en para a una. A su FPDF_IMAGEOBJ_METADATA del a de en su vez por de en de su con que horizontal_dpi y a su vertical_dpi a para de en a del de por un en a que para a un por un en la de por a un de a y en a un. Del a en por el a del un por la a de por a de un a en la de un para por la de. En a de por la de. En el a del por en a un artículo sobre exportación de JPEG
De por el a del un en la de por a en y
Del de a un por en a del un para de un en a y a del el a de. Para con el a del y en por a de en de por a un a el en la un para con de a en por a un a de en y por a con un y del a. Para el a de en a del un de la por a. Y el a un por en un a de la un y el del a. A de un por un en a de la a un en. De en de por la a de en a un a el en de a la por un a en a del un de por a de un y en un a en por a de a FPDF_LoadDocument en de por la nil a del en a de FPDF_GetLastError por a en de a FPDF_ERR_PASSWORD. Por a en la a un en a del por de un a en. En a un por a de la por a en a un de por la a un de la por a en a del a en un. En a del a por a de en a un
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,
'se requiere de una contraseña de usuario (para abrir); no es posible llevar a cabo la auditoría')
else
Add(Findings, fsError, 'DOC-BROKEN', 0, 'error en la página al momento del análisis sintáctico');
Exit;
end;
Revision := FPDF_GetSecurityHandlerRevision(Result);
if Revision >= 0 then // -1 means the file is not encrypted
begin
// Opened with an empty password yet encrypted: owner-password-only.
// Anyone may read it, but the permission bits restrict what a
// conforming reader lets them do. Unencrypted files report all
// bits set, which is why the revision gate comes first.
Perms := FPDF_GetDocPermissions(Result);
Add(Findings, fsInfo, 'SEC-ENC', 0,
Format('encriptado, revisión %d del encargado de la seguridad', [Revision]));
if (Perms and 4) = 0 then // bit 3: print
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'no se permite la impresión');
if (Perms and 16) = 0 then // bit 5: copy / extract content
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'no se permite la extracción del contenido');
if (Perms and 2048) = 0 then // bit 12: high-resolution print
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'solo se permite la impresión en baja resolución');
end;
end;
Las máscaras proceden de la Tabla 22 de ISO 32000-1, que numera los bits a partir de 1: el bit 3 del valor /P es la máscara 4, el bit 5 es 16, el bit 12 es 2048. La importancia de un hallazgo determinado es una decisión de enrutamiento. Una oficina de impresión debería rechazar un archivo SEC-NOPRINT en el momento de la admisión, donde el remitente recibe un mensaje claro, en lugar de en el procesador de imágenes rasterizadas (RIP) tres horas antes de la fecha límite. Un archivo debería tratar al propio SEC-ENC como un bloqueador, ya que la encriptación y la conservación a largo plazo no se mezclan; un punto que la comprobación de estándares está a punto de plantear formalmente
Marcadores de estándares: lectura de una declaración en PDF/A
Un archivo declara su conformidad con PDF/A en su paquete de metadatos XMP, a través de la propiedad pdfaid:part (1 a 4) y de pdfaid:conformance (la letra del nivel, como b para fidelidad visual o a para etiquetado estructural completo). La API de C de PDFium no ofrece ningún descriptor de acceso XMP; FPDF_GetMetaText lee únicamente el diccionario de información (Info), que no es donde reside la identificación. La vía de escape es una regla en la norma misma: la ISO 19005 requiere que el flujo de metadatos XMP se almacene sin comprimir, precisamente para que las herramientas puedan encontrarlo sin un analizador sintáctico de PDF completo. Un análisis de bytes sin procesar es, por lo tanto, un detector de afirmaciones legítimo, y un archivo cuya afirmación se esconde dentro de un flujo comprimido ya ha violado la norma que afirma cumplir
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // empty = no PDF/A claim present
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // XMP identification schema
if P = 0 then
Exit;
// Handles both <pdfaid:part>2</pdfaid:part> and pdfaid:part="2":
// take the first digit after the property name.
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 simplemente eso, una declaración, no una propiedad del archivo. La entrada XMP es una línea de código XML que puede escribir cualquier productor, incluso uno defectuoso; la conformidad es el hecho de que el archivo satisfaga realmente cientos de reglas acerca de fuentes integradas, colores independientes del dispositivo y características prohibidas. La detección de la afirmación le indica qué archivos debe enviar para que se los valide de verdad, y nada más. El motor de preimpresión integrado del componente ejecuta dicha validación a lo largo de los perfiles PDF/A, PDF/UA y PDF/X, y el artículo sobre la CLI de lotes muestra cómo integrarlo en una canalización con informes que un auditor puede abrir más tarde
Una ejecución contra un archivo problemático
El controlador encadena las comprobaciones: primero la seguridad, porque ella es la que decide si la auditoría se ejecuta o no, y luego le siguen los comportamientos a nivel del documento, la declaración de estándares y un bucle por cada página para las 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); // audit failure, not a verdict
try
AuditDocumentBehaviors(Doc, Findings);
Claim := PdfAClaim(FileName);
if Claim <> '' then
Add(Findings, fsInfo, 'STD-PDFA', 0,
Claim + ' conformidad declarada (solo declaración, sin validación)');
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, 'error en la página al momento del análisis sintáctico');
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 provino de una agencia externa, el resultado se ve así:
> preflight_audit brochure_final.pdf
brochure_final.pdf: 5 finding(s)
[ERROR] ACT-LAUNCH page 3 La acción de inicio apunta a "..\tools\setup.exe"
[ERROR] JS-DOC doc 2 acciones de JavaScript a nivel de documento se ejecutan al abrirse
[WARNING] IMG-LOWRES page 7 imagen de 412x287 px ubicada en 396.0x275.8 pt = efectiva 75 ppp
[WARNING] SEC-NOPRINT doc no se permite la impresión
[INFO] STD-PDFA doc conformidad con PDF/A-2 declarada (solo declaración, sin validación)
exit code 1
Cada línea es objeto de acción por sí misma, pero la combinación es el verdadero veredicto. Este archivo declara ser PDF/A-2 mientras que porta un diccionario de encriptación y un JavaScript activo, y el PDF/A prohíbe rotundamente ambos, por lo que puede demostrarse que la declaración es falsa antes de que se ejecute cualquier validador profundo. Ese es el tipo de contradicción que saca a la luz una lista de hallazgos plana y oculta un booleano que pasa o falla
Lo que esta auditoría no puede decirle
La honestidad en cuanto al alcance es lo que mantiene la confianza en una herramienta de preimpresión. Todo lo anterior lee lo que el archivo declara acerca de sí mismo: PDFium analiza la estructura de manera sintáctica y esta auditoría la inventaría. No realiza una validación PDF/A: sin verificaciones de cobertura de glifos en comparación con las fuentes incrustadas, ni análisis del espacio de color frente a los propósitos de salida, y tampoco contiene ninguna de las reglas a nivel de cláusula que separan una afirmación de la conformidad; para ello se necesita un validador dedicado, como el motor de preimpresión del componente o veraPDF. Los bits de permisos son declaraciones que los lectores que conforman cumplen, no muros criptográficos, por lo que SEC-NOPRINT describe el intento más que la ejecución. El análisis de acciones abarca las anotaciones de enlaces y los scripts a nivel del documento; los scripts ocultos en los diccionarios de eventos de los campos de formulario necesitan además las API de formularios. Además, si amplía la auditoría con una comprobación de firma, lo que informará es el propósito declarado y no una criptografía verificada: la validación de la cadena de certificados es un trabajo aparte. Una auditoría de preimpresión es la entrevista de admisión, no el juicio: su función es hacer que la decisión de enrutamiento sea informada, rápida y repetible
Nota: Las API de objetos de imagen, anotaciones, páginas y documentos usadas a lo largo de esta auditoría, junto a un envoltorio de alto nivel para Delphi y un motor de preimpresión de validación de estándares completo, se entregan con el Componente PDFium