Wyeksportuj dokument z Microsoft Word lub Excel za pomocą Zapisz jako PDF, a plik na dysku jest — częściej niż nie — plikiem hybrydowym. Nosi swoje informacje o odsyłaczach dwukrotnie: raz jako klasyczną tabelę o stałej szerokości, którą kończyły się wszystkie pliki PDF do wersji 1.4, i raz jako skompresowany strumień odsyłaczy, na którym większość dokumentu faktycznie polega. Pojedynczy klucz zwiastuna (trailer), /XRefStm, zszywa oba widoki razem, a to, czy narzędzie widzi cały dokument, sprowadza się do tego, czy podąża za tym kluczem
Ten artykuł patrzy na pliki hybrydowe od strony konsumenta: jak wyglądają bajty na końcu pliku, jak oba widoki rozjeżdżają się podczas edycji oraz jak potok Delphi może wykrywać i kierować dane hybrydowe. To, jak loader scala widoki i dlaczego kolejność nie podlega negocjacji, jest tematem naszego artykułu o HotPDF dotyczącego ładowania plików hybrydowych; ten dotyczy rozpoznania układu w pierwszej kolejności
Dlaczego eksporty Office zapisują indeks dwukrotnie
PDF 1.5 wprowadził dwie funkcje, które zmieniły kształt pliku: strumienie odsyłaczy, które przechowują indeks obiektów jako skompresowane dane binarne zamiast tekstowej tabeli, oraz strumienie obiektów, które pakują wiele małych obiektów w jeden kontener skompresowany metodą Flate. Writer, który ich używa, produkuje mniejsze pliki, ale czytnik PDF 1.4 nie może otworzyć wyniku, ponieważ struktury, na których się opiera, słowo kluczowe xref i słownik trailer, zniknęły
ISO 32000-1 §7.5.8.4 definiuje kompromis. Plik hybrydowy zapisuje oba: klasyczną tabelę odsyłaczy adresującą obiekty, do których stary czytnik musi dotrzeć, między nimi katalog i drzewo stron, oraz strumień odsyłaczy, który indeksuje wszystko inne. Obiekty złożone do strumieni obiektów są oznaczone jako wolne w klasycznej tabeli, więc czytnik 1.4 pomija je bez skargi; ich prawdziwe lokalizacje istnieją tylko w strumieniu. Klasyczny zwiastun niesie następnie klucz /XRefStm zawierający przesunięcie bajtowe tego strumienia. Stary przeglądarka nigdy nie czyta klucza i renderuje plik z widoku tabeli. Nowoczesna przeglądarka podąża za nim i widzi kompletny dokument. Word i Excel emitują dokładnie ten układ od lat, dlatego pliki hybrydowe nie są egzotycznym przypadkiem brzegowym, ale dużą częścią tego, co otrzymują potoki biznesowe
Jak wygląda ogon pliku hybrydowego
Układ najłatwiej zrozumieć z bajtów. Oto ogon małego pliku hybrydowego, przesunięcia skrócone; w prawdziwym eksporcie Office wartość /XRefStm jest zwykle dużym przesunięciem blisko końca pliku. Kolejność czytania to obchodzenie od ogona opisane w naszym przeglądzie struktury pliku PDF: znajdź %%EOF, odczytaj startxref, przeskocz do tabeli
% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...
xref % classic section: what startxref points at
0 4
0000000000 65535 f % slot 0: head of the free list, always present
0000000017 00000 n % object 1: the catalog, visible to any reader
0000000000 65535 f % object 2: marked free -- lives in an object stream
0000000000 65535 f % object 3: same; only the stream view locates it
trailer
<<
/Size 4
/Root 1 0 R
/XRefStm 116 % byte offset of the cross-reference stream
>>
startxref
7164 % byte offset of the 'xref' keyword above
%%EOF
Dwa szczegóły w tym zrzucie niosą cały mechanizm. Po pierwsze, startxref wskazuje na klasyczną sekcję, celowo: to adres, na którym stary czytnik musi wylądować. Strumień odsyłaczy jest osiągalny tylko poprzez klucz /XRefStm wewnątrz słownika zwiastuna, więc parser, który nigdy nie szuka tego klucza, nigdy nie dowiaduje się, że strumień istnieje. Po drugie, obiekty 2 i 3 to kłamstwa łagodnego rodzaju. Klasyczna tabela deklaruje je jako wolne, ale są to prawdziwe obiekty siedzące wewnątrz skompresowanego kontenera; oznaczenie jako wolne to to, co powstrzymuje czytnik 1.4 przed potykaniem się o wpisy, których nie może użyć. Konsument, który ufa samemu klasycznemu widokowi, wnioskuje, że większość tego dokumentu nie istnieje
Jak oba widoki rozjeżdżają się
Plik hybrydowy prosto z Worda jest wewnętrznie spójny: oba widoki opisują ten sam dokument, każdy w swoim zadeklarowanym zakresie. Kłopot zaczyna się, gdy plik jest edytowany przez narzędzie, które rozumie tylko jeden z widoków. Rozważ narzędzie do stemplowania, które dołącza aktualizację przyrostową w klasycznym stylu: nowe obiekty, nową sekcję xref, łańcuch /Prev do poprzedniej sekcji oraz nowy zwiastun. Jeśli ten zwiastun porzuci klucz /XRefStm, widok strumienia jest osierocony; jeśli skopiuje starą wartość dalej, widok strumienia nadal opisuje dokument taki, jaki był przed edycją. Tak czy inaczej, oba indeksy teraz nie zgadzają się co do tego, co zawiera plik
Powstały plik ma charakterystyczny podpis awarii: obiekty widoczne w jednym widoku są brakujące lub przestarzałe w drugim. Czytnik, który rozwiązuje przez widok strumienia, znajduje wersję sprzed edycji zaktualizowanego obiektu lub wcale nie ma wpisu dla dołączonego. Czytnik na widoku tabeli widzi edycję, ale traci ślad skompresowanych obiektów, które lokalizuje sam strumień. W praktyce ujawnia się to jako pola formularza, które przetrwają w jednej przeglądarce i znikają w innej, adnotacje, które przebieg stemplowania pozornie usunął, lub wyszukiwania, które lądują na całkowicie niewłaściwym obiekcie
To, co czyni te pliki kosztownymi w debugowaniu, to fakt, że Adobe Acrobat zwykle otwiera je bez skargi: gdy indeks nie zgadza się z bajtami, po cichu odbudowuje dane odsyłaczy skanując w poszukiwaniu nagłówków obiektów, więc ten, kto wyprodukował uszkodzony plik, nie widzi nic złego. Awaria ujawnia się później, gdy plik dociera do rygorystycznego konsumenta, walidatora preflight, usługi podpisywania, zadania przyjęcia do archiwum, które ufa zadeklarowanej strukturze i zgłasza brakujące obiekty lub niedopasowanie odsyłaczy. „Otwiera się dobrze w Acrobacie” to sposób, w jaki zaczyna się niemal każde zgłoszenie o desynchronizacji hybrydowej
Wykrywanie pliku hybrydowego w czystym Delphi
Klasyfikowanie danych nie wymaga biblioteki PDF. Klucz /XRefStm może wystąpić tylko wewnątrz klasycznego słownika zwiastuna, a aktywny zwiastun znajduje się w ostatnich kilku kilobajtach pliku, ponieważ specyfikacja wymaga, aby %%EOF pojawił się blisko fizycznego końca. Odczytanie ograniczonego okna ogona i przeszukanie go wystarcza do triage:
uses
System.SysUtils, System.Classes, System.StrUtils, System.Math;
function IsHybridReferencePdf(const FileName: string): Boolean;
const
TailWindow = 2048;
var
Stream: TFileStream;
Buf: TBytes;
Tail: string;
Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
Result := False;
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
if Stream.Size < 48 then
Exit;
Len := Min(TailWindow, Integer(Stream.Size));
SetLength(Buf, Len);
Stream.Position := Stream.Size - Len;
Stream.ReadBuffer(Buf[0], Len);
finally
Stream.Free;
end;
// Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
Tail := TEncoding.ANSI.GetString(Buf);
// Find the LAST 'trailer' keyword: with incremental updates,
// the newest trailer is the one that governs the file
TrailerPos := 0;
NextPos := Pos('trailer', Tail);
while NextPos > 0 do
begin
TrailerPos := NextPos;
NextPos := PosEx('trailer', Tail, NextPos + 1);
end;
if TrailerPos = 0 then
Exit; // no classic trailer: a pure xref-stream file, not hybrid
// A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
Trzy wyniki układają się z trzema układami. Plik tylko-klasyczny ma zwiastun, ale nie ma /XRefStm: False. Plik, który w pełni angażuje się w strumienie odsyłaczy, nie ma w ogóle słowa kluczowego trailer, jego klucze zwiastuna żyją w słowniku strumienia: również False, poprawnie, ponieważ taki plik jest skompresowany, nie hybrydowy. Tylko podwójnie indeksowany układ zwraca True
Do zastosowania produkcyjnego dwa utwardzenia są warte dodatkowych linii. Sparsuj liczbę całkowitą po /XRefStm, przejdź (seek) do tego przesunięcia i potwierdź, że obiekt strumienia z /Type /XRef faktycznie tam siedzi; obcięty plik może nieść klucz, gdy strumienia nie ma, co należy do innego kubełka niż zdrowa hybryda. I traktuj rozmiar okna jako parametr: 2 KB obejmuje zwykłe wyjście Office, ale niezwykle duży słownik zwiastuna może wypchnąć słowo kluczowe poza zasięg, a poszerzenie okna bije zadeklarowanie pliku jako klasycznego przez przypadek
Kierowanie plikami hybrydowymi przez potok Delphi
Wykrycie kupuje ci decyzję dotyczącą kierowania. Dla plików, które są tylko czytane, renderowane lub walidowane, użyj loadera, który rozwiązuje oba widoki, a następnie zweryfikuj zachowanie zamiast bajtów. PDFium Component parsuje łańcuch /XRefStm podczas ładowania, więc tabela obiektów, którą widzi twój kod, jest scaloną, a sprawdzenia opisane w naszym artykule o walidacji strumieni obiektów i odsyłaczy stosują się bez zmian. Jeśli zdesynchronizowana hybryda jest uszkodzona na tyle mocno, że odmawia ładowania, silnik zgłasza to przez swój zestaw błędów, FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY oraz FPDF_ERR_PAGE, przy czym FPDF_ERR_FORMAT to ten, który produkuje uszkodzenie strukturalne. Nie polegaj jednak na tym sygnale: PDFium jest z założenia pobłażliwy i po cichu odbudowuje większość niespójnych plików, więc udane ładowanie dowodzi, że plik był odzyskiwalny, a nie że jego dwa widoki się zgadzają. Znaczące sprawdzenie spójności to porównanie tego, co znajduje pełne obejście obiektów, z tym, co deklaruje /Size zwiastuna
Dla plików, które twój potok modyfikuje, najbezpieczniejszą polityką jest w ogóle powstrzymanie ich przed byciem hybrydowymi. Ładowanie, po którym następuje pełny zapis przez HotPDF, przepisuje dokument z jednym, wewnętrznie spójnym odsyłaczem w jednej postaci: bez /XRefStm, bez drugiego widoku, który mógłby wypaść z synchronizacji, każdy obiekt należący do dokładnie jednego wpisu indeksu. Ta normalizacja to to, czego chcesz przed przyjęciem do archiwum, przed rygorystycznym RIP lub usługą podpisywania w dół strumienia oraz po każdej edycji zastosowanej do danych hybrydowych. Działa, ponieważ loader poprawnie scalił widoki po drodze do środka, mechanizm, który artykuł o hybrydowym HotPDF omawia szczegółowo
Jedną klasą plików, którą należy zostawić w spokoju, są dokumenty podpisane cyfrowo. Pełne przepisanie przemieszcza każdy bajt, co unieważnia każdy podpis obliczony na oryginalnych zakresach. Zmiana w podpisanej hybrydzie musi wejść jako właściwa aktualizacja przyrostowa, która utrzymuje oba widoki; plik, który potrzebuje tylko czytania, powinien przejść nietknięty. Normalizacja jest dla plików, które posiadasz; do podpisanych plików tylko kiedykolwiek dołączasz
Hybrydowe pliki PDF nie są zniekształcone; są własnym mostem kompatybilności formatu, a aplikacje Office będą je produkować tak długo, jak czytniki PDF 1.4 przetrwają w bazie instalacji. Potok, który potrafi wypatrzeć klucz /XRefStm, zwalidować scalony dokument za pomocą PDFium Component oraz zregenerować czyste wyjście z jednym indeksem za pomocą HotPDF Component, traktuje je jako to, czym są: zwykłe dane wejściowe z jednym dodatkowym drogowskazem w zwiastunie