Експортирайте документ от Microsoft Word или Excel чрез "Save as PDF" и файлът на диска в повечето случаи е хибридно-референтен файл. Той носи своята информация за кръстосани препратки (cross-reference) два пъти: веднъж като класическата таблица с фиксирана ширина, която завършваше всеки PDF до версия 1.4, и веднъж като компресиран поток за кръстосани препратки (cross-reference stream), от който всъщност зависи по-голямата част от документа. Един-единствен ключ на трейлър (trailer key), /XRefStm, зашива двата изгледа заедно, и това дали даден инструмент вижда целия документ зависи от това дали следва този ключ
Тази статия разглежда хибридните файлове от страна на консуматора: как изглеждат байтовете в края на файла, как двата изгледа се раздалечават при редактиране и как един Delphi конвейер (pipeline) може да засече и маршрутизира хибридни входове. Как един товарач (loader) слива изгледите и защо редът не подлежи на преговори е обект на нашата HotPDF статия за зареждане на хибридно-референтни файлове; тази е за разпознаване на оформлението на първо място
Защо Office експортите записват индекса два пъти
PDF 1.5 въведе две функции, които промениха формата на файла: потоци за кръстосани препратки (cross-reference streams), които съхраняват индекса на обектите като компресирани бинарни данни вместо таблица в обикновен текст, и потоци за обекти (object streams), които пакетират много малки обекти в един компресиран с Flate контейнер. Един писател (writer), който ги използва, произвежда по-малки файлове, но PDF 1.4 четец не може да отвори резултата, защото структурите, по които се ориентира, ключовата дума xref и речникът trailer, са изчезнали
ISO 32000-1 §7.5.8.4 дефинира компромиса. Един хибридно-референтен файл записва и двете: класическа таблица за кръстосани препратки, адресираща обектите, до които един стар четец трябва да достигне, сред които каталогът и дървото на страниците, и поток за кръстосани препратки, който индексира всичко останало. Обектите, сгънати в потоци за обекти (object streams), са маркирани като свободни (free) в класическата таблица, така че един 1.4 четец ги пропуска без оплаквания; истинските им местоположения съществуват само в потока. Класическият трейлър след това носи ключ /XRefStm, държащ байтовото отместване (byte offset) на този поток. Един стар viewer никога не чете ключа и рендира файла от изгледа на таблицата. Един модерен viewer го следва и вижда пълния документ. Word и Excel излъчват точно това оформление от години, поради което хибридните файлове не са екзотичен краен случай (corner case), а голям дял от това, което бизнес конвейерите получават
Как изглежда опашката на един хибриден файл
Оформлението е най-лесно за разбиране от байтовете. Ето опашката на малък хибриден файл, със съкратени отмествания; в реален Office експорт стойността на /XRefStm обикновено е голямо отместване близо до края на файла. Редът на четене е разходката отзад напред (tail-first walk), описана в нашия преглед на PDF структурата: намерете %%EOF, прочетете startxref, скочете към таблицата
% ... обекти на тялото, включително потоци за обекти и, при байт 116,
% потокът за кръстосани препратки (обект stream с /Type /XRef) ...
xref % класическа секция: към какво сочи startxref
0 4
0000000000 65535 f % слот 0: глава на списъка със свободни, винаги присъства
0000000017 00000 n % обект 1: каталогът, видим за всеки четец
0000000000 65535 f % обект 2: маркиран като свободен -- живее в поток за обекти
0000000000 65535 f % обект 3: същото; само изгледът на потока го намира
trailer
<<
/Size 4
/Root 1 0 R
/XRefStm 116 % байтово отместване на потока за кръстосани препратки
>>
startxref
7164 % байтово отместване на ключовата дума 'xref' по-горе
%%EOF
Два детайла в този дъмп носят целия механизъм. Първо, startxref сочи към класическата секция, умишлено: това е адресът, на който трябва да кацне един стар четец. Потокът за кръстосани препратки е достъпен само чрез ключа /XRefStm вътре в речника trailer, така че парсер, който никога не търси този ключ, никога не научава, че потокът съществува. Второ, обекти 2 и 3 са лъжи от доброкачествен вид. Класическата таблица ги обявява за свободни, но те са реални обекти, седящи вътре в компресиран контейнер; маркировката като свободни (free marking) е това, което предпазва един 1.4 четец да не се спъне в записи, които не може да използва. Консуматор, който вярва само на класическия изглед, заключава, че по-голямата част от този документ не съществува
Как двата изгледа се раздалечават
Един хибриден файл, току-що излязъл от Word, е вътрешно последователен: и двата изгледа описват един и същ документ, всеки в рамките на декларирания си обхват. Проблемът започва, когато файлът е редактиран от инструмент, който разбира само единия от изгледите. Помислете за програма за поставяне на печати (stamping utility), която добавя (appends) инкрементална актуализация в класически стил: нови обекти, нова xref секция, /Prev верига към предишната секция и нов трейлър. Ако този трейлър изпусне ключа /XRefStm, изгледът на потока остава осиротял; ако копира старата стойност напред, изгледът на потока все още описва документа такъв, какъвто е бил преди редакцията. Така или иначе, двата индекса вече не са съгласни относно това какво съдържа файлът
Полученият файл има отличителен подпис на провал: обекти, видими в единия изглед, липсват или са остарели (stale) в другия. Четец, който резолвира (resolves) чрез изгледа на потока, намира версията преди редакцията на актуализиран обект, или изобщо не намира запис за добавен такъв. Четец върху изгледа на таблицата вижда редакцията, но губи следата на компресираните обекти, които само потокът намира. На практика това излиза на повърхността като полета на формуляри, които оцеляват в един viewer и изчезват в друг, анотации, които проход за печат изглежда е изтрил, или търсения, които кацат върху напълно грешен обект
Това, което прави тези файлове скъпи за дебъгване, е, че Adobe Acrobat обикновено ги отваря без оплаквания: когато индексът не е съгласен с байтовете, той тихо преизгражда данните за кръстосани препратки, сканирайки за хедъри на обекти, така че който и да е произвел счупения файл, не вижда нищо нередно. Провалът излиза на повърхността по-късно, когато файлът достигне до строг консуматор, preflight валидатор, услуга за подписване, задача за архивно поглъщане (archival ingest job), които вярват на декларираната структура и отчитат липсващи обекти или несъответствие в кръстосаните препратки. "Отваря се добре в Acrobat" е начинът, по който започва почти всеки билет (ticket) за хибридна десинхронизация
Засичане на хибриден файл в чист Delphi
Класифицирането на входовете не изисква PDF библиотека. Ключът /XRefStm може да се появи само вътре в класически речник trailer, а активният трейлър седи в рамките на последните няколко килобайта от файла, защото спецификацията изисква %%EOF да се появи близо до физическия край. Четенето на ограничен прозорец на опашката и търсенето в него е достатъчно за триаж:
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;
// Всяка замесена ключова дума е 7-bit ASCII, така че побайтовото декодиране е безопасно
Tail := TEncoding.ANSI.GetString(Buf);
// Намерете ПОСЛЕДНАТА ключова дума 'trailer': при инкрементални актуализации,
// най-новият трейлър е този, който управлява файла
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; // няма класически трейлър: чист xref-stream файл, не е хибриден
// Един хибриден трейлър носи /XRefStm между 'trailer' и 'startxref'
KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
StartXrefPos := PosEx('startxref', Tail, TrailerPos);
Result := (KeyPos > 0) and
((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;
Трите резултата се подреждат с трите оформления. Файл само с класически изглед има трейлър, но няма /XRefStm: False. Файл, който се ангажира напълно с потоци за кръстосани препратки, няма ключова дума trailer изобщо, неговите ключове на трейлъра живеят в речника на потока: също False, правилно, защото такъв файл е компресиран, а не хибриден. Само двойно-индексираното оформление връща True
За производствена употреба, две втвърдявания (hardenings) си струват допълнителните редове. Парснете цялото число след /XRefStm, потърсете това отместване и потвърдете, че обект stream с /Type /XRef действително седи там; отрязан (truncated) файл може да носи ключа, докато потокът липсва, което принадлежи към различна кофа (bucket) от здрав хибрид. И третирайте размера на прозореца като параметър: 2 KB покрива обикновен Office изход, но необичайно голям речник на трейлъра може да избута ключовата дума извън обхват, а разширяването на прозореца е по-добро от това да обявите файла за класически по погрешка
Маршрутизиране на хибридни файлове през Delphi конвейер
Засичането ви купува решение за маршрутизиране. За файлове, които само се четат, рендират или валидират, използвайте товарач (loader), който резолвира и двата изгледа, след което валидирайте поведението, а не байтовете. Компонентът PDFium парсва веригата /XRefStm по време на зареждане, така че таблицата с обекти, която вашият код вижда, е слятата, и проверките, описани в нашата статия за валидиране на потоци за обекти и кръстосани препратки, се прилагат непроменени. Ако десинхронизиран хибрид е повреден достатъчно лошо, за да откаже зареждане, енджинът го отчита чрез своя набор от грешки, FPDF_ERR_SUCCESS, FPDF_ERR_UNKNOWN, FPDF_ERR_FILE, FPDF_ERR_FORMAT, FPDF_ERR_PASSWORD, FPDF_ERR_SECURITY и FPDF_ERR_PAGE, като FPDF_ERR_FORMAT е тази, която структурната повреда произвежда. Не разчитайте на този сигнал обаче: PDFium е снизходителен (lenient) по дизайн и преизгражда повечето непоследователни файлове тихо, така че успешно зареждане доказва, че файлът е бил възстановим, а не че двата му изгледа са съгласни. Смислената проверка за последователност (consistency check) е сравняването на това, което пълна разходка по обектите (object walk) намира, спрямо това, което /Size на трейлъра декларира
За файлове, които вашият конвейер променя, най-безопасната политика е да спрете да бъдат хибридни изобщо. Зареждане, последвано от пълно запазване чрез HotPDF, пренаписва документа с единична, самосъгласувана (self-consistent) кръстосана препратка в една форма: без /XRefStm, без втори изглед, който да изпадне от синхрон, всеки обект е притежаван от точно един запис в индекса. Тази нормализация е това, което искате преди архивно поглъщане (archival ingest), преди строг RIP надолу по веригата или услуга за подписване, и след всяка редакция, приложена към хибриден вход. Работи, защото товарачът е слял изгледите правилно на влизане, механизмът, през който статията на HotPDF за хибридни препратки (hybrid-reference) преминава в детайли
Единственият клас файлове, които трябва да оставите на мира, са цифрово подписаните документи. Едно пълно пренаписване премества всеки байт, което инвалидира всеки подпис, изчислен върху оригиналните диапазони. Една промяна на подписан хибрид трябва да влезе като правилна инкрементална актуализация, която поддържа и двата изгледа; файл, който има нужда само от четене, трябва да премине непокътнат. Нормализацията е за файлове, които притежавате; подписани файлове само допълвате (append)
Хибридно-референтните PDF файлове не са лошо форматирани (malformed); те са собственият мост за съвместимост на формата, и Office приложенията ще продължат да ги произвеждат, докато PDF 1.4 четци оцеляват в инсталационната база (install base). Конвейер, който може да забележи ключа /XRefStm, да валидира слетия документ с компонента PDFium и да регенерира чист изход с единствен индекс с компонента HotPDF, ги третира като това, което са: обикновени входове с един допълнителен указател в трейлъра