HotPDF percorre ogni operatore nel content stream di una pagina e consegna ciascuno di essi al vostro codice tramite VisitLoadedPageContentOperators. Per ogni operatore scegliete un'azione: mantenerlo, eliminarlo, sostituirlo con i vostri byte, fermare il percorso, oppure rifiutare l'intera modifica. Quando ogni operatore viene mantenuto, lo stream riscritto è identico byte per byte all'originale, ed è questa proprietà a rendere sicuro l'utilizzo del meccanismo su documenti che non avete creato voi
La modifica dei content stream è dove gli strumenti PDF di solito sbagliano. Uno stream è una sequenza di operatori su una macchina a stack, non un albero di documento, e l'approccio ingenuo di eseguire un'espressione regolare sui byte decodificati si rompe nel momento in cui un letterale stringa contiene qualcosa che assomiglia a un operatore, o un'immagine inline contiene dati binari arbitrari. Un visitor che ha già tokenizzato lo stream non ha questo problema
Cosa si può fare realmente con l'accesso a livello di operatore?
Tre famiglie di compiti. Auditing: contare quante volte una pagina imposta un colore al di fuori di un set conosciuto, o trovare testo disegnato con un font non incorporato. Rimozione: eliminare una filigrana disegnata come contenuto marcato, o rimuovere l'invocazione di un'immagine specifica. E normalizzazione: sostituire una forma di operatore con un equivalente che un consumatore a valle gestisce meglio
Ciascuno di questi compiti condivide un requisito: dovete poter lasciare intatto l'altro 99 percento dello stream. L'albero sintattico di HotPDF conserva una copia del sorgente più span compatti, e le modifiche sono overlay ordinati e non sovrapposti. Scrivere il risultato copia direttamente gli intervalli non toccati ed emette byte sostitutivi solo dove avete cambiato qualcosa, così un round trip senza modifiche non può introdurre deriva
Il contratto del visitor
Implementate l'interfaccia o assegnate l'evento, poi chiamate il metodo di visita con ApplyChanges impostato a false per un audit o a true per una riscrittura. I dati dell'evento portano la fase, il nome dell'operatore, il conteggio dei suoi operandi, il suo span di byte, lo stato di annidamento corrente e una view presa in prestito per leggere gli operandi:
uses
HPDFDoc, HPDFContentStream;
procedure TAuditor.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
var
Bytes: TBytes;
begin
if EventData.Phase <> covpOperator then
Exit;
// Conta gli operatori di visualizzazione testo disegnati dentro contenuto marcato
if (EventData.OperatorName = 'Tj') or
(EventData.OperatorName = 'TJ') then
begin
Inc(FTextOperators);
if EventData.State.MarkedContentDepth > 0 then
Inc(FTextInsideMarkedContent);
end;
// Leggi un operando solo quando serve
if EventData.OperatorName = 'Do' then
if EventData.View.GetOperandBytes(0, Bytes) then
NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));
EventData.Action := coaKeep;
end;
procedure TAuditor.Run(const FileName: string);
var
Pdf: THotPDF;
Stats: THPDFContentOperatorVisitStatistics;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) <= 0 then
Exit;
Pdf.OnContentOperator := HandleOperator;
for I := 0 to Pdf.LoadedPageCount - 1 do
Pdf.VisitLoadedPageContentOperators(I, False, Stats); // solo audit
Writeln(Format('%d operators visited, max q/Q depth %d',
[Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
finally
Pdf.Free;
end;
end;
Perché la view è presa in prestito e non modificabile?
Il callback non riceve mai un albero sintattico modificabile. Riceve una view che può recuperare i byte dell'operazione corrente o uno specifico operando su richiesta, e nient'altro. Ogni modifica passa invece attraverso i campi di azione e sostituzione
Questa indirezione non è cerimoniale. La mutazione diretta dell'albero permetterebbe a un visitor di aggirare il budget di sostituzione e il controllo delle modifiche sovrapposte, e le modifiche sovrapposte in un modello di overlay a span di byte producono un output silenziosamente sbagliato invece che rumorosamente rotto. Instradare le modifiche attraverso il campo di azione significa che ogni modifica viene validata allo stesso modo indipendentemente da cosa intendesse il visitor
Leggere su richiesta mantiene anche economico il caso comune. Un audit che si limita a contare i nomi degli operatori non materializza mai i byte degli operandi, il che conta su un content stream con centinaia di migliaia di operazioni
Il campo di stato descrive sempre l'input
Ogni evento porta la profondità dello stato grafico, la profondità dell'oggetto testo, la profondità del contenuto marcato e la profondità della sezione di compatibilità così come si presentavano nell'input originale immediatamente prima dell'esecuzione di quell'operatore. Questo è vero anche se il vostro visitor ha eliminato o sostituito operatori precedenti
Ancorare lo stato all'input originale anziché all'output modificato è una scelta deliberata con un ritorno pratico: eseguire lo stesso audit due volte sullo stesso documento produce risultati identici, e un audit che gira insieme a delle modifiche non sposta il proprio sistema di riferimento. Se avete bisogno di conoscere la profondità nell'output, sapete cosa avete eliminato, quindi potete calcolarla; il contrario, ricostruire posizioni di input stabili a partire da un output in movimento, non è possibile
Eliminare e sostituire, con le regole che vi proteggono
Una sostituzione è composta da byte grezzi di content stream, e siete voi i responsabili della loro sintassi. Le protezioni attorno a essi sono il budget sui byte totali di sostituzione, il requisito di non sovrapposizione, e la validazione opzionale dell'output che ri-analizza il risultato prima che venga accettato:
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
begin
EventData.Action := coaKeep;
if EventData.Phase <> covpOperator then
Exit;
// Elimina gli operatori che disegnano il form della filigrana
if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
EventData.Action := coaDelete
// Normalizza un colore vagante al grigio interno
else if EventData.OperatorName = 'rg' then
begin
EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
EventData.Action := coaReplace;
end
// Rifiuta l'intera modifica se incontriamo qualcosa che non comprendiamo
else if EventData.OperatorName = 'BI' then
EventData.Action := coaReject;
end;
var
Options: THPDFContentOperatorVisitOptions;
begin
Options := THPDFContentOperatorVisitOptions.Default;
Options.ValidateOutput := True; // ri-analizza prima di accettare
Options.MaxReplacementBytes := 64 * 1024;
Pdf.OnContentOperator := HandleOperator;
if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
if Stats.Modified then
Pdf.SaveLoadedDocument('cleaned.pdf');
end;
Il set di azioni include due uscite per un motivo preciso. coaStop termina il percorso e mantiene ciò che è stato deciso finora, il che è corretto quando avete trovato ciò che cercavate. coaReject abbandona l'intera visita e lascia la pagina intatta, il che è corretto quando incontrate un costrutto che le vostre regole di riscrittura non coprono. Le immagini inline sono l'esempio classico: il loro payload binario non è qualcosa attorno a cui un visitor di normalizzazione dei colori dovrebbe operare modifiche
Leggere le statistiche, e dove si ferma il percorso
Il campo di stato distingue il completamento dall'interruzione, dal rifiuto, dall'input non valido, da un limite superato, dall'output non valido e da un errore del visitor. Sono problemi diversi: output non valido significa che i vostri byte di sostituzione non hanno superato la ri-analisi, mentre input non valido significa che lo stream sorgente era già malformato e non è stata tentata alcuna modifica
I contatori rendono un audit auto-verificabile. VisitedOperatorCount confrontato con KeptOperatorCount, DeletedOperatorCount e ReplacedOperatorCount deve tornare, e le cifre di profondità massima vi dicono se la pagina si annida abbastanza in profondità da meritare uno sguardo più attento. SourceBytes confrontato con OutputBytes fornisce in un solo numero l'effetto dimensionale della vostra modifica
Per i compiti comuni esistono percorsi di livello più alto che non richiedono affatto di scrivere un visitor. La rimozione permanente di contenuti, con la pulizia delle risorse di cui la vera redazione ha bisogno, è trattata in redazione e assemblaggio di pagine su documenti caricati. La sostituzione di testo visibile è trattata in cerca e sostituisci in un PDF esistente. Ricorrete al visitor di operatori quando la vostra regola è strutturale piuttosto che testuale, o quando avete bisogno di un audit a cui nessun report esistente risponde
La visita, l'estrazione e la riscrittura dei content stream operano tutte sullo stesso modello di documento caricato in Delphi e C++Builder; l'elenco completo delle funzionalità è disponibile sulla pagina del componente PDF HotPDF Delphi