Artykuł techniczny

Rzeczywista redakcja PDF w Delphi: usuwanie i oczyszczanie

Rzeczywista redakcja (redagowanie) plików PDF w środowisku Delphi polega na usuwaniu leżących u podstaw bajtów danych, a nie na ich zamalowywaniu. Biblioteka losLab PDF Library realizuje to za pomocą dwóch interfejsów API: metoda RedactRegion modyfikuje strumień zawartości strony, dzięki czemu usunięty tekst, wektory oraz obrazy są fizycznie usuwane, natomiast metoda SanitizeDocument czyści metadane, skrypty i akcje, w których również mogą ukrywać się poufne informacje. Popełnienie błędu w którejkolwiek z tych kwestii sprawi, że wyślesz plik, który na ekranie wygląda na ocenzurowany, ale ujawnia tajemnicę każdemu, kto skopiuje tekst lub otworzy Właściwości Dokumentu

Ten rodzaj błędu jest powszechnie znany z wielu publicznych wpadek. Redakcje gazet, sądy i agencje rządowe wielokrotnie publikowały pliki PDF z czarnym prostokątem nałożonym na nazwisko lub numer, po czym czytelnicy po prostu zaznaczali ukryty pod spodem tekst, kopiowali go i wklejali rzekomo zredagowany ciąg znaków. Prostokąt był tylko rysunkiem. Tekst nadal tam był, zapisany w strumieniu zawartości jedną warstwę pod nałożoną grafiką, w pełni nienaruszony i gotowy do zaznaczenia. Biblioteka losLab PDF Library — silnik PDF dla Delphi i C++Builder dostępny poprzez klasę TPDFlib — traktuje to rozróżnienie jako kluczowy cel procesu redagowania, a nie jako kwestię drugorzędną

Dlaczego rysowanie czarnego prostokąta na tekście PDF nie jest prawdziwą redakcją?

Biblioteka losLab PDF Library udostępnia metodę MaskRect dla przypadków, w których rzeczywiście zależy Ci jedynie na przykryciu zawartości: znaku wodnego, wizualnego zaciemnienia lub stempla kontrolnego. Wywołanie MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) dołącza prostokąt do strumienia zawartości strony i rysuje go za pomocą operatora tworzenia ścieżki re, a następnie operatora wypełnienia f (ISO 32000-1 §8.5). Każdy bajt, który znajdował się już na stronie — glify zasłoniętego nazwiska, linie wektorowe czy osadzony obraz — pozostaje dokładnie na swoim miejscu, jedną instrukcję wcześniej w tym samym strumieniu. Zgodna zintegrowana przeglądarka rysuje czarną ramkę na końcu, dzięki czemu wizualnie dominuje ona na ekranie. Nakładka nie zmienia jednak niczego pod spodem

Każdy może odzyskać pierwotną zawartość na trzy sposoby: zaznaczając i kopiując tekst bezpośrednio przez nieprzezroczyste pole, uruchamiając narzędzie do ekstrakcji tekstu ze strony lub usuwając prostokąt zasłaniający ze strumienia zawartości. Metoda MaskRect jest pod tym względem szczera w swojej nazwie — ona maskuje, a nie usuwa. Rezultat przypomina naklejkę na słowie, które nadal można przeczytać po odklejeniu tej naklejki

Jak RedactRegion usuwa zawartość na poziomie instrukcji

Metoda RedactRegion(Page, Left, Top, Width, Height, Options) działa jedną warstwę głębiej niż nałożony kolor. Biblioteka losLab PDF Library analizuje każdą warstwę zawartości strony do postaci obiektu TPDFContentProgram, przeszukuje listę instancji za pomocą analizatora śledzącego macierz transformacji CTM (funkcja FindInstructionsInRect w modelu zawartości) i zaznacza każdy operator wyświetlania tekstu, rysowania ścieżki oraz osadzania obrazu Do, którego obszar rysowania przecina się z Twoim prostokątem. Te instrukcje są następnie usuwane za pomocą metody TPDFContentProgram.Delete, a zmodyfikowana warstwa jest zapisywana z powrotem poprzez WritePageContentLayer. Te bajty nie przesuwają się pod maskę — one trwale znikają ze strumienia. Uruchomiony później ekstraktor tekstu nie znajdzie nic, ponieważ nie ma tam już nic do odczytania

Warto wziąć pod uwagę dwa szczegóły implementacyjne. Metoda RedactRegion przyjmuje współrzędne prostokąta lewego górnego rogu w bieżącym układzie współrzędnych i jednostkach, po czym konwertuje go wewnętrznie na lewy dolny układ współrzędnych użytkownika (user space) wykorzystywany przez strumień zawartości, dzięki czemu określasz region w taki sposób, w jaki widzisz go na stronie. Gdy prostokąt obejmuje kilka instrukcji, RedactRegion usuwa je w kolejności malejących indeksów, ponieważ usunięcie instrukcji 4 przed instrukcją 7 przesunęłoby wszystkie późniejsze indeksy i uszkodziło zakres. Jeśli przekażesz parametr Options = 0, biblioteka losLab PDF Library nałoży również jednolity czarny prostokąt na wyczyszczony obszar jako znacznik wizualny — jednak ten znacznik ma charakter wyłącznie kosmetyczny i jest nakładany już po trwałym usunięciu rzeczywistej zawartości

W praktyce rzadko wpisuje się współrzędne prostokąta na sztywno w kodzie. Zazwyczaj przeszukuje się stronę w poszukiwaniu poufnego ciągu znaków i odczytuje jego ramkę otaczającą (bounding box) — to samo przeszukiwanie stron i enumeracja elementów, które opisano w notatce o lokalizowaniu tekstu i elementów strony w środowisku Delphi — a następnie przekazuje się ten prostokąt do metody RedactRegion:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', '') = 1 then
    begin
      // Delete everything painted inside a 220 x 14 pt box on page 1.
      // Coordinates are top-left, in the current origin and units.
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);   // Options=0 stamps a black marker
      PDF.SaveToFile('contract-redacted.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Gdzie w dokumencie PDF nadal ukrywają się zredagowane informacje?

Redagowanie, które ogranicza się do warstwy wizualnej, pozostawia ślady, ponieważ pliki PDF przechowują informacje w miejscach, do których zwykły czytelnik nigdy nie zagląda. Biblioteka losLab PDF Library grupuje te wycieki w konkretne kanały. Słownik informacji o dokumencie (Document Information Dictionary) oraz strumień metadanych XMP rutynowo przechowują ciągi znaków takie jak Autor, Program tworzący (Producer) czy Słowa kluczowe, które mogą ujawniać dane osoby lub sprawy usunięte z głównej treści dokumentu. Skrypty JavaScript na poziomie dokumentu oraz akcja OpenAction katalogu mogą wywoływać się przy otwarciu i uzyskać dostęp do danych, które uważałeś za usunięte. Każda adnotacja może posiadać opcjonalną akcję /A, a sama adnotacja — żółta kartka, stary formularz czy link — może znajdować się bezpośrednio nad zredagowanym miejscem, przechowując własną kopię tekstu. Żaden z tych elementów nie podlega wpływowi szyfrowania; zaszyfrowany plik z jawnym pakietem metadanych XMP nadal ujawnia swój tytuł robotom indeksującym, co stanowi lukę bezpieczeństwa warta osobnego audytu szyfrowania i uprawnień. Skuteczne oczyszczanie (sanitizacja) musi dotrzeć do każdego z tych ukrytych miejsc, a nie tylko do strony, którą właśnie oglądasz

Oczyszczanie dokumentu za pomocą metody SanitizeDocument

Biblioteka losLab PDF Library odpowiada na te zagrożenia wyciekiem danych metodą SanitizeDocument(Flags), która koordynuje usuwanie wybranych obszarów na podstawie maski bitowej i zwraca liczbę zastosowanych procedur czyszczenia. Flagi te można ze sobą łączyć. Flaga SANITIZE_JAVASCRIPT usuwa wszystkie pakiety JavaScript na poziomie dokumentu oraz wszelkie skrypty powiązane z akcją OpenAction. SANITIZE_ACTIONS usuwa słownik akcji /A ze wszystkich adnotacji (poprzez nową metodę TPDFAnnots.RemoveAnnotAction) i czyści OpenAction katalogu. Flaga SANITIZE_METADATA usuwa pola Autor, Temat, Słowa kluczowe, Twórca (Creator) oraz Program tworzący (Producer) zarówno ze Słownika informacji o dokumencie (ISO 32000-1 §14.3.3), jak i ze strumienia metadanych XMP (§14.3.2). Flaga SANITIZE_ANNOTATIONS usuwa wszystkie adnotacje w całym dokumencie, a SANITIZE_OPEN_ACTION usuwa wpis /OpenAction z katalogu dokumentu. Flaga SANITIZE_ALL stanowi sumę bitową wszystkich piesiu wymienionych flag

Konwencje indeksowania wewnątrz tych procedur czyszczących to dokładnie ten rodzaj szczegółów, na których łatwo wyłożyć się przy ręcznym pisaniu pętli, co jest głównym powodem, dla którego SanitizeDocument istnieje jako pojedyncze wywołanie. Indeksowanie w GlobalJavaScriptPackageName zaczyna się od 0, a metoda RemoveGlobalJavaScript zmniejsza rozmiar listy w miarę usuwania elementów, dlatego procedura czyszczenia skryptów JavaScript zawsze odczytuje pakiet o indeksie 0 i usuwa go, dopóki licznik nie spadnie do zera. Z kolei DeleteAnnotation korzysta z indeksowania od 1. AnnotationCount zwraca liczbę adnotacji dla aktualnie wybranej strony, dlatego procedura czyszcząca najpierw wybiera kolejno każdą ze stron przed zliczaniem. Biblioteka losLab PDF Library dba o te granice wewnętrznie, więc przekazujesz tylko jeden zestaw flag i odczytujesz wynik liczbowy zamiast implementować pięć różnych reguł iteracji

var
  Applied: Integer;
begin
  // Strip metadata, scripts, and actions but keep the annotations:
  Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
    SANITIZE_ACTIONS or SANITIZE_OPEN_ACTION);

  // Or clear every hidden channel, annotations included:
  Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;

Pełny przepływ pracy w zakresie redagowania i oczyszczania

Po połączeniu obu tych elementów cała procedura staje się krótka. Biblioteka losLab PDF Library usuwa wideczną zawartość za pomocą metody RedactRegion, strona po stronie, a następnie czyści wszystkie ukryte kanały za pomocą SanitizeDocument(SANITIZE_ALL) przed zapisaniem pliku. Ponieważ redagowanie zazwyczaj wykonuje się na dokumentach pochodzących z zewnętrznych, niekontrolowanych źródeł, warto ładować je w trybie zabezpieczonym — zgodnie z defensywnym podejściem opisanym w artykule o bezpiecznym parsowaniu niezaufanych plików PDF — dzięki czemu nieprawidłowo sformatowane dane wygenerują czysty błąd zamiast zawieszenia procesu w połowie operacji

var
  PDF: TPDFlib;
  Applied: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('incoming.pdf', '') = 1 then
    begin
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);       // delete the sensitive line
      Applied := PDF.SanitizeDocument(SANITIZE_ALL);   // clear metadata, JS, actions, annots
      PDF.SaveToFile('published.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Gdzie kończą się możliwości redagowania strumienia zawartości

Metoda RedactRegion operuje na strumieniu zawartości, w którym znajduje się tekst wygenerowany cyfrowo (born-digital), lecz nie ma w nim tekstu ze zeskanowanych stron. Gdy strona jest zeskanowanym obrazem, słowa są jedynie pikselami wewnątrz obiektu graficznego image XObject, a nie operatorami tekstowymi. W takiej sytuacji biblioteka losLab PDF Library nie może „wyciąć” słowa ze środka rastra w taki sposób, w jaki usuwa pojedynczy glif. Obsługa obrazów jest tu celowo konserwatywna: każdy obraz, którego obszar przecina się z prostokątem redagowania, jest usuwany w całości. Z tego powodu nałożenie pola redagowania na zeskanowaną stronę usunie cały skan, a nie tylko wybrany fragment. Redagowanie obszaru wewnątrz obrazu wymaga rasteryzacji, zamalowania i ponownego osadzenia na poziomie pikseli, co jest zupełnie inną operacją niż usunięcie instrukcji ze strumienia zawartości. Ta sama ostrożność dotyczy warstwy tekstowej OCR nałożonej na zeskanowaną stronę: RedactRegion usunie operatory tekstowe, które potrafi zinterpretować, ale obraz pod spodem pozostanie nienaruszony, chyba że jego obiekt image XObject również przetnie się z prostokątem

Dwa dodatkowe szczegóły decydują o dokładności wyników. Test przecięcia szacuje szerokość każdego fragmentu tekstu na podstawie liczby znaków i rozmiaru czcionki, a nie dokładnych metryk glifów, przez co bardzo krótkie słowo zapisane czcionką proporcjonalną może owinąć się wokół ciasno wyznaczonego prostokąta — warto nadać polu redagowania niewielki margines. Usuwanie odbywa się ponadto na poziomie całych instrukcji: metoda RedactRegion usuwa całe operatory wyświetlania tekstu, dlatego prostokąt, który obejmuje tylko część wyrazu, może usunąć sąsiednie znaki współdzielące ten sam operator. Żadne z tych ograniczeń nie dyskredytuje mechanizmu, stanowi jedynie powód do starannego wyznaczania obszaru i weryfikacji rezultatów

Zasadą zamykającą każdy proces redagowania jest weryfikacja, a nie ślepe zaufanie. Wykonaj redagowanie regionu, oczyść dokument, zapisz go do nowego pliku, otwórz go ponownie i spróbuj wyodrębnić zarówno usunięty tekst, jak i wyczyszczone metadane. Gdy obie próby zwrócą pusty wynik, oznacza to, że redagowanie powiodło się. Interfejsy API RedactRegion oraz SanitizeDocument opisane w tym artykule są dostarczane wraz z biblioteką losLab PDF Library dla Delphi i C++Builder wraz z pełną specyfikacją modelu zawartości i oczyszczania