Un PDF che arriva a un confine di produzione — una coda di stampa, un archivio, un portale di caricamento per i clienti — andrebbe verificato prima che qualcosa lo renda. Il file potrebbe portare un'azione Launch collegata all'avvio di un programma esterno, immagini troppo grossolane per sopravvivere alla stampa, un dizionario di cifratura che vieta proprio il lavoro di stampa per cui è stato inviato, oppure un'etichetta PDF/A che non rispetta. Ispezionare un documento rispetto a regole come queste prima che entri in un flusso di lavoro si chiama preflight, e la API C di PDFium dà a Delphi tutto il necessario per implementare i controlli direttamente, senza rendere una sola pagina
Questo articolo costruisce i controlli veri e propri: quattro classi di audit, ciascuna una piccola routine che accoda i risultati a un elenco condiviso. Elementi interattivi, metriche delle risorse, stato di sicurezza e marcatori di conformità ottengono tutti codice funzionante, aritmetica inclusa. Se quel che vi serve è la meccanica attorno ai controlli — cicli su cartelle in batch, file di report JSON e HTML, isolamento per file — il PDFium Component distribuisce un motore di preflight già pronto, e l'articolo sulla CLI di preflight in batch tratta quell'impianto. I due condividono deliberatamente un unico vocabolario di codici di uscita, così un auditor scritto qui si incastra direttamente sotto quel driver batch
Il record dei risultati e il contratto sui codici di uscita
Ogni controllo scrive in un unico tipo di record piatto, perché l'alternativa, ogni controllo che stampa la propria prosa, non si può contare, filtrare né confrontare con una soglia in un secondo momento. Bastano quattro campi
uses
System.SysUtils, System.Math, System.IOUtils,
System.Generics.Collections, pdfium_lib;
type
TFindingSeverity = (fsInfo, fsWarning, fsError);
TPreflightFinding = record
Severity: TFindingSeverity;
Code: string; // chiave macchina stabile, ad es. 'ACT-LAUNCH'
Page: Integer; // in base 1; 0 significa livello documento
Message: string; // per le persone; riformulabile fra le release
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;
Gli strumenti a valle si basano su Code, mai sul testo di Message, che è libero di cambiare. Il codice di uscita del processo segue lo stesso contratto a tre valori dell'articolo sul batch: 0 significa che il file non ha prodotto risultati, 1 che esistono risultati e 2 che l'audit stesso non è potuto partire perché il file non si è analizzato o richiede una password. Tenere il codice 2 separato conta. Una cartella di scansioni corrotte è uno scanner rotto a monte, non un improvviso crollo della conformità, e mescolare le due cose manda qualcuno a inseguire il problema sbagliato
Elementi interattivi: script, destinazioni Launch, collegamenti esterni
PDFium classifica ogni azione che trova con un tipo intero, e vale la pena fissare con precisione le costanti di fpdf_doc.h, perché valori copiati male rendono uno scanner cieco in silenzio. L'enumerazione reale è PDFACTION_UNSUPPORTED = 0, PDFACTION_GOTO = 1, PDFACTION_REMOTEGOTO = 2, PDFACTION_URI = 3, PDFACTION_LAUNCH = 4 e PDFACTION_EMBEDDEDGOTO = 5. Notate cosa manca: non c'è un membro JavaScript. Gli script a livello di documento non sono azioni di collegamento e non compaiono mai attraverso FPDFAction_GetType; vengono enumerati da una famiglia di chiamate separata. Un auditor che confronti i tipi di azione con una costante JavaScript immaginaria compila, gira e non trova nulla, per sempre
const
PDFACTION_GOTO = 1; // salto interno al documento: innocuo
PDFACTION_REMOTEGOTO = 2; // salto dentro un altro file locale
PDFACTION_URI = 3; // apre un URL esterno
PDFACTION_LAUNCH = 4; // avvia un programma esterno
PDFACTION_EMBEDDEDGOTO = 5; // salto dentro un file incorporato
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; // collegamento con sola destinazione, nulla da segnalare
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 resta silenzioso per scelta
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 ripartizione delle gravità codifica una politica. Un'azione Launch è un errore perché avviare un programma arbitrario è la cosa più pericolosa che un clic dentro un PDF possa fare, e nessuna fattura ne ha bisogno. Gli URI esterni sono avvisi: comuni nei documenti legittimi, ma un revisore dovrebbe vedere la destinazione senza fare clic, dato che il testo visibile del collegamento e la destinazione reale non devono per forza coincidere. I salti GoTo interni al documento sono struttura, non comportamento, e restano del tutto fuori dal report — un preflight che grida al lupo su ogni voce di un indice insegna alle persone a ignorarlo. Per leggere i corpi degli script dietro il conteggio JavaScript, e per i livelli MDP delle firme e il rilevamento XFA, l'articolo sull'audit dei rischi di sicurezza percorre la stessa superficie attraverso il wrapper a oggetti del componente
Metriche delle risorse: DPI effettivi delle immagini
Un'immagine dentro un PDF non ha DPI propri. Ha pixel, e la pagina colloca quei pixel in un rettangolo misurato in punti, dove 72 punti fanno un pollice. La risoluzione esiste solo come rapporto fra i due, ed è per questo che la stessa foto da 600 per 400 è nitidissima come miniatura e un pasticcio sfocato come immagine a piena pagina. L'audit ha quindi bisogno di entrambi i numeri per ogni immagine: le dimensioni in pixel della sorgente dai metadati dell'immagine e il rettangolo di collocazione dai confini dell'oggetto
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; // dimensione collocata sulla pagina, in punti
HeightPt := T - B;
if (WidthPt <= 0) or (HeightPt <= 0) or
(Meta.Width = 0) or (Meta.Height = 0) then
Continue;
// 72 punti = 1 pollice, quindi i pollici collocati = punti / 72, e
// i DPI effettivi = pixel sorgente / pollici collocati.
DpiX := Meta.Width / (WidthPt / 72.0);
DpiY := Meta.Height / (HeightPt / 72.0);
EffDpi := Min(DpiX, DpiY); // l'asse peggiore decide la qualità di stampa
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;
Le soglie sono politica, non fisica: 150 DPI è un pavimento sotto il quale la stampa da ufficio si sgrana visibilmente, 300 è il consueto obiettivo commerciale, e qualsiasi cosa sopra i 600 non compra qualità visibile mentre gonfia la dimensione del file, ed è per questo che viene riportata come sovrappeso informativo anziché come difetto. Un avvertimento onesto: FPDFPageObj_GetBounds restituisce il riquadro allineato agli assi, quindi per un'immagine collocata con una rotazione la cifra calcolata sottostima la densità reale. La struttura FPDF_IMAGEOBJ_METADATA porta anche i campi horizontal_dpi e vertical_dpi che PDFium deriva dalla matrice di trasformazione completa, e confrontare i due risultati è un modo economico per individuare le collocazioni ruotate. La stessa aritmetica fra punti e pixel guida il rendering nella direzione opposta, trattata nell'articolo sull'esportazione JPEG
Stato di sicurezza: cifratura e bit di permesso
La cifratura PDF definisce due password con compiti diversi. La password utente presidia la decifratura: senza di essa il file non si apre affatto, e FPDF_LoadDocument restituisce nil con FPDF_GetLastError che riporta FPDF_ERR_PASSWORD. La password proprietario presidia i permessi: un file protetto solo da una password proprietario si apre senza credenziali ma porta bit di restrizione che un lettore conforme deve onorare. Il tentativo di caricamento è quindi il primo sondaggio di sicurezza, e la distinzione decide il codice di uscita — un file con password utente non è verificabile (codice 2), mentre un file con password proprietario si verifica normalmente e si limita ad accumulare risultati
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 che il file non è cifrato
begin
// Aperto con password vuota eppure cifrato: solo password proprietario.
// Chiunque può leggerlo, ma i bit di permesso limitano ciò che un
// lettore conforme gli lascia fare. I file non cifrati riportano tutti
// i bit impostati, ed è per questo che il controllo sulla revisione viene prima.
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: stampa
Add(Findings, fsWarning, 'SEC-NOPRINT', 0,
'printing is not permitted');
if (Perms and 16) = 0 then // bit 5: copia / estrazione del contenuto
Add(Findings, fsInfo, 'SEC-NOCOPY', 0,
'content extraction is not permitted');
if (Perms and 2048) = 0 then // bit 12: stampa ad alta risoluzione
Add(Findings, fsWarning, 'SEC-LOWPRINT', 0,
'only low-resolution printing is permitted');
end;
end;
Le maschere vengono dalla Tabella 22 di ISO 32000-1, che numera i bit a partire da 1: il bit 3 del valore /P è la maschera 4, il bit 5 è 16, il bit 12 è 2048. Se un dato risultato conti oppure no è una decisione di instradamento. Un centro stampa dovrebbe respingere un file SEC-NOPRINT alla ricezione, dove chi lo invia riceve un messaggio chiaro, anziché al RIP tre ore prima di una scadenza. Un archivio dovrebbe trattare SEC-ENC stesso come un blocco, dato che cifratura e conservazione a lungo termine non vanno d'accordo — un punto che il controllo sulla conformità sta per rendere formale
Marcatori di conformità: leggere un claim PDF/A
Un file dichiara la conformità PDF/A nel proprio pacchetto di metadati XMP, tramite la proprietà pdfaid:part (da 1 a 4) e pdfaid:conformance (la lettera del livello, come b per la fedeltà visiva o a per la marcatura strutturale completa). La API C di PDFium non offre alcun accessore XMP; FPDF_GetMetaText legge solo il dizionario Info, che non è dove risiede l'identificazione. La via d'uscita è una regola dello standard stesso: ISO 19005 richiede che il flusso di metadati XMP sia memorizzato non compresso, proprio perché gli strumenti possano trovarlo senza un parser PDF completo. Una scansione grezza dei byte è quindi un rilevatore di claim legittimo — e un file il cui claim si nasconde dentro un flusso compresso ha già violato lo standard che dichiara
function PdfAClaim(const FileName: string): string;
var
Bytes: TBytes;
S: RawByteString;
P, Limit: Integer;
begin
Result := ''; // vuoto = nessun claim PDF/A presente
Bytes := TFile.ReadAllBytes(FileName);
if Length(Bytes) = 0 then
Exit;
SetString(S, PAnsiChar(@Bytes[0]), Length(Bytes));
P := Pos('pdfaid:part', S); // schema di identificazione XMP
if P = 0 then
Exit;
// Gestisce sia <pdfaid:part>2</pdfaid:part> sia pdfaid:part="2":
// prende la prima cifra dopo il nome della proprietà.
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;
Il risultato che ne esce è deliberatamente informativo, perché un claim è una dichiarazione, non una proprietà del file. La voce XMP è una riga di XML che qualsiasi produttore può scrivere, anche uno difettoso; la conformità è il file che soddisfa davvero centinaia di regole su font incorporati, colore indipendente dal dispositivo e funzionalità vietate. Rilevare il claim vi dice quali file instradare a una validazione vera, e nulla di più. Il motore di preflight incorporato nel componente esegue quella validazione sui profili PDF/A, PDF/UA e PDF/X, e l'articolo sulla CLI in batch mostra come inserirlo in una pipeline con report che un revisore possa aprire in seguito
Una esecuzione su un file problematico
Il driver mette in fila i controlli: prima la sicurezza, perché decide se l'audit possa girare, poi i comportamenti a livello di documento e il claim di conformità, poi un ciclo sulle pagine per azioni e immagini
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); // fallimento dell'audit, non un verdetto
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;
Su una brochure tornata da un'agenzia esterna, il risultato si presenta così
> 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
Ogni riga è azionabile per conto suo, ma è la combinazione il verdetto vero. Questo file dichiara PDF/A-2 pur portando un dizionario di cifratura e JavaScript attivo, e PDF/A vieta entrambi in modo netto — quindi il claim è dimostrabilmente falso prima ancora che giri un validatore approfondito. È il tipo di contraddizione che un elenco piatto di risultati fa emergere e che un esito booleano di superato o non superato nasconde
Cosa questo audit non può dirvi
L'onestà sull'ambito è ciò che mantiene affidabile uno strumento di preflight. Tutto quanto sopra legge ciò che il file dichiara di sé: PDFium ne analizza la struttura, e questo audit la inventaria. Non esegue la validazione PDF/A — nessun controllo di copertura dei glifi contro i font incorporati, nessuna analisi degli spazi colore contro gli output intent, nessuna delle regole a livello di clausola che separano un claim dalla conformità; per quello vi serve un validatore dedicato come il motore di preflight del componente oppure veraPDF. I bit di permesso sono dichiarazioni che i lettori conformi onorano, non muri crittografici, quindi SEC-NOPRINT descrive un'intenzione anziché un'imposizione. La scansione delle azioni copre le annotazioni di collegamento e gli script a livello di documento; gli script sepolti nei dizionari di evento dei campi modulo richiedono in più le API dei moduli. E un controllo sulle firme, se estendete l'audit con uno, riporta l'intenzione dichiarata, non la crittografia verificata — la validazione della catena di certificati è un lavoro a parte. Un audit di preflight è il colloquio di ammissione, non il processo: il suo compito è rendere la decisione di instradamento informata, rapida e ripetibile
Nota: le API per documento, pagina, annotazioni e oggetti immagine usate in tutto questo audit, insieme a un wrapper Delphi di alto livello e a un motore di preflight completo per la validazione degli standard, sono distribuite con PDFium Component