Articolo tecnico

Workbench di triage PDF in ingresso in Delphi con PDFium

Un workbench di revisione dei PDF in ingresso è un piccolo programma con un solo compito: guardare ogni file prima che qualsiasi cosa a valle possa toccarlo. Per svolgere quel compito deve riunire una manciata di capacità in una sola passata. Apre il file (senza fidarsene), legge ciò che il file dichiara su sé stesso, cerca contenuti che inganneranno un estrattore ingenuo o che veicoleranno un attacco, decide se esiste del testo estraibile e poi instrada il documento verso una coda in base a ciò che ha trovato. Saltate l'ispezione e i guasti saranno silenziosi: un PDF cifrato con password proprietario che avvolge un modulo XFA attraversa un estrattore di testo come stringhe vuote, viene indicizzato come documento in bianco e nessuno se ne accorge finché qualcuno a valle non va a cercare un contenuto che non è mai stato letto. PDFium Component è un viewer VCL/LCL con sorgenti e una libreria di ispezione per Delphi, C++Builder e Lazarus, ed espone le chiamate di introspezione di cui questo workbench ha bisogno. Le sezioni seguenti mostrano quale chiamata risponde a quale domanda, e i due punti in cui la chiamata ovvia vi dà con sicurezza una risposta sbagliata

Cinque domande a cui rispondere prima di instradare un file

Tolti la griglia e la striscia delle miniature, il triage in ingresso si riduce a cinque domande:

  • Il file si riesce ad aprire, e con quale password?
  • Che cosa dichiara di essere: titolo, autore, data di creazione?
  • Contiene elementi attivi o rischiosi come JavaScript, un modulo XFA o file incorporati?
  • Esiste testo estraibile, oppure è una scansione destinata a OCR?
  • Alla luce di tutto questo, quale coda lo riceve: elaborazione diretta, revisione manuale o quarantena?
Diagramma di un workbench Delphi per PDF in ingresso che risponde a cinque domande di triage in una sola apertura economica e instrada i file negli stati pronto, revisione, bloccato o danneggiato
Il triage in ingresso risponde a cinque domande in una sola apertura economica e instrada il file verso pronto, revisione, bloccato o danneggiato

Ogni domanda corrisponde a una o due chiamate di PDFium Component. Due di quelle corrispondenze hanno spigoli vivi che spiegano la maggior parte dei file instradati male che ho dovuto analizzare in produzione. I metadati del documento risiedono in due posti diversi che possono non concordare, e la cifratura non impedisce necessariamente a un documento di aprirsi

Aprire in modo economico: form fill spento, zero pagine renderizzate

Il triage dovrebbe essere l'apertura meno costosa possibile. Impostare FormFill := False prima di Active := True dice al componente di saltare completamente l'ambiente di compilazione dei moduli. Questo accorcia il tempo di caricamento e (cosa altrettanto importante per file di origine ignota) impedisce l'inizializzazione di qualsiasi JavaScript a livello di documento. Nessuna delle proprietà di ispezione usate qui sotto richiede di renderizzare una pagina, quindi una passata di triage non deve mai produrre neppure una bitmap

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // nessun ambiente form, nessun avvio di JavaScript
    Pdf.Active := True;        // il fallimento è silenzioso: Active resta False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // file danneggiato o blocco con password utente
      Exit;                    // il blocco finally viene comunque eseguito
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // mai perdere istanze su un file malformato
  end;
end;

Il controllo dopo l'assegnazione non è facoltativo, ed è un controllo anziché un gestore di eccezioni per un motivo preciso. Quando il motore non riesce a caricare il file, il componente inghiotte l'EPdfError interno e lascia Active a False invece di propagarlo. Il codice che aspetta un'eccezione leggerà tranquillamente PageCount da un documento che non si è mai aperto. Se il flusso di rifiuto ha bisogno del testo di errore reale del motore, leggete il file in un array di byte e chiamate l'overload di LoadDocument che accetta TBytes; quel percorso solleva davvero EPdfError con il messaggio, incluso il caso della password. Il try..finally si guadagna comunque il proprio posto. I servizi di intake girano senza sorveglianza per settimane, e nessuna eccezione successiva deve poter perdere l'istanza di TPdf o trattenere un lock su cui inciamperà la passata di ritentativo

Il throughput raramente diventa il collo di bottiglia. Con il form fill disattivato e senza rendering, un'apertura di triage è dominata dall'I/O, e un singolo worker ispeziona comodamente diversi file al secondo da disco locale. Se il volume in ingresso supera davvero le capacità di un worker, ripartite il lavoro per file e non per controllo. Le cinque domande condividono una sola apertura, e distribuirle fra processi moltiplicherebbe il passo più costoso invece di ammortizzarlo

I metadati risiedono in due posti, e i due non concordano

ISO 32000-1 definisce due sedi per i metadati del documento: il dizionario di informazioni del documento (clausola 14.3.3) e un pacchetto XMP allegato al catalogo (clausola 14.3.2). Le proprietà Title, Author, Subject e CreationDate leggono il dizionario Info, con MetaText[] per qualsiasi altra chiave e DecodeDate per interpretare la stringa di data D:YYYYMMDD.... Il problema è che i produttori moderni scrivono sempre più spesso soltanto XMP, una direzione che ISO 32000-2 rende ufficiale deprecando la maggior parte delle chiavi del dizionario Info in PDF 2.0. Il sintomo in uno strumento di intake è concreto. Il vostro workbench mostra un titolo vuoto mentre Adobe Acrobat ne visualizza uno, perché Acrobat è ricaduto su dc:title dentro il pacchetto XMP, che le proprietà del dizionario Info non toccano mai

Diagramma dei metadati PDF che risiedono in due posti, il dizionario Info e il pacchetto XMP, che possono non concordare sul titolo in uno strumento di intake Delphi
I metadati del documento risiedono nel dizionario Info e nel pacchetto XMP, e le due sedi possono non concordare sul titolo
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // valore del dizionario Info
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // stringa di data PDF grezza ("D:2026...")

  // Un titolo Info vuoto non significa che il documento sia senza titolo.
  // Il componente non espone il pacchetto XMP, quindi cercate nei byte
  // grezzi l'elemento dc:title prima di fidarvi del campo vuoto.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Persino il rozzo controllo per sottostringa qui sopra si guadagna il proprio posto: metadati presenti, ma non dove li cercano gli strumenti tradizionali, è un fatto rilevante per l'instradamento in qualsiasi pipeline di archiviazione che indicizzi per titolo o autore. Se il vostro indice a valle legge soltanto il dizionario Info, i file segnalati in questo modo diventeranno silenziosamente non ricercabili

File cifrati che si aprono lo stesso

Un documento cifrato non necessariamente non si apre. Il gestore di sicurezza standard (ISO 32000-1 clausola 7.6.3) distingue una password utente, necessaria per aprire il documento, da una password proprietario che si limita a regolare permessi come stampa e copia. Una quota consistente dei documenti aziendali protetti è cifrata con una password proprietario e una password utente vuota. Si aprono senza richiedere nulla, si decifrano completamente e contano sul fatto che i viewer si offrano spontaneamente di rispettare i flag di permesso. Quella è politica, non protezione, e i vostri stati di intake dovrebbero riflettere la differenza

Rilevare la cifratura dopo un'apertura riuscita richiede una chiamata al motore più un ripiego. FPDF_GetSecurityHandlerRevision(Pdf.Document) restituisce -1 per i file non protetti e altrimenti la revisione del gestore, e il fatto che Pdf.Permissions restituisca qualcosa di diverso dalla maschera con tutti i bit impostati $FFFFFFFF è il segnale di conferma. Per i file davvero bloccati da password utente, assegnate Password prima di impostare Active := True; se l'apertura fallisce ancora, instradate il file verso uno stato bloccato che richieda le credenziali al mittente tramite un canale sicuro invece di ritentare alla cieca. E resistete alla tentazione di trattare la cifratura come una quarantena automatica. Nella maggior parte dei settori a forte intensità documentale, i file cifrati ma apribili sono il caso normale, non quello sospetto

Contenuto attivo: JavaScript, XFA e file incorporati

Tre riscontri dovrebbero sempre arrivare alla decisione di instradamento. Primo, JavaScript: l'evento OnUnsupportedFeature segnala caratteristiche strutturali come XFA o contenuti 3D man mano che il motore le incontra, ma non rileva JavaScript. Controllate invece JavaScriptActionCount e trattate un risultato diverso da zero come contenuto attivo. Secondo, XFA: quando FormType restituisce ftXfaFull, le pagine visibili sono spesso poco più di un rendering del template XFA, e l'estrazione di testo convenzionale vedrà testo standard invece dei valori compilati. Terzo, allegati: un PDF è un formato contenitore, e AttachmentCount vi dice se questo trasporta passeggeri

Diagramma dei segnali di rischio nel triage PDF in Delphi: revisione del gestore di cifratura, conteggio delle azioni JavaScript, tipo di modulo XFA e allegati pericolosi
Lo stato di cifratura e i conteggi di JavaScript, XFA e allegati sono i segnali che devono sopravvivere fino alla decisione di instradamento
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 è una proprietà per pagina; scorrete le pagine per
  // sommarla. Caricare una pagina non renderizza nulla: resta economico.
  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;

Due dettagli di quel ciclo meritano attenzione. Il nome dell'allegato proviene dall'interno del documento, quindi non riutilizzatelo mai come percorso di output senza prima ripulirlo; un nome incorporato come ..\..\start.exe è un path traversal in attesa di una chiamata di salvataggio distratta. E una blocklist di estensioni è un filo di allarme, non una garanzia. Il suo compito è costringere a una decisione umana, non certificare che il file sia pulito

Trasformare i segnali in stati di instradamento

Un modello di stati funzionante richiede meno stati di quanti la maggior parte dei team si aspetti: pronto (nessun ostacolo, testo presente), revisione (apertura riuscita ma qualcosa richiede occhi umani, come un modulo XFA, JavaScript, un livello di testo vuoto o un titolo presente solo in XMP), bloccato (serve la password utente) e danneggiato (apertura fallita). Registrate le prove accanto allo stato. L'hash del file, il conteggio delle pagine, i flag esatti e il messaggio di errore del motore per i file danneggiati contano tutti, perché chi metterà in discussione una decisione di instradamento lo farà settimane dopo, a fronte di un file che nel frattempo potrebbe essere stato sostituito o modificato

Quando un operatore ha davvero bisogno di guardare un file in quarantena, non consegnatelo al viewer predefinito del sistema. Renderizzatelo dentro un riquadro irrobustito con scripting e gestione dei collegamenti disattivati, l'approccio descritto in come costruire una superficie di anteprima PDF sicura in Delphi. E se il vostro intake alimenta un archivio con requisiti di conformità, la passata di triage è il punto naturale in cui programmare un controllo più approfondito; la validazione preflight in batch rispetto ai profili PDF/A e PDF/UA riprende esattamente dove questa ispezione si ferma

La pagina di prodotto del componente copre le licenze, l'API di ispezione completa e i demo inclusi, fra cui un ispettore di documenti in stile intake: PDFium Component