Qualcuno disegna un rettangolo nero sopra un nome, non appiattisce nulla, spedisce il file, e il revisore seleziona il rettangolo e incolla il nome in una email. PDFiumPas risponde con la redazione a livello di operatore: SaveAsRedacted elimina solo gli scalari Unicode i cui box di carattere toccano un rettangolo di redazione, ricostruisce i sopravvissuti da font, dimensione, matrice, modalità di rendering e colore originali, e ritaglia i percorsi e le immagini allineati agli assi invece di eliminarli interi
Perché un rettangolo disegnato non è una redazione
Una operazione di disegno aggiunta sopra uno stream di contenuto non nasconde nulla, perché gli operatori di visualizzazione del testo sotto di essa sono ancora nello stream e mappano ancora a code point. ISO 32000-1 §9.4 definisce un text object come una sequenza di operatori di posizionamento e visualizzazione dentro BT ed ET; un rettangolo pieno disegnato dopo è semplicemente un altro operatore nello stesso stream. Lestrazione cammina sugli operatori, non sui pixel, quindi la stringa coperta torna intatta. Una redazione vera deve rimuovere l'operando, non oscurare l'output
L'implementazione sicura ovvia è brutale: trova ogni page object il cui bounding box interseca un rettangolo di redazione ed elimina loggetto intero. È quello che facevano le versioni precedenti di PDFiumPas, ed è corretto ma costoso. Un solo Tj può portare un'intera riga di tabella, quindi oscurare un numero di conto si portava via data, descrizione e importo. Un riempimento rettangolare che per caso era una banda full-width della tabella spariva dall'intera pagina. Il logo di una fattura spariva perché la redazione ne tagliava un angolo. La versione 3.101.0 sposta la decisione un livello sotto, dal page object all'operando
Cosa elimina davvero la redazione a livello di operatore?
PDFiumPas elimina scalari Unicode, non text object. Durante SaveAsRedacted il componente costruisce una mappa carattere-page object dalla pagina di testo caricata, poi per ogni carattere posseduto dalloggetto in esame legge il box del carattere e interseca quel box con ogni rettangolo di redazione. I caratteri che toccano un rettangolo sono marcati per leliminazione; gli altri sono marcati come sopravvissuti. Se nulla interseca, loggetto viene lasciato completamente in pace. Se ogni carattere interseca, loggetto viene eliminato intero, esattamente come prima. Solo il caso misto innesca uno split
Ogni sopravvissuto è poi riemesso come suo text object costruito dal font handle originale, dalla dimensione originale del font, dalla matrice di testo per carattere, dalla modalità di rendering del testo originale, e dallo stato di fill e stroke delloggetto padre inclusi spessore del tratto, line join, line cap e dash array. Riusare il font handle invece di risolverne uno nuovo è ciò che tiene i glifi metricamente identici, e riusare la matrice per carattere è ciò che tiene kerning e spaziatura delle parole al loro posto senza rilanciare il layout. Il costo è il numero di oggetti: un carattere trattenuto diventa un text object, ed è per questo che TPdfRedactionOptions.MaxSplitObjects esiste come tetto rigido sui frammenti generati
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // il file porta già le annotazioni /Redact
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // split a livello di operatore (il predefinito)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // tetto sui frammenti generati
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, non spedire
finally
Pdf.Free;
end;
end;
I rettangoli vengono ritagliati, la geometria ruotata no
I percorsi vengono divisi solo quando PDFiumPas può dimostrare che il percorso è un rettangolo allineato agli assi. La dimostrazione è volutamente stretta: la matrice delloggetto deve avere entrambi i termini di shear sotto 0.0001, il percorso deve consistere di quattro a sei segmenti che iniziano con MOVETO e continuano solo con LINETO, e i punti trasformati devono cadere su tutti e quattro gli angoli dei bounds delloggetto entro una tolleranza di 0.01. Un percorso che supera quel controllo viene ridotto per sottrazione rettangolare successiva, ogni rettangolo di redazione intaglia linsieme dei sopravvissuti in strisce a sinistra, a destra, sotto e sopra, e ogni striscia risultante viene ricreata con la modalità di fill originale, il flag di stroke e lo stato di paint. Curve, triangoli, forme con clip e qualsiasi cosa ruotata falliscono il controllo e loggetto intero viene rimosso
Le immagini seguono ISO 32000-1 §8.9, dove i campioni dellimmagine occupano il quadrato unitario mappato attraverso la matrice di trasformazione corrente. PDFiumPas inverte quella mappatura per riportare ogni frammento sopravvissuto in spazio pagina a coordinate immagine normalizzate, le limita allintervallo unitario, poi converte a indici di pixel arrotondando verso linterno: i bordi sinistro e superiore passano per Ceil, il destro e linferiore per Floor. Quella direzione conta. Arrotondare verso lesterno lascerebbe sopravvivere una colonna parziale di pixel sorgente dal lato redatto al bordo del frammento. I bounds interi dei pixel vengono poi riconvertiti in coordinate normalizzate e usati per derivare la matrice del frammento, così la bitmap ritagliata atterra esattamente sul confine di pixel in cui è stata tagliata. Il ritaglio stesso è una copia di righe consapevole dello stride sui formati Gray, BGR, BGRx e BGRA. Come per i percorsi, unimmagine ruotata o inclinata, o una la cui matrice ha un termine di scala degenere, viene rimossa per intero
// Dopo una chiamata SaveAsRedacted riuscita
Writeln(Format('applied %d redaction(s) on %d page(s)',
[Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
[Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
[Report.SplitTextObjectCount, Report.SplitPathObjectCount,
Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
[Report.ResourcePruneReport.RemovedNameCount,
Report.ResourcePruneReport.RemovedObjectCount]));
if Report.PreservedFragmentCount = 0 then
// nulla poteva essere diviso: ogni oggetto intersecante è stato buttato intero
LogWholeObjectFallback(SourcePdf);
Perché PDFiumPas fa fail closed sui caratteri non mappati?
Perché un glifo senza scalare Unicode riproducibile non può essere ricostruito onestamente. Ricostruire un sopravvissuto significa chiamare la API di impostazione del testo con una stringa, e questo richiede un code point stabile per ogni carattere trattenuto. I font subset simbolici con dati ToUnicode rotti o assenti possono dare una mappatura vuota, e ricodificare a tentoni produrrebbe output che sembra corretto sullo schermo mentre porta sotto un carattere diverso. PDFiumPas rifiuta: il controllo dei caratteri trattenuti solleva, leccezione viene catturata dentro SaveAsRedacted, TPdfRedactionReport.Succeeded torna False col messaggio in ErrorMessage, e la funzione restituisce False. La stessa regola vale per il budget di split, che solleva invece di troncare in silenzio linsieme dei frammenti. Quando un documento ha font di cui non ti fidi e vuoi il vecchio comportamento deterministico, imposta Options.PreservePartialObjects := False e ogni oggetto intersecante va via intero
Pulizia delle risorse attraverso scope condivisi
Dividere gli oggetti lascia dietro orfani, e pulirli non è semplice come fare il diff del dizionario /Resources a livello pagina. ISO 32000-1 §7.8.3 permette allo stesso dizionario risorse di essere referenziato da più pagine, da Form XObjects, da pattern, e da appearance stream di annotazioni insieme. Eliminare un nome di font perché una pagina ha smesso di usarlo romperebbe unaltra pagina che lo usa ancora. PruneUnusedPdfResources quindi lavora per scope: risolve /Contents sia che sia un array diretto, un riferimento indiretto a un array, o un singolo stream, poi raccoglie luso delle risorse dagli operatori che effettivamente nominano risorse — Tf per i font, Do per gli XObjects, gs per lo stato grafico, CS, cs, SCN e scn per i color space e i pattern, sh per gli shading, BDC e DP per le proprietà marked-content, più la voce /CS delle immagini inline. Quando un dizionario è condiviso da più scope, gli insiemi dei nomi usati vengono uniti per categoria prima di rimuovere qualcosa
Vengono buttati solo i nomi confermati non referenziati in ogni scope che punta al dizionario. Uno scope che non può essere analizzato con sicurezza viene lasciato intatto, che è la direzione conservativa: un file non pulito è solo più grande, uno pulito male è corrotto. I dizionari sopravvissuti vengono riscritti come aggiornamento incrementale sparso con i numeri di generazione esatti, e una riscrittura di raggiungibilità poi spazza gli oggetti diventati irraggiungibili una volta spariti i nomi. TPdfResourcePruneReport riporta ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, i conteggi di byte, e un flag Succeeded. SaveAsRedacted esegue questo passo automaticamente sulloutput sanificato, quindi il percorso di redazione lo include già, ma la funzione è esportata a livello stream per le pipeline che la vogliono da sola
uses
FPdfCompress;
procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
Source, Dest: TFileStream;
Report: TPdfResourcePruneReport;
begin
Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create(TargetPdf, fmCreate);
try
// AllowSignedDocument resta False: una riscrittura incrementale
// invaliderebbe i byte range che una firma copre
PruneUnusedPdfResources(Source, Dest, Report);
if not Report.Succeeded then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
[Report.RemovedNameCount, Report.UpdatedScopeCount,
Report.SourceByteCount, Report.OutputByteCount]));
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Collegarlo in una pipeline di documenti
Il percorso di redazione non muta mai il documento che hai caricato. SaveAsRedacted cattura uno snapshot isolato, applica lì le annotazioni /Redact, toglie gli allegati, esegue il passaggio di sanificazione che rimuove lazione open, le azioni di catalogo, i name tree, i file associati, lAcroForm e i metadati, pulisce le risorse, e solo allora scrive lo stream di output. Riaprire quelloutput come documento indipendente e riestrarre il testo è il passo di verifica da tenere nella tua suite di test, perché è lunico controllo che risponde alla domanda originale — un lettore può ancora ottenere la stringa. Una conseguenza da pianificare: lo split sostituisce i page object, quindi qualsiasi handle FPDF_PAGEOBJECT che tenevi è morto dopo, la stessa trappola di lifetime descritta nella pagina su handle di page object stantii dopo una transform
Due pezzi vicini completano il flusso di lavoro. Decidere dove vanno i rettangoli di redazione di solito parte dalla geometria estratta, e il modello di blocchi e ordine di lettura nella pagina su blocchi di testo strutturato e ordine di lettura è una fonte migliore di box candidati delle sequenze grezze di caratteri. Servire il risultato a un revisore appartiene alle regole di hardening nella pagina su costruire un anteprima PDF sicura, dove il riempimento dei form e JavaScript restano spenti per impostazione predefinita. Insieme coprono il ciclo che la maggior parte dei flussi di conformità richiede: localizzare, redigere a livello di operatore, verificare riaprendo, mostrare in anteprima in sicurezza. La superficie API completa, il download di prova e le condizioni di licenza del componente vivono sulla pagina del prodotto PDFium Delphi Component