Відкрийте 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, та потік перехресних посилань, який індексує все інше. Трейлер класичної секції містить запис /XRefStm, значенням якого є зміщення цього потоку в байтах.
Розподіл праці є навмисним. Об'єкти, до яких має отримати доступ старий читач, зокрема каталог і дерево сторінок, доступні з класичної таблиці. Об'єкти, які були згорнуті в стиснуті потоки об'єктів, позначені в класичній таблиці як вільні із записом типу f, тому читач PDF 1.4 пропускає їх і ніколи не стикається зі структурою, яку він не може проаналізувати. Їхні реальні розташування містяться лише в потоці перехресних посилань. Ознакою такого файлу є його кінець: коротка класична секція, часто не більше ніж xref, за якою йде заголовок підсекції 0 0, трейлер якої вказує на /XRefStm, де знаходяться фактичні дані відновлення.
Чому правильна кількість сторінок нічого не доводить
Оскільки каталог і дерево сторінок навмисно доступні з класичної таблиці, завантажувач, який читає лише цю таблицю, знаходить /Root, обходить дерево сторінок і повідомляє правильну кількість сторінок. Усе, що потрібно старому читачеві, присутнє, тому файл виглядає справним. Об'єкти, які було втрачено, - це ті, що упаковані в потоки об'єктів: словники полів AcroForm, елементи структури тегованого PDF, довгий хвіст малих словників, які ніколи не мали бути видимими для застарілого переглядача.
Ви не помічаєте цієї прогалини, доки щось не торкнеться цих об'єктів, а повне повторне збереження торкається їх усіх. Обхід документа для повторного шифрування або перезапису - це саме та операція, яка запитує кожен номер об'єкта по черзі, тому симптом виявляється під час збереження, а не завантаження, далеко від своєї причини.
Пастка полягає в детекторі, який бачить xref і зупиняється
Простий спосіб визначити, як індексується файл, полягає в тому, щоб перейти за посиланням startxref і перевірити перші байти, на які воно вказує. Ключове слово xref означає класичну таблицю; об'єкт потоку означає потік перехресних посилань. Цей тест є правильним для будь-якого файлу, який використовує лише одну схему. Він є помилковим для гібридного файлу, у якому startxref вказує на класичну секцію виключно для задоволення старих читачів, тоді як /XRefStm у трейлері цієї секції - це місце, де насправді індексується більша часть документа. Детектор, який повертає "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;
З детектором раннього виходу завантаження виглядає нормально, а повторне збереження - це момент, коли про себе заявляють відсутні об'єкти. Виправлення полягає не в тому, щоб читати більше байтів на початку; воно полягає в тому, щоб розпізнати гібридний трейлер і перейти за посиланням /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; гібридний файл показує коротку класичну секцію, словник трейлера якої містить /XRefStm. Або порівняйте кількість об'єктів, про яку повідомляє повний аналіз, із найбільшим номером об'єкта, який вказує параметр /Size у трейлері. Велика різниця означає, що об'єкти ховаються в потоках, які завантажувач не відкрив, а це саме той дефіцит, який пізніше перетворюється на помилку під час збереження.
Сторона записувача в цій історії, тобто те, як взагалі створюються потоки об'єктів і стиснуті перехресні посилання, описана в нашій статті про потоки об'єктів та інкрементні оновлення. Якщо аналізований гібридний файл також є дуже великим, методи завантаження з інструкції з Direct File API для великих PDF-файлів дозволять вам досліджувати його без зчитування всього вмісту в пам'ять. Обидва підходи природно поєднуються з описаним тут відновленням, яке постачається як частина компонента HotPDF для Delphi та C++Builder alongside the loading, editing, encryption, and signing APIs covered elsewhere on this blog.