Отворете PDF файл, създаден от Microsoft Word или Excel, прелистете го и нищо не изглежда необичайно. Заредете го в програма на Delphi, прочетете броя на страниците и числото е правилно. След това го запишете отново с включено шифриране и процесът се срива с EListError или изходният файл се отваря с предупреждение за повредена таблица за кръстосани препратки. Файлът никога не е бил повреден. Това е файл с хибридни референции и самата структура, която позволява на петнадесетгодишен четец да го отвори, е структурата, която проваля модул за зареждане, който спира четенето твърде рано.
Това е един от най-често срещаните начини, по които тръбопровод за PDF, преминал всички вътрешни тестове, се сблъсква с файл, който не може да преработи. Всички тестови файлове са генерирани вътрешно, така че никога не са били хибридни. Първият хибриден файл пристига в деня, в който клиент препрати фактура, експортирана от електронна таблица.
Какво всъщност записват Word и Excel
ISO 32000-1 описва оформлението с хибридни референции в §7.5.8.4. Приложение, което иска функции на PDF 1.5, като например потоци от обекти, но същевременно иска да позволи на четец за PDF 1.4 да отвори файла, записва информацията за кръстосани препратки два пъти. Има класическа таблица за кръстосани препратки, редовете с фиксирана ширина в ASCII формат, с които завършваше всеки PDF до версия 1.4, и има поток за кръстосани препратки, който индексира останалата част. Заключителната част (trailer) на класическата секция съдържа запис /XRefStm, чиято стойност е отместването в байтове на този поток.
Разделението на задачите е умишлено. Обектите, до които по-стар четец трябва да достигне, сред които са каталогът и дървото на страниците, са адресируеми от класическата таблица. Обектите, които са събрани в компресирани потоци от обекти, са маркирани като свободни в класическата таблица със запис от тип f, така че четец за 1.4 ги прескача директно и никога не се спъва в структура, която не може да анализира. Действителните им местоположения се намират само в потока за кръстосани препратки. Подписът на такъв файл е неговият край: кратка класическа секция, често нищо повече от xref, последвано от заглавна част на подсекция 0 0, чийто trailer сочи към /XRefStm, където се намират действителните данни за възстановяване.
Защо правилният брой страници не доказва нищо
Тъй като каталогът и дървото на страниците са умишлено достъпни от класическата таблица, модулът за зареждане, който чете само тази таблица, намира /Root, обхожда дървото на страниците и съобщава правилния брой страници. Всичко, от което се нуждае един стар четец, е налице, така че файлът изглежда изправен. Обектите, които са изчезнали, са тези, пакетирани в потоци от обекти: речници на полета на AcroForm, структурни елементи на tagged-PDF, дългата опашка от малки речници, които никога не е трябвало да бъдат видими за остарелия четец.
Не забелязвате празнината, докато нещо не докосне тези обекти, а едно пълно повторно записване докосва всички тях. Обхождането на документа за повторно шифриране или презаписване е точно операцията, която изисква поред всеки номер на обект, поради което симптомът се появява при записване, а не при зареждане, далеч от неговата причина.
Капанът е детектор, който вижда xref и спира
Лесният начин да решите как е индексиран един файл е да последвате startxref и да проверите първите байтове, към които сочи. Ключовата дума xref означава класическа таблица; поток от обекти означава поток за кръстосани препратки. Този тест е правилен за всеки файл, който се придържа към една схема. Той е погрешен за хибриден файл, чийто startxref е насочен към класическа секция с единствената цел да удовлетвори старите четци, докато /XRefStm в trailer-а на тази секция е мястото, където всъщност е индексирана по-голямата част от документа. Детектор, който връща „classic“ при първия xref, който срещне, никога не чете /XRefStm и всеки обект, който живее само в потока, става невидим.
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;
При наличието на детектор за ранно излизане зареждането изглежда добре, а повторното записване е мястото, където липсващите обекти се проявяват. Решението не е да се четат повече байтове в началото; то е да се разпознае хибридният trailer и да се последва /XRefStm, преди да се реши, че файлът е готов.
Редът на сливане не подлежи на договаряне
След като и двата индекса са прочетени, те могат да бъдат комбинирани само в една посока. Потокът за кръстосани препратки трябва да бъде слят първи, а класическите записи да се попълнят около него. Причината е малката измама в основата на формата. Хибридният файл маркира компресираните си обекти като свободни в класическата таблица, за да ги игнорират старите четци. Модул за зареждане, който спазва политиката „първият видян печели“ и първо чете класическата таблица, ще запише тези номера на обекти като свободни, след което ще изхвърли записите от потока, които действително ги локализират, тъй като слотовете вече са заети. Обърнете реда и записите от тип 2 от потока, всеки от които е номер на поток от обекти плюс индекс, печелят слотовете, които трябва да притежават, а класическите записи се подреждат около тях.
Същата дисциплина предпазва от това по-стара ревизия да възкреси изтрит обект. Инкременталните актуализации се свързват назад чрез /Prev, а свободният запис от тип 0 е маркер, че по-нова секция е извадила от употреба номер на обект. На по-късна, по-стара секция във веригата не трябва да се позволява да презаписва този маркер с остаряло местоположение. Разглеждайте първия видян запис като авторитетен за свободни маркери и изтритият обект остава изтрит; отнесете се небрежно и собствената история на файла ще съживи съдържание, което последната ревизия е премахнала.
Какво означава това в HotPDF
Механизмът разрешава вместо вас файлове с хибридни референции и прави това при всеки път, който трябва да анализира данните за кръстосани препратки. Заредете документ с LoadFromFile или LoadFromStream, направете промените си и извикайте SaveLoadedDocument; или изпълнете еднократна операция, като например EncryptFile, която чете входни данни и записва изходни. И в двата случая възстановяването чете /XRefStm, слива секцията на потока преди класическите записи и разрешава обектите, които живеят в потоци, преди записа да ги изброи. Пътят на шифриране с AES-256 е мястото, където проблемът се появи за първи път, тъй като шифрирането на документ презаписва всеки обект и съответно изисква всеки обект вече да е бил локализиран.
// 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]);
Детайлът, който си струва да запомните, се намира преди API. Файловете, които пристигат от Word, Excel, PowerPoint и дълъг списък от тръбопроводи за „Запазване като PDF“, обикновено са хибридни, така че модул за зареждане, който тествате само срещу изхода на вашия собствен генератор, може никога да не срещне такъв при тестване. Захранете тестовата си среда с документи, експортирани от реални Office приложения, а не само с файлове, произведени от вашия собствен код.
Проверка на файл, в който се съмнявате
Две проверки решават бързо въпроса. Отворете файла в шестнадесетичен изглед и прочетете байтовете след последния startxref; хибридният файл показва кратка класическа секция, чийто trailer речник съдържа /XRefStm. Или сравнете броя на обектите, които пълният анализ отчита, с най-високия номер на обект, който /Size декларира в trailer-а. Голямата празнина означава, че обектите се крият в потоци, които модулът за зареждане не е отворил, което е същият недостиг, който по-късно се превръща в срив при записване.
Страната на разработчика в тази история – как се създават потоци от обекти и компресирани кръстосани препратки, е разгледана в нашата статия за потоци от обекти и инкрементални актуализации. Когато разглежданият хибриден файл е много голям, техниките за зареждане в ръководството за Direct File API за големи PDF работни процеси ви позволяват да го проверите, без да го зареждате изцяло в паметта. И двете се допълват по естествен път с описаното тук възстановяване, което се доставя като част от HotPDF Component за Delphi и C++Builder, заедно с API за зареждане, редактиране, шифриране и подписване, разгледани на други места в този блог.