Artykuł techniczny

Obsługa hybrydowych plików PDF z aplikacji Office w Delphi

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

PDF: Ogon PDF z odniesieniem hybrydowym, gdzie przestarzały czytnik ufa klasycznej tabeli xref, podczas gdy nowoczesny czytnik podąża za /XRefStm do widoku strumienia skompresowanego
Jeden klucz traileru rozstrzyga widok: klasyczna tabela obsługuje starych czytników, podczas gdy /XRefStm wręcza nowoczesnym czytnikom strumień, który lokalizuje wszystko inne

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

% ... obiekty treści, w tym strumienie obiektów i, przy bajcie 116,
% strumień cross-reference (obiekt strumieniowy z /Type /XRef) ...

xref                    % klasyczna sekcja: na co wskazuje startxref
0 4
0000000000 65535 f      % slot 0: początek listy wolnych obiektów, zawsze obecny
0000000017 00000 n      % obiekt 1: katalog, widoczny dla każdego czytnika
0000000000 65535 f      % obiekt 2: oznaczony jako wolny — znajduje się w strumieniu obiektów
0000000000 65535 f      % obiekt 3: tak samo; lokalizuje go tylko widok strumienia
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % offset bajtowy strumienia cross-reference
>>
startxref
7164                    % offset bajtowy powyższego słowa kluczowego 'xref'
%%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;

  // Każde zaangażowane słowo kluczowe to 7-bitowy ASCII, więc dekodowanie bajt po bajcie jest bezpieczne
  Tail := TEncoding.ANSI.GetString(Buf);

  // Znajdź OSTATNIE słowo kluczowe 'trailer': przy aktualizacjach przyrostowych,
  // to ostatni trailer rządzi plikiem
  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;  // brak klasycznego trailera: plik czysto xref-stream, nie hybrydowy

  // Hybrydowy trailer niesie /XRefStm między 'trailer' a '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

Przepływ decyzyjny Delphi skanujący ogon pliku za ostatnim trailerem i /XRefStm, potem kierujący hybrydowe PDF-y ku walidacji, normalizacji albo obsłudze tylko-do-dopisywania
Ograniczone przeszukanie ogona daje trzy werdykty, i tylko przypadek podwójnie indeksowany rusza dalej jako prawdziwa hybryda

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 Delphi Component, traktuje je jako to, czym są: zwykłe dane wejściowe z jednym dodatkowym drogowskazem w zwiastunie