Technický článek

Zpracování hybridních referenčních PDF z aplikací Office v Delphi

Exportujte dokument z Microsoft Word nebo Excel pomocí příkazu Uložit jako PDF a soubor na disku bude s největší pravděpodobností hybridní referenční soubor. Nese své informace o křížových odkazech (cross-reference information) dvakrát: jednou jako klasickou tabulku s pevnou šířkou, která ukončovala každé PDF až do verze 1.4, a podruhé jako komprimovaný datový proud křížových odkazů (cross-reference stream), na kterém ve skutečnosti závisí většina dokumentu. Jediný klíč v traileru, /XRefStm, spojuje oba pohledy (views) dohromady, a to, zda nástroj uvidí celý dokument, závisí na tom, zda tento klíč následuje

Tento článek se dívá na hybridní soubory ze strany spotřebitele (consuming side): jak vypadají bajty na konci souboru, jak se od sebe oba pohledy při úpravách vzdalují (drift apart) a jak dokáže pipeline v Delphi detekovat a směrovat hybridní vstupy. Jak zavaděč (loader) tyto pohledy slučuje a proč je pořadí neoddiskutovatelné, je předmětem našeho článku o načítání hybridních referenčních souborů v HotPDF; tento článek je v prvé řadě o rozpoznání rozložení (layout)

Proč exporty z Office zapisují index dvakrát

PDF 1.5 zavedlo dvě funkce, které změnily podobu souboru: datové proudy křížových odkazů (cross-reference streams), které ukládají index objektů jako komprimovaná binární data namísto tabulky v prostém textu (plaintext table), a datové proudy objektů (object streams), které sbalí (pack) mnoho malých objektů do jednoho kontejneru s kompresí Flate. Zapisovač, který je používá, vytváří menší soubory, ale čtečka PDF 1.4 nedokáže výsledek otevřít, protože struktury, na které se spoléhá, klíčové slovo xref a slovník trailer, jsou pryč

ISO 32000-1 §7.5.8.4 definuje kompromis. Hybridní referenční soubor zapisuje obojí: klasickou tabulku křížových odkazů, která adresuje objekty, k nimž se musí dostat stará čtečka, včetně katalogu a stromu stránek (page tree), a datový proud křížových odkazů (cross-reference stream), který indexuje vše ostatní. Objekty zabalené (folded) do datových proudů objektů jsou v klasické tabulce označeny jako volné (free), takže čtečka verze 1.4 je bez reptání přeskočí; jejich skutečná umístění existují pouze ve streamu. Klasický trailer pak nese klíč /XRefStm obsahující bajtový offset (byte offset) tohoto datového proudu. Starý prohlížeč tento klíč nikdy nečte a vykreslí soubor z pohledu tabulky. Moderní prohlížeč jej následuje a vidí kompletní dokument. Word a Excel přesně toto rozložení produkují už léta, a proto hybridní soubory nejsou exotickým okrajovým případem (corner case), ale tvoří velký podíl toho, co přijímají podnikové pipeline

Jak vypadá konec hybridního souboru (tail of a hybrid file)

Rozložení se nejlépe chápe z bajtů. Zde je konec (tail) malého hybridního souboru, offsety jsou zkráceny; ve skutečném exportu z Office je hodnota /XRefStm obvykle velký offset blízko konce souboru. Pořadí čtení je postup od konce (tail-first walk) popsaný v našem přehledu struktury souboru PDF: najděte %%EOF, přečtěte startxref, přeskočte na tabulku

% ... tělo objektů (body objects), včetně datových proudů objektů a na bajtu 116
% datového proudu křížových odkazů (objekt typu stream s /Type /XRef) ...

xref                    % klasická sekce: na co ukazuje startxref
0 4
0000000000 65535 f      % slot 0: hlavička seznamu volných položek (free list), vždy přítomna
0000000017 00000 n      % objekt 1: katalog, viditelný pro každou čtečku
0000000000 65535 f      % objekt 2: označen jako volný -- žije ve streamu objektů
0000000000 65535 f      % objekt 3: to samé; lokalizuje jej pouze pohled ze streamu (stream view)
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % bajtový offset datového proudu křížových odkazů
>>
startxref
7164                    % bajtový offset klíčového slova 'xref' výše
%%EOF

Dva detaily v tomto výpisu (dump) nesou celý mechanismus. Zaprvé, startxref ukazuje na klasickou sekci, a to záměrně: to je adresa, na které musí přistát stará čtečka. K datovému proudu křížových odkazů se lze dostat pouze přes klíč /XRefStm uvnitř slovníku traileru, takže parser, který tento klíč nikdy nehledá, se nikdy nedozví o existenci streamu. Zadruhé, objekty 2 a 3 jsou lži neškodného druhu. Klasická tabulka je deklaruje jako volné (free), ale jsou to skutečné objekty sedící uvnitř komprimovaného kontejneru; označení "free" je to, co brání čtečce verze 1.4 zakopnout o položky, které nemůže použít. Spotřebitel (consumer), který důvěřuje pouze klasickému pohledu, dojde k závěru, že většina tohoto dokumentu neexistuje

Jak se od sebe oba pohledy vzdalují (drift apart)

Hybridní soubor čerstvě vytvořený ve Wordu je vnitřně konzistentní: oba pohledy popisují stejný dokument, každý ve svém deklarovaném rozsahu (scope). Problémy začínají ve chvíli, kdy je soubor upravován nástrojem, který rozumí pouze jednomu z pohledů. Uvažujme například nástroj pro razítkování (stamping utility), který připojí přírůstkovou aktualizaci klasického typu (classic-style incremental update): nové objekty, novou sekci xref, řetězec /Prev do předchozí sekce a nový trailer. Pokud tento trailer zahodí klíč /XRefStm, pohled ze streamu (stream view) osiří; pokud zkopíruje starou hodnotu vpřed, pohled ze streamu stále popisuje dokument tak, jak vypadal před úpravou. Tak či onak se nyní oba indexy neshodnou na tom, co soubor obsahuje

Výsledný soubor má charakteristický znak selhání: objekty viditelné v jednom pohledu ve druhém chybí, nebo jsou zastaralé (stale). Čtečka, která překládá (resolves) přes pohled ze streamu, najde verzi aktualizovaného objektu před úpravou, nebo nenajde vůbec žádný záznam pro připojený (appended) objekt. Čtečka pracující s pohledem z tabulky vidí úpravu, ale ztrácí přehled o komprimovaných objektech, které lokalizuje pouze stream. V praxi se to projeví tak, že se objeví pole formuláře, která v jednom prohlížeči přežijí a ve druhém zmizí, anotace, které podle všeho smazal průchod razítkováním, nebo vyhledávání (lookups), která dopadnou na úplně špatný objekt

Důvodem, proč je ladění těchto souborů tak drahé, je to, že Adobe Acrobat je obvykle bez reptání otevře: když index nesouhlasí s bajty, tiše přestaví data křížových odkazů tím, že vyhledává (scanning) hlavičky objektů, takže kdokoli poškozený soubor vytvořil, nevidí, že by bylo něco špatně. Selhání vypluje na povrch později, když se soubor dostane k přísnému spotřebiteli (strict consumer), validátoru předtiskové přípravy (preflight validator), podepisovací službě (signing service) nebo úloze pro příjem do archivu, která důvěřuje deklarované struktuře a ohlásí chybějící objekty nebo neshodu křížových odkazů. „V Acrobatu se to otevírá v pohodě“ (It opens fine in Acrobat), tak začíná téměř každý ticket o desynchronizaci hybridního souboru

Detekce hybridního souboru v čistém (plain) Delphi

Klasifikace vstupů nevyžaduje PDF knihovnu. Klíč /XRefStm se může vyskytnout pouze uvnitř klasického slovníku traileru a aktivní trailer sedí v posledních několika kilobajtech souboru, protože specifikace vyžaduje, aby se %%EOF objevilo blízko fyzického konce. Na třídění (triage) stačí přečíst ohraničené okno na konci souboru (bounded tail window) a prohledat ho:

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;

  // Všechna použitá klíčová slova jsou v 7bitovém ASCII, takže dekódování po bajtech je bezpečné
  Tail := TEncoding.ANSI.GetString(Buf);

  // Najděte POSLEDNÍ (LAST) klíčové slovo 'trailer': u přírůstkových aktualizací
  // ten nejnovější trailer je ten, který řídí soubor
  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;  // žádný klasický trailer: čistě xref-stream soubor, ne hybridní

  // Hybridní trailer nese /XRefStm mezi 'trailer' a 'startxref'
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

Tři možné výsledky se shodují s těmito třemi rozloženími. Soubor s výhradně klasickým rozložením má trailer, ale nemá /XRefStm: False. Soubor, který se plně opírá o datové proudy křížových odkazů (cross-reference streams), nemá klíčové slovo trailer vůbec, jeho klíče traileru žijí ve slovníku streamu: také False, a to správně, protože takový soubor je komprimovaný, ne hybridní. Pouze dvojitě indexované rozložení vrací True

Pro produkční použití stojí za těch pár řádků navíc dvě vylepšení k zajištění větší odolnosti (hardenings). Analyzujte (parse) celé číslo za /XRefStm, skočte (seek) na tento offset a potvrďte, že tam skutečně sedí objekt typu stream s /Type /XRef; zkrácený (truncated) soubor může nést klíč, zatímco stream je pryč, což patří do jiné přihrádky než zdravý hybridní soubor. A zacházejte s velikostí okna jako s parametrem: 2 KB pokryjí běžný výstup z Office, ale neobvykle velký slovník traileru může vytlačit klíčové slovo mimo dosah, a tak je rozšíření okna lepší než omylem deklarovat soubor jako klasický

Směrování hybridních souborů přes Delphi pipeline

Detekce vám usnadní rozhodování o směrování. Pro soubory, které se pouze čtou, vykreslují nebo ověřují, použijte zavaděč (loader), který přeloží (resolves) oba pohledy, a pak ověřujte chování, nikoli bajty. PDFium Component během načítání analyzuje řetězec /XRefStm, takže tabulka objektů, kterou váš kód vidí, je ta sloučená, a kontroly popsané v našem článku o ověřování datových proudů objektů a křížových odkazů platí beze změny. Pokud je desynchronizovaný hybridní soubor poškozen tak závažně, že ho nelze načíst, jádro (engine) to ohlásí prostřednictvím své sady chyb, FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY a FPDF_ERR_PAGE, přičemž FPDF_ERR_FORMAT je chyba, kterou vyvolá poškození struktury. Nespoléhejte se však na tento signál: PDFium je ze své podstaty tolerantní a tiše přebuduje (rebuilds) většinu nekonzistentních souborů, takže úspěšné načtení dokazuje, že soubor byl obnovitelný (recoverable), ne to, že se jeho dva pohledy shodují. Smysluplnou kontrolou konzistence je porovnat to, co najde úplný průchod objekty (full object walk), s tím, co deklaruje /Size v traileru

Pro soubory, které vaše pipeline upravuje, je nejbezpečnější politikou zabránit tomu, aby vůbec byly hybridní. Načtení následované úplným uložením přes HotPDF přepíše dokument pomocí jediného, vnitřně konzistentního křížového odkazu v jedné podobě: žádný /XRefStm, žádný druhý pohled (view), který by mohl vypadnout ze synchronizace, každý objekt je vlastněn (owned) přesně jedním záznamem indexu. Tato normalizace je to, co chcete před příjmem do archivu (archival ingest), před přísným RIP nástrojem nebo podepisovací službou dále v řetězci (downstream) a po jakékoli úpravě aplikované na hybridní vstup. Funguje to, protože zavaděč (loader) sloučil pohledy správně už při vstupu (on the way in), což je mechanismus, kterým podrobně provází článek o hybridních odkazech v HotPDF

Jedinou třídou souborů, kterou byste měli nechat být, jsou digitálně podepsané dokumenty. Úplné přepsání (full rewrite) posune každý bajt, což zneplatní (invalidates) každý podpis vypočítaný nad původními rozsahy (original ranges). Změna v podepsaném hybridním souboru musí projít jako řádná přírůstková aktualizace (proper incremental update), která zachová oba pohledy; soubor, který stačí pouze přečíst, by měl projít nedotčený. Normalizace je pro soubory, které vlastníte; podepsané soubory pouze vždy jen doplňujete (append to)

Hybridní referenční PDF nejsou chybně naformátované (malformed); představují nativní most pro zajištění kompatibility samotného formátu a aplikace Office je budou i nadále produkovat tak dlouho, dokud se v instalované základně udrží čtečky PDF 1.4. Pipeline, která dokáže rozpoznat klíč /XRefStm, ověřit sloučený dokument pomocí komponenty PDFium Component a znovu vygenerovat čistý výstup s jedním indexem pomocí HotPDF Component, s nimi zachází tak, jaké skutečně jsou: běžné vstupy s jedním rozcestníkem (signpost) navíc v traileru