HotPDF prochází každý operátor v obsahovém streamu stránky a předává každý z nich vašemu kódu přes VisitLoadedPageContentOperators. Pro každý operátor zvolíte akci: ponechat ho, smazat ho, nahradit ho vlastními bajty, zastavit průchod, nebo odmítnout celou úpravu. Když se ponechají všechny operátory, přepsaný stream je bajt po bajtu identický s originálem, což je vlastnost, díky které je mechanismus bezpečné pouštět na dokumenty, které jste nevytvořili
Úprava obsahových streamů je místo, kde nástroje pro PDF obvykle chybují. Stream je posloupnost operátorů nad zásobníkovým strojem, ne strom dokumentu, a naivní přístup spouštění regulárního výrazu nad dekódovanými bajty se rozbije ve chvíli, kdy řetězcový literál obsahuje něco, co vypadá jako operátor, nebo když inline obrázek obsahuje libovolná binární data. Visitor, který stream už tokenizoval, tento problém nemá
Co skutečně můžete dělat s přístupem na úrovni operátorů?
Tři rodiny úloh. Audit: spočítat, kolikrát stránka nastaví barvu mimo známou sadu, nebo najít text nakreslený fontem, který není vložený. Odstranění: smazat vodoznak nakreslený jako značkovaný obsah, nebo odstranit konkrétní vyvolání obrázku. A normalizace: nahradit jednu formu operátoru ekvivalentní formou, kterou lépe zpracuje navazující spotřebitel
Každá z nich sdílí jeden požadavek: musíte umět ponechat zbývajících 99 procent streamu netknutých. Strom syntaxe HotPDF ukládá jednu kopii zdroje plus kompaktní rozsahy a úpravy jsou seřazené nepřekrývající se překryvy. Zápis výsledku kopíruje netknuté rozsahy přímo a emituje náhradní bajty jen tam, kde jste něco změnili, takže bezoperační zpáteční průchod nemůže vnést odchylku
Smlouva visitoru
Implementujte rozhraní nebo přiřaďte událost, pak volejte metodu visit s ApplyChanges nastaveným na false pro audit nebo na true pro přepsání. Data události nesou fázi, název operátoru, počet jeho operandů, jeho bajtový rozsah, aktuální stav zanoření a vypůjčený pohled pro čtení operandů:
uses
HPDFDoc, HPDFContentStream;
procedure TAuditor.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
var
Bytes: TBytes;
begin
if EventData.Phase <> covpOperator then
Exit;
// Počítá operátory zobrazující text nakreslené uvnitř značkovaného obsahu
if (EventData.OperatorName = 'Tj') or
(EventData.OperatorName = 'TJ') then
begin
Inc(FTextOperators);
if EventData.State.MarkedContentDepth > 0 then
Inc(FTextInsideMarkedContent);
end;
// Operand čtěte jen tehdy, když ho skutečně potřebujete
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); // jen audit
Writeln(Format('%d operators visited, max q/Q depth %d',
[Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
finally
Pdf.Free;
end;
end;
Proč je pohled vypůjčený, ne měnitelný?
Callback nikdy neobdrží upravitelný strom syntaxe. Obdrží pohled, který dokáže na vyžádání získat bajty aktuální operace nebo konkrétní operand, a to je vše. Každá změna prochází přes pole akce a náhrady
Tato nepřímost není formalita. Přímá mutace stromu by umožnila visitoru obejít rozpočet na náhrady a kontrolu překrývajících se úprav, a překrývající se úpravy v modelu bajtově-rozsahových překryvů produkují výstup, který je tiše špatný, ne hlasitě rozbitý. Směrování změn přes pole akce znamená, že každá úprava je validována stejným způsobem bez ohledu na to, co visitor zamýšlel
Čtení na vyžádání také udržuje běžný případ levný. Audit, který jen počítá názvy operátorů, nikdy nematerializuje operandové bajty vůbec, což má význam na obsahovém streamu se stovkami tisíc operací
Pole stavu vždy popisuje vstup
Každá událost nese hloubku stavu grafiky, hloubku textového objektu, hloubku značkovaného obsahu a hloubku sekce kompatibility tak, jak stály v původním vstupu bezprostředně před vykonáním daného operátoru. To platí, i když váš visitor dřívější operátory smazal nebo nahradil
Ukotvení stavu k původnímu vstupu, ne k upravenému výstupu, je záměrná volba s praktickým přínosem: spuštění stejného auditu dvakrát nad stejným dokumentem produkuje identické výsledky a audit běžící souběžně s úpravami neposouvá svůj vlastní referenční rámec. Pokud potřebujete znát hloubku ve výstupu, víte, co jste smazali, takže si ji můžete spočítat sami; opak, rekonstrukce stabilních vstupních pozic z pohyblivého výstupu, možný není
Mazání a nahrazování, s pravidly, která vás chrání
Náhrada jsou syrové bajty obsahového streamu a za jejich syntaxi odpovídáte vy. Ochranné mechanismy kolem nich jsou rozpočet na celkový počet náhradních bajtů, požadavek na nepřekrývání a volitelná validace výstupu, která výsledek před přijetím znovu parsuje:
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
begin
EventData.Action := coaKeep;
if EventData.Phase <> covpOperator then
Exit;
// Zahodit operátory, které kreslí formulář vodoznaku
if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
EventData.Action := coaDelete
// Normalizovat zbloudilou barvu na firemní šedou
else if EventData.OperatorName = 'rg' then
begin
EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
EventData.Action := coaReplace;
end
// Odmítnout celou úpravu, pokud narazíme na něco, čemu nerozumíme
else if EventData.OperatorName = 'BI' then
EventData.Action := coaReject;
end;
var
Options: THPDFContentOperatorVisitOptions;
begin
Options := THPDFContentOperatorVisitOptions.Default;
Options.ValidateOutput := True; // před přijetím znovu parsovat
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;
Sada akcí obsahuje dva východy, a to z dobrého důvodu. coaStop ukončí průchod a ponechá to, co bylo dosud rozhodnuto, což je správné, když jste našli, co jste hledali. coaReject opustí celý průchod a ponechá stránku netknutou, což je správné, když narazíte na konstrukci, kterou vaše pravidla přepisu nepokrývají. Klasickým příkladem jsou inline obrázky: jejich binární payload není něco, co by visitor normalizující barvy měl upravovat
Čtení statistik a kde se průchod zastaví
Pole stavu rozlišuje dokončení od zastavení, odmítnutí, neplatného vstupu, překročeného limitu, neplatného výstupu a chyby visitoru. Jsou to různé problémy: neplatný výstup znamená, že vaše náhradní bajty se znovu neparsovaly, zatímco neplatný vstup znamená, že zdrojový stream byl už poškozený a žádná úprava se ani nezkusila
Čítače dělají audit sebekontrolní. VisitedOperatorCount proti KeptOperatorCount, DeletedOperatorCount a ReplacedOperatorCount musí sedět, a čísla maximální hloubky vám řeknou, zda se stránka zanořuje dostatečně hluboko na to, aby stála za bližší pohled. SourceBytes proti OutputBytes dává v jednom čísle efekt vaší úpravy na velikost
Pro běžné úlohy existují cesty na vyšší úrovni, které nevyžadují psaní visitoru vůbec. Trvalé odstranění obsahu, s úklidem zdrojů, který skutečná redakce potřebuje, popisuje článek redakce a slučování stránek na načtených dokumentech. Nahrazení viditelného textu popisuje článek hledání a nahrazování textu v existujícím PDF. Po visitoru operátorů sáhněte, když je vaše pravidlo strukturální, ne textové, nebo když potřebujete audit, na který neodpovídá žádný existující report
Procházení, extrakce a přepisování obsahových streamů pracují v Delphi a C++Builder na stejném modelu načteného dokumentu; úplný seznam funkcí najdete na stránce komponenty HotPDF Delphi PDF component