Technický článek

Bezpečné přepisování obsahových streamů PDF v Delphi

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á

Operátory obsahového streamu PDF jako BT, Tf a TJ proudící do dat události visitoru s fází, názvem operátoru, počtem operandů, bajtovým rozsahem a stavem zanoření, které pak vrátí jednu z pěti akcí: ponechat, nahradit, smazat, zastavit nebo odmítnout
Každý navštívený operátor přinese přesně jedno rozhodnutí a průchod, který vše ponechá, zapíše zpět netknuté rozsahy pro bajtově identický výstup

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:

Ochranné mechanismy přepisu v Delphi: visitor deklarující záměr Action a Replacement, směrovaný přes rozpočet na náhrady, kontrolu nepřekrývajícího se bajtového rozsahu a validaci výstupu do ověřené uložené stránky
Každá změna prochází přes pole akce do rozpočtu, kontroly překryvu a kontroly opětovného parsování, takže neúspěšná validace úpravu přeruší, místo aby zapsala rozbité operátory
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

Sesouhlasení statistik auditu, kde počty ponechaných, smazaných a nahrazených operátorů dávají dohromady počet navštívených, vedle sedmi možných konců stavu od dokončeno po chybu visitoru, s poznámkou, že hloubky stavu popisují původní vstup, takže se audity opakují identicky
Čítače, které musí sedět, spolu se stavem pojmenovávajícím každý možný konec dělají audit sebekontrolní i na dokumentech, které jste nevytvořili

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