Articolo tecnico

Vera redazione PDF in Delphi: eliminazione e sanificazione

La vera redazione di un PDF in Delphi significa eliminare i byte sottostanti, non dipingerci sopra. losLab PDF Library traccia questa linea con due API: RedactRegion modifica il content stream della pagina in modo che il testo, i vettori e le immagini rimossi siano fisicamente eliminati, e SanitizeDocument cancella i metadati, gli script e le azioni in cui si nascondono anche le informazioni redatte. Se sbagli una delle due parti, invierai un file che appare censurato sullo schermo ma che restituisce il segreto a chiunque copi il testo o apra le Proprietà del documento

Questa modalità di fallimento è di dominio pubblico. Giornali, tribunali e agenzie governative hanno tutti rilasciato PDF con un rettangolo nero sovraposto a un nome o a un numero, solo perché un lettore potesse selezionare il testo sottostante, copiarlo e incollare la stringa presumibilmente redatta in bella vista. Il rettangolo era solo un disegno. Il testo era ancora lì, presente nel content stream un livello sotto il colore, del tutto intatto e selezionabile. losLab PDF Library, il motore PDF per Delphi e C++Builder esposto tramite la classe TPDFlib, tratta questa distinzione come il punto centrale della redazione piuttosto che come un'idea a posteriori

Perché disegnare una casella nera sul testo di un PDF non è una vera redazione?

losLab PDF Library espone MaskRect per i casi in cui la copertura è sinceramente ciò che desideri: una filigrana, un oscuramento visivo, un timbro di prova. MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) aggiunge un rettangolo al content stream della pagina e lo disegna con l'operatore di costruzione del tracciato re seguito dall'operatore di riempimento f (ISO 32000-1 §8.5). Ogni byte già presente sulla pagina — i glifi del nome coperto, i tratti vettoriali, l'immagine incorporata — rimane esattamente dove era, un'istruzione prima nello stesso stream. Un visualizzatore conforme disegna la casella nera per ultima, quindi prevale visivamente. La copertura non cambia nulla al di sotto

Chiunque può recuperare il contenuto originale in tre modi: selezionando e copiando il testo direttamente attraverso la casella opaca, eseguendo un estrattore di testo sulla pagina o eliminando il rettangolo di copertura dal content stream. MaskRect è onesto fin dal nome: maschera, non rimuove. Il risultato è come un adesivo sopra una parola che si può ancora leggere sollevando l'adesivo stesso

Come RedactRegion elimina il contenuto a livello di istruzione

RedactRegion(Page, Left, Top, Width, Height, Options) agisce un livello sotto la pittura. losLab PDF Library analizza ogni livello di contenuto della pagina in un TPDFContentProgram, scorre l'elenco delle istruzioni con un analizzatore di tracciamento CTM (FindInstructionsInRect nel modello di contenuto) e contrassegna ogni operatore di visualizzazione del testo, disegno del tracciato e immagine Do la cui area disegnata interseca il rettangolo. Tali istruzioni vengono rimosse con TPDFContentProgram.Delete e il livello modificato viene riscritto con WritePageContentLayer. I byte non scivolano dietro una copertura: lasciano lo stream. Un estrattore di testo eseguito successivamente non troverà nulla, perché non c'è nulla da trovare

Vale la pena includere due dettagli di implementazione nel tuo ragionamento. RedactRegion accetta un rettangolo in alto a sinistra nell'origine e nelle unità correnti e lo converte internamente nello spazio utente in basso a sinistra utilizzato dal content stream, in modo da specificare la regione nel modo in cui la vedi sulla pagina. Quando un rettangolo copre diverse istruzioni, RedactRegion le elimina in ordine di indice decrescente, poiché la rimozione dell'istruzione 4 prima dell'istruzione 7 sposterebbe ogni indice successivo e corromperebbe l'intervallo. Passando Options = 0, losLab PDF Library stampa anche una casella nera piena sulla regione ripulita come indicatore visibile, ma tale indicatore è puramente cosmetico, applicato dopo che il vero contenuto è già andato perduto

In pratica raramente si cabla il rettangolo nel codice. Cerchi nella pagina la stringa sensibile e leggi la sua casella di delimitazione (bounding box) — la stessa ricerca di pagina ed enumerazione degli elementi trattata in individuare testo ed elementi di pagina in Delphi — quindi passi quel rettangolo a RedactRegion:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', '') = 1 then
    begin
      // Elimina tutto ciò che è disegnato all'interno di una casella 220 x 14 pt sulla pagina 1.
      // Le coordinate sono in alto a sinistra, nell'origine e nelle unità correnti.
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);   // Options=0 stampa un indicatore nero
      PDF.SaveToFile('contract-redacted.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Dove si nascondono ancora le informazioni redatte in un PDF?

Una redazione che si ferma al livello visibile lascia una traccia, perché un PDF memorizza informazioni in punti che un lettore non esamina mai. losLab PDF Library raggruppa le perdite in canali che vale la pena nominare. Il Document Information Dictionary e lo stream di metadati XMP contengono regolarmente stringhe relative ad Autore, Produttore o Parole chiave che nominano la persona o il caso esatto rimosso dal corpo del testo. Il JavaScript a livello di documento e l'OpenAction del catalogo possono essere eseguiti all'apertura e raggiungere dati che pensavi fossero andati perduti. Ogni annotazione contiene un'azione /A opzionale, e l'annotazione stessa — una nota adesiva, un campo modulo obsoleto, un collegamento — può trovarsi direttamente sopra il punto redatto con la propria copia del testo. Niente di tutto ciò viene toccato dalla crittografia; un file crittografato con un pacchetto XMP in chiaro rivela comunque il suo titolo a qualsiasi indicizzatore, una lacuna che merita di per sé di controllare crittografia e autorizzazioni. Una sanificazione difendibile deve visitare ciascuno di questi nascondigli, non solo la pagina che stavi guardando

Pulizia del documento con SanitizeDocument

losLab PDF Library risponde a questa diffusione di perdite con SanitizeDocument(Flags), una chiamata di orchestrazione che esegue una serie di rimozioni indipendenti selezionate da una maschera di bit e restituisce il numero di rimozioni applicate. I flag sono combinable. SANITIZE_JAVASCRIPT rimuove ogni pacchetto JavaScript a livello di documento e qualsiasi script OpenAction. SANITIZE_ACTIONS rimuove il dizionario delle azioni /A da ogni annotazione, tramite il nuovo TPDFAnnots.RemoveAnnotAction, e cancella l'OpenAction del catalogo. SANITIZE_METADATA azzera Autore, Oggetto, Parole chiave, Creatore e Produttore sia nel Document Information Dictionary (ISO 32000-1 §14.3.3) che nello stream di metadati XMP (§14.3.2). SANITIZE_ANNOTATIONS elimina ogni annotazione in tutto il documento, e SANITIZE_OPEN_ACTION rimuove la voce /OpenAction del catalogo. SANITIZE_ALL è l'unione bit a bit di tutti e cinque

Le convenzioni di indice all'interno di queste rimozioni sono esattamente il tipo di cosa che interrompe silenziosamente un ciclo scritto a mano, che è in gran parte il motivo per cui SanitizeDocument esiste come singola chiamata. GlobalJavaScriptPackageName è a base 0, e RemoveGlobalJavaScript riduce l'elenco man mano che procede, quindi la rimozione del JavaScript legge sempre l'indice di pacchetto 0 ed elimina fino a quando il conteggio raggiunge lo zero. DeleteAnnotation, al contrario, è a base 1. AnnotationCount fa riferimento alla pagina attualmente selezionata, quindi la rimozione delle annotazioni seleziona ciascuna pagina a sua volta prima del conteggio. losLab PDF Library gestisce correttamente questi limiti al suo interno, in modo da poter passare un set di flag e ottenere un conteggio finale invece di riprodurre cinque diverse regole di iterazione

var
  Applied: Integer;
begin
  // Rimuove metadati, script e azioni ma mantiene le annotazioni:
  Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
    SANITIZE_ACTIONS or SANITIZE_OPEN_ACTION);

  // Oppure cancella ogni canale nascosto, annotazioni incluse:
  Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;

Il flusso di lavoro completo di redazione e sanificazione

Mettendo insieme le due cose, la procedura completa è breve. losLab PDF Library rimuove il contenuto visibile con RedactRegion, pagina per pagina, quindi cancella ogni canale nascosto con SanitizeDocument(SANITIZE_ALL) prima che il file venga scritto. Poiché la redazione viene eseguita quasi sempre su documenti provenienti dall'esterno e fuori dal tuo controllo, vale la pena caricarli attraverso un percorso protetto — lo stesso approccio difensivo descritto in analizzare in sicurezza i PDF non attendibili — in modo che un input malformato fallisca in modo pulito anziché a metà della redazione

var
  PDF: TPDFlib;
  Applied: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('incoming.pdf', '') = 1 then
    begin
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);       // elimina la riga sensibile
      Applied := PDF.SanitizeDocument(SANITIZE_ALL);   // cancella metadati, JS, azioni, annotazioni
      PDF.SaveToFile('published.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Dove si ferma la redazione del content-stream

RedactRegion opera sul content stream, che è esattamente dove vive il testo nato digitale ed esattamente dove non si trova il testo scansionato. Quando una pagina è un'immagine scansionata, le parole sono pixel all'interno di un'immagine XObject, non operatori di testo, e losLab PDF Library non può ritagliare un nome dal centro di una trama raster nello stesso modo in cui elimina un glifo. La sua gestione delle immagini è deliberatamente conservativa: qualsiasi immagine il cui posizionamento interseca il rettangolo viene rimossa interamente, quindi una casella di redazione su una pagina scansionata trascina con sé l'intera scansione piuttosto che una parte di essa. Redigere una regione all'interno di un'immagine significa rasterizzare, dipingere e reinserire a livello di pixel, operazione diversa dall'eliminazione del content-stream. La stessa cautela si applica a un livello di testo OCR sopra una pagina scansionata: RedactRegion rimuove gli operatori di testo che riesce a vedere, ma l'immagine sottostante rimane intatta a meno che anche la sua immagine XObject non intersechi la casella

Due dettagli mantengono i risultati precisi. Il test di intersezione approssima la larghezza di ogni riga di testo a partire dal numero di caratteri e dalla dimensione del font anziché misurare le metriche esatte dei glifi, quindi una parola molto breve impostata in un font proporzionale può sfuggire a un rettangolo tracciato in modo troppo stretto — concedi alla casella un piccolo margine. Inoltre, l'eliminazione avviene a livello di istruzione (instruction-granular): RedactRegion rimuove interi operatori di visualizzazione del testo, quindi un rettangolo che ritaglia solo parte di una riga può rimuovere caratteri adiacenti che condividevano lo stesso operatore con essa. Nessuno dei due limiti è un motivo per diffidare del meccanismo, ma solo un motivo per delimitare il rettangolo con cura e controllare il risultato

La disciplina che conclude ogni flusso di lavoro di redazione è la verifica, non la fiducia. Redigi la regione, sanifica il documento, salvalo in un nuovo file, riaprilo e prova a recuperare sia il testo rimosso che i metadati cancellati; quando entrambi risultano vuoti, la redazione è andata a buon fine. Le API RedactRegion e SanitizeDocument mostrate qui sono fornite con la losLab PDF Library per Delphi e C++Builder, insieme al riferimento completo del modello di contenuto e sanificazione