Artykuł techniczny

Bezpieczne przepisywanie strumieni treści PDF w Delphi

HotPDF przechodzi przez każdy operator w strumieniu treści strony i przekazuje każdy z nich twojemu kodowi za pomocą VisitLoadedPageContentOperators. Dla każdego operatora wybierasz akcję: zachować go, usunąć, zastąpić własnymi bajtami, zatrzymać przejście albo odrzucić całą edycję. Gdy każdy operator zostanie zachowany, przepisany strumień jest bajt w bajt identyczny z oryginałem, co jest właściwością czyniącą ten mechanizm bezpiecznym do uruchamiania na dokumentach, których nie stworzyłeś

Edycja strumienia treści to miejsce, w którym narzędzia PDF zwykle zawodzą. Strumień to sekwencja operatorów działających na maszynie stosowej, a nie drzewo dokumentu, a naiwne podejście polegające na uruchomieniu wyrażenia regularnego na zdekodowanych bajtach psuje się w chwili, gdy literał łańcuchowy zawiera coś, co wygląda jak operator, albo obraz osadzony w tekście zawiera dowolne dane binarne. Odwiedzający (visitor), który już zetokenizował strumień, nie ma tego problemu

Operatory strumienia treści PDF, takie jak BT, Tf i TJ, trafiające do danych zdarzenia odwiedzającego z fazą, nazwą operatora, liczbą operandów, zakresem bajtów i stanem zagnieżdżenia, a następnie zwracające jedną z pięciu akcji: zachowaj, zastąp, usuń, zatrzymaj lub odrzuć
Każdy odwiedzony operator daje dokładnie jedną decyzję, a przebieg zachowujący wszystko zapisuje z powrotem nietknięte zakresy, dając wyjście identyczne bajtowo

Co właściwie można zrobić z dostępem na poziomie operatorów?

Trzy rodziny zadań. Audyt: policz, ile razy strona ustawia kolor spoza znanego zbioru, albo znajdź tekst narysowany czcionką, która nie jest osadzona. Usuwanie: usuń znak wodny narysowany jako treść znakowana albo wytnij konkretne wywołanie obrazu. I normalizacja: zastąp jedną formę operatora równoważną formą, którą lepiej obsługuje odbiorca dalej w potoku

Każde z tych zadań ma jeden wspólny wymóg: musisz umieć zostawić pozostałe 99 procent strumienia nietknięte. Drzewo składniowe HotPDF przechowuje jedną kopię źródła plus zwarte zakresy, a edycje to posortowane, niezachodzące na siebie nakładki. Zapisywanie wyniku kopiuje nietknięte zakresy bezpośrednio i emituje bajty zastępcze tylko tam, gdzie coś zmieniłeś, dzięki czemu przebieg bez żadnych zmian nie może wprowadzić dryfu

Kontrakt odwiedzającego

Zaimplementuj interfejs albo przypisz zdarzenie, a następnie wywołaj metodę odwiedzającą z ApplyChanges ustawionym na false dla audytu albo na true dla przepisania. Dane zdarzenia niosą fazę, nazwę operatora, liczbę jego operandów, jego zakres bajtów, bieżący stan zagnieżdżenia oraz pożyczony widok do odczytu operandów:

uses
  HPDFDoc, HPDFContentStream;

procedure TAuditor.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
var
  Bytes: TBytes;
begin
  if EventData.Phase <> covpOperator then
    Exit;

  // Zlicz operatory wyświetlające tekst narysowane wewnątrz treści znakowanej
  if (EventData.OperatorName = 'Tj') or
     (EventData.OperatorName = 'TJ') then
  begin
    Inc(FTextOperators);
    if EventData.State.MarkedContentDepth > 0 then
      Inc(FTextInsideMarkedContent);
  end;

  // Odczytaj operand tylko wtedy, gdy jest potrzebny
  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);   // tylko audyt
    Writeln(Format('%d operators visited, max q/Q depth %d',
      [Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
  finally
    Pdf.Free;
  end;
end;

Dlaczego widok jest pożyczony, a nie mutowalny?

Wywołanie zwrotne nigdy nie otrzymuje modyfikowalnego drzewa składniowego. Otrzymuje widok, który na żądanie może pobrać bajty bieżącej operacji albo konkretny operand, i to wszystko. Każda zmiana przechodzi zamiast tego przez pola akcji i zastąpienia

Ta pośredniość to nie formalność bez znaczenia. Bezpośrednia mutacja drzewa pozwoliłaby odwiedzającemu ominąć budżet zastąpień i sprawdzanie nakładających się edycji, a nakładające się edycje w modelu nakładek opartym na zakresach bajtów dają wynik, który jest po cichu błędny, a nie głośno zepsuty. Kierowanie zmian przez pole akcji oznacza, że każda edycja jest walidowana w ten sam sposób, niezależnie od tego, co zamierzał odwiedzający

Odczyt na żądanie sprawia też, że typowy przypadek pozostaje tani. Audyt, który tylko zlicza nazwy operatorów, w ogóle nigdy nie materializuje bajtów operandów, co ma znaczenie przy strumieniu treści liczącym setki tysięcy operacji

Pole stanu zawsze opisuje wejście

Każde zdarzenie niesie głębokość stanu graficznego, głębokość obiektu tekstowego, głębokość treści znakowanej i głębokość sekcji zgodności takie, jakie były w oryginalnym wejściu bezpośrednio przed wykonaniem danego operatora. Jest to prawdą nawet wtedy, gdy twój odwiedzający usunął lub zastąpił wcześniejsze operatory

Zakotwiczenie stanu w oryginalnym wejściu, a nie w edytowanym wyjściu, to celowy wybór z praktyczną korzyścią: uruchomienie tego samego audytu dwukrotnie na tym samym dokumencie daje identyczne wyniki, a audyt uruchamiany równolegle z edycjami nie przesuwa własnego układu odniesienia. Jeśli potrzebujesz znać głębokość w wyjściu, wiesz, co usunąłeś, więc możesz to obliczyć; odwrotność, czyli odtworzenie stabilnych pozycji wejściowych z ruchomego wyjścia, nie jest możliwa

Usuwanie i zastępowanie, wraz z regułami, które cię chronią

Zastąpienie to surowe bajty strumienia treści, a ty odpowiadasz za ich składnię. Zabezpieczenia wokół nich to budżet na łączną liczbę bajtów zastąpień, wymóg braku nakładania się oraz opcjonalna walidacja wyjścia, która ponownie parsuje wynik, zanim zostanie zaakceptowany:

Zabezpieczenia przepisywania w Delphi: odwiedzający deklarujący zamiar Action i Replacement kierowany przez budżet zastąpień, sprawdzanie braku nakładania się zakresów bajtów oraz zabezpieczenia walidacji wyjścia do zwalidowanej zapisanej strony
Każda zmiana przepływa przez pole akcji do zabezpieczeń budżetu, nakładania się i ponownego parsowania, więc nieudana walidacja przerywa działanie zamiast zapisywać uszkodzone operatory
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
begin
  EventData.Action := coaKeep;
  if EventData.Phase <> covpOperator then
    Exit;

  // Odrzuć operatory rysujące formularz znaku wodnego
  if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
    EventData.Action := coaDelete
  // Znormalizuj przypadkowy kolor do firmowej szarości
  else if EventData.OperatorName = 'rg' then
  begin
    EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
    EventData.Action := coaReplace;
  end
  // Odrzuć całą edycję, jeśli napotkamy coś, czego nie rozumiemy
  else if EventData.OperatorName = 'BI' then
    EventData.Action := coaReject;
end;

var
  Options: THPDFContentOperatorVisitOptions;
begin
  Options := THPDFContentOperatorVisitOptions.Default;
  Options.ValidateOutput := True;        // ponownie sparsuj przed akceptacją
  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;

Zbiór akcji nie bez powodu zawiera dwa wyjścia. coaStop kończy przejście i zachowuje to, co zostało dotychczas zdecydowane, co jest właściwe, gdy znalazłeś już to, czego szukałeś. coaReject porzuca całe odwiedziny i pozostawia stronę nietkniętą, co jest właściwe, gdy napotkasz konstrukcję nieobjętą twoimi regułami przepisywania. Obrazy osadzone w tekście (inline images) są klasycznym przykładem: ich binarny ładunek nie jest czymś, wokół czego powinien majstrować odwiedzający normalizujący kolory

Odczytywanie statystyk i miejsce zatrzymania przejścia

Pole statusu odróżnia ukończenie od zatrzymania, odrzucenia, nieprawidłowego wejścia, przekroczonego limitu, nieprawidłowego wyjścia i błędu odwiedzającego. To różne problemy: nieprawidłowe wyjście oznacza, że twoje bajty zastąpienia nie dały się ponownie sparsować, natomiast nieprawidłowe wejście oznacza, że strumień źródłowy był już zniekształcony i nie podjęto żadnej edycji

Liczniki sprawiają, że audyt sam się kontroluje. VisitedOperatorCount zestawione z KeptOperatorCount, DeletedOperatorCount i ReplacedOperatorCount musi się zgadzać, a wartości maksymalnej głębokości mówią, czy strona zagnieżdża się na tyle głęboko, by warto było przyjrzeć się jej bliżej. SourceBytes zestawione z OutputBytes daje w jednej liczbie efekt rozmiarowy twojej edycji

Uzgadnianie statystyk audytu, w którym liczby operatorów zachowanych, usuniętych i zastąpionych sumują się do liczby odwiedzonych, obok siedmiu zakończeń statusu od ukończenia po błąd odwiedzającego, nad notatką, że głębokości stanu opisują oryginalne wejście, dzięki czemu audyty powtarzają się identycznie
Liczniki, które muszą się zgadzać, wraz ze statusem nazywającym każde możliwe zakończenie, sprawiają, że audyt sam się kontroluje na dokumentach, których nie stworzyłeś

Dla typowych zadań istnieją ścieżki wyższego poziomu, które w ogóle nie wymagają pisania odwiedzającego. Trwałe usuwanie treści, wraz z porządkowaniem zasobów, którego wymaga prawdziwa redakcja, opisano w redakcji i łączeniu stron w wczytanych dokumentach. Zastępowanie widocznego tekstu opisano w wyszukiwaniu i zastępowaniu tekstu w istniejącym PDF. Sięgnij po odwiedzającego operatory, gdy twoja reguła jest strukturalna, a nie tekstowa, albo gdy potrzebujesz audytu, na który nie odpowiada żaden istniejący raport

Odwiedzanie, ekstrakcja i przepisywanie strumieni treści działają na tym samym modelu wczytanego dokumentu w Delphi i C++Builder; pełną listę funkcji znajdziesz na stronie komponentu HotPDF PDF dla Delphi