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
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:
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
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