Otwórz plik PDF wygenerowany przez Microsoft Word lub Excel, przejrzyj go i nic nie wyda się niezwykłe. Załaduj go do programu w Delphi, odczytaj liczbę stron, a liczba ta będzie prawidłowa. Następnie zapisz go ponownie z włączonym szyfrowaniem, a operacja zakończy się niepowodzeniem z błędem EListError lub plik wyjściowy otworzy się z ostrzeżeniem o uszkodzonej tabeli odsyłaczy skrośnych. Plik nigdy nie był uszkodzony. Jest to plik z referencjami hybrydowymi, a sama struktura, która pozwala piętnastoletniej przeglądarce go otworzyć, jest strukturą, która pokonuje parser zatrzymujący odczyt zbyt wcześnie.
Jest to jeden z najczęstszych sposobów, w jaki potok PDF, który przeszedł każdy test wewnętrzny, trafia na plik, którego nie jest w stanie przetworzyć w obie strony. Wszystkie pliki wejściowe były generowane wewnętrznie, więc nigdy nie były hybrydowe. Pierwszy plik hybrydowy pojawia się w dniu, w którym klient przesyła fakturę wyeksportowaną z arkusza kalkulacyjnego.
Co faktycznie zapisują Word i Excel
Norma ISO 32000-1 opisuje układ referencji hybrydowych w §7.5.8.4. Aplikacja, która chce korzystać z funkcji PDF 1.5, takich jak strumienie obiektów, pozwalając jednocześnie czytnikowi PDF 1.4 na otwarcie pliku, zapisuje informacje o odsyłaczach skrośnych dwukrotnie. Istnieje klasyczna tabela odsyłaczy skrośnych z wierszami ASCII o stałej szerokości, które kończyły każdy plik PDF do wersji 1.4, oraz strumień odsyłaczy skrośnych, który indeksuje resztę. Sekcja trailer sekcji klasycznej zawiera wpis /XRefStm, którego wartością jest przesunięcie bajtowe tego strumienia.
Podział zadań jest celowy. Obiekty, do których stary czytnik musi dotrzeć, w tym katalog i drzewo stron, są adresowalne z poziomu klasycznej tabeli. Obiekty, które zostały złożone w skompresowane strumienie obiektów, są oznaczone jako wolne w klasycznej tabeli za pomocą wpisu o typie f, dzięki czemu czytnik 1.4 pomija je i nigdy nie potyka się o strukturę, której nie potrafi przeanalizować. Ich rzeczywiste lokalizacje znajdują się wyłącznie w strumieniu odsyłaczy skrośnych. Sygnaturą takiego pliku jest jego końcówka: krótka sekcja klasyczna, często niebędąca niczym więcej niż xref, po którym następuje nagłówek podsekcji 0 0, której trailer wskazuje na /XRefStm, gdzie znajdują się rzeczywiste dane odzyskiwania.
Dlaczego prawidłowa liczba stron niczego nie dowodzi
Ponieważ katalog i drzewo stron są celowo osiągalne z klasycznej tabeli, moduł ładujący, który czyta tylko tę tabelę, znajduje /Root, przechodzi przez drzewo stron i zgłasza prawidłową liczbę stron. Wszystko, czego potrzebuje stary czytnik, jest obecne, więc plik wydaje się zdrowy. Obiektami, które zaginęły, są te spakowane do strumieni obiektów: słowniki pól AcroForm, elementy struktury tagowanego PDF, długi ogon małych słowników, które nigdy nie musiały być widoczne dla starszego czytnika.
Nie zauważysz luki, dopóki coś nie dotknie tych obiektów, a pełny ponowny zapis dotyka ich wszystkich. Przejście przez dokument w celu ponownego zaszyfrowania lub przepisania to dokładnie ta operacja, która żąda po kolei każdego numeru obiektu, dlatego objaw pojawia się w momencie zapisu, a nie ładowania, daleko od swojej przyczyny.
Pułapką jest detektor, który widzi xref i się zatrzymuje
Tanim sposobem na podjęcie decyzji o sposobie indeksowania pliku jest podążanie za startxref i zbadanie pierwszych bajtów, na które wskazuje. Słowo kluczowe xref oznacza klasyczną tabelę; obiekt strumieniowy oznacza strumień odsyłaczy skrośnych. Ten test jest poprawny dla każdego pliku, który decyduje się na jeden schemat. Jest on błędny w przypadku pliku hybrydowego, którego startxref celuje w klasyczną sekcję wyłącznie po to, by zadowolić stare czytniki, podczas gdy /XRefStm w trailerze tej sekcji jest miejscem, w którym faktycznie indeksowana jest większość dokumentu. Detektor, który zwraca "klasyczny" przy pierwszym napotkanym xref, nigdy nie odczyta /XRefStm, a każdy obiekt, który żyje tylko w strumieniu, staje się niewidoczny.
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // count is correct
// inspect or edit the loaded document here
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // walks every object
finally
Pdf.Free;
end;
end;
W przypadku detektora wczesnego wyjścia ładowanie wygląda dobrze, a ponowny zapis jest momentem, w którym dają o sobie znać nieobecne obiekty. Rozwiązaniem nie jest czytanie większej liczby bajtów na starcie; jest nim rozpoznanie hybrydowego trailera i podążanie za /XRefStm przed podjęciem decyzji, że przetwarzanie pliku zostało zakończone.
Kolejność scalania nie podlega negocjacjom
Po odczytaniu obu indeksów można je połączyć tylko w jednym kierunku. Strumień odsyłaczy skrośnych musi zostać scalony jako pierwszy, a klasyczne wpisy wypełnione wokół niego. Powodem jest małe oszustwo leżące u podstaw tego formatu. Plik hybrydowy oznacza swoje skompresowane obiekty jako wolne w klasycznej tabeli, aby stary czytnik je ignorował. Moduł ładujący, który honoruje zasadę "kto pierwszy, ten lepszy" i najpierw czyta klasyczną tabelę, zarejestruje te numery obiektów jako wolne, a następnie odrzuci wpisy strumienia, które faktycznie je lokalizują, ponieważ gniazda są już zajęte. Odwróć kolejność, a wpisy typu 2 ze strumienia (z których każdy jest numerem strumienia obiektu plus indeksem) zajmą gniazda, które mają posiadać, a klasyczne wpisy ułożą się wokół nich.
Ta sama dyscyplina chroni przed przywróceniem usuniętego obiektu przez starszą rewizję. Aktualizacje przyrostowe łączą się wstecz poprzez /Prev, a wolny wpis typu 0 jest wartownikiem informującym, że nowsza sekcja wycofała numer obiektu. Późniejsza, starsza sekcja w łańcuchu nie może nadpisać tego wartownika przestarzałą lokalizacją. Potraktuj pierwsze napotkanie jako autorytatywne dla wolnych znaczników, a usunięty obiekt pozostanie usunięty; potraktuj to nieostrożnie, a historia pliku przywróci zawartość, którą usunęła najnowsza rewizja.
Co to oznacza w HotPDF
Silnik sam obsługuje pliki z referencjami hybrydowymi na każdej ścieżce, która musi analizować dane odsyłaczy skrośnych. Załaduj dokument za pomocą LoadFromFile lub LoadFromStream, wprowadź zmiany i wywołaj SaveLoadedDocument; lub uruchom jednorazową operację, taką jak EncryptFile, która odczytuje plik wejściowy i zapisuje plik wyjściowy. W obu przypadkach odzyskiwanie odczytuje /XRefStm,scala sekcję strumienia przed klasycznymi wpisami i rozwiązuje obiekty żyjące w strumieniach, zanim moduł zapisu je wyliczy. Ścieżka szyfrowania AES-256 była miejscem, w którym problem pojawił się po raz pierwszy, ponieważ szyfrowanie dokumentu przepisuje każdy obiekt, a więc wymaga uprzedniego zlokalizowania każdego z nich.
// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
Szczegół warty zapamiętania znajduje się powyżej interfejsu API. Pliki pochodzące z programów Word, Excel, PowerPoint i wielu potoków typu "Zapisz jako PDF" są rutynowo hybrydowe, więc moduł ładujący, który testujesz tylko na wynikach własnego generatora, może nigdy nie napotkać takiego pliku w testach. Wypełnij swoje środowiska testowe dokumentami wyeksportowanymi z rzeczywistych aplikacji biurowych, a nie tylko plikami wygenerowanymi przez własny kod.
Sprawdzanie podejrzanego pliku
Dwa kroki kontrolne pozwalają szybko rozstrzygnąć tę kwestię. Otwórz plik w widoku szesnastkowym i odczytaj bajty po końcowym startxref; plik hybrydowy pokazuje krótką sekcję klasyczną, której słownik trailera zawiera /XRefStm. Możesz też porównać liczbę obiektów zgłoszoną przez pełny parser z najwyższym numerem obiektu deklarowanym przez /Size w trailerze. Duża różnica oznacza, że obiekty ukrywają się w strumieniach, których moduł ładujący nie otworzył, co jest tym samym brakiem, który później zmienia się w awarię przy zapisie.
Strona modułu zapisu tej historii, czyli sposób generowania strumieni obiektów i skompresowanych odsyłaczy skrośnych, została omówione w naszym artykule na temat strumieni obiektów i aktualizacji przyrostowych. Gdy omawiany plik hybrydowy jest również bardzo duży, techniki ładowania opisane w przewodniku po Direct File API dla dużych przepływów pracy PDF pozwalają na jego inspekcję bez ładowania całości do pamięci. Oba te rozwiązania naturalnie łączą się z opisaną tutaj procedurą odzyskiwania, która jest dostarczana jako część pakietu HotPDF Component dla Delphi i C++Builder wraz z interfejsami API do ładowania, edycji, szyfrowania i podpisów cyfrowych omówionymi w innych częściach tego bloga.