Технічна стаття

Ручне редагування PDF у текстовому редакторі з подальшим відновленням

HotPDF може зберегти завантажений документ у діагностичній формі, призначеній для читання й редагування людиною, а потім відновити файл після того, як ви його відредагували. SaveLoadedDocumentDiagnostic записує документ із детермінованим порядком ключів словника, кожен об'єкт як звичайний непрямий об'єкт, без потоків об'єктів, без потоку перехресних посилань і без лінеаризації. Потім RepairDiagnosticDocument перебудовує зсуви байтів, довжини потоків, таблицю перехресних посилань і трейлер, які ваше редагування зробило недійсними

Ця пара методів існує заради одного робочого процесу: розуміння або виправлення того, що насправді міститься в PDF. Кожен розробник, який налагоджував проблему рендерингу, хотів відкрити файл, змінити одне число в одному словнику й подивитися на результат. У звичайному PDF це неможливо, адже вміст стиснутий у потоки об'єктів, і кожен вставлений байт зсуває зсуви, які таблиця перехресних посилань записує точно

Чим діагностичне збереження відрізняється?

Шість налаштувань змінюються одночасно. Ключі словника записуються в детермінованому порядку, тож два збереження одного документа дають порівнянні файли. Діагностичний режим виводу анотує структуру. Примусово застосовується повний перезапис, тож нічого не переноситься як недоторкана прирощена (incremental) редакція. Лінеаризацію вимкнено, адже лінеаризований файл організований для веб-переглядача, а не для читача. Потоки перехресних посилань вимкнено, тож таблиця — звичайний текст. І потоки об'єктів вимкнено, тож жоден об'єкт не прихований усередині стиснутого контейнера

Результат — файл, заголовок якого несе діагностичну позначку поряд зі звичайним підписом %PDF-, і чиє тіло можна погортати в будь-якому редакторі. Об'єкти йдуть один за одним, словники читабельні, а потоки вмісту розташовані там, де їх можна знайти. Інструмент відновлення перевіряє наявність цієї позначки й відхиляє файли без неї, адже компактну структуру звичайного PDF інструмент відновлення за діапазонами байтів не повинен намагатися інтерпретувати

Схема, що порівнює звичайне виробниче збереження PDF із діагностичним збереженням HotPDF, яке записує прості читабельні об'єкти й текстову таблицю xref, що відкривається в будь-якому текстовому редакторі
Звичайне збереження приховує об'єкти всередині стиснутих потоків, тоді як діагностичний засіб запису розкладає кожен об'єкт окремим блоком із текстовою таблицею перехресних посилань — результат можна погортати й змінити в будь-якому редакторі
uses
  HPDFDoc;

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice.pdf') <= 0 then
      Exit;
    // Читабельно, один об'єкт на блок, нічого не приховано в потоках об'єктів
    Pdf.SaveLoadedDocumentDiagnostic('invoice-diagnostic.pdf');
  finally
    Pdf.Free;
  end;
end;

Чому редагування ламає файл і що потрібно перебудувати?

У PDF три елементи кодують позиції байтів або кількість байтів. Таблиця перехресних посилань зіставляє кожен номер об'єкта зі зсувом у байтах. Значення startxref наприкінці файлу вказує на цю таблицю. І кожен словник потоку несе /Length, що вказує, скільки байтів займає тіло потоку

Змініть один символ будь-де до кінця файлу — і всі зсуви після нього стануть неправильними. Додайте рядок у потік вмісту — і його /Length теж стане неправильним. Більшість переглядачів відмовляться відкривати такий файл, а ті, що все ж відновлюють його, роблять це шляхом сканування, що приховує помилку, а не виправляє її. Відновлення перебудовує всі три елементи на основі того, що байти насправді містять зараз

Схема, що показує, як ручне редагування зсуває зсуви байтів, через що таблиця перехресних посилань, вказівник startxref і довжини потоків застарівають, доки RepairDiagnosticDocument не перебудує всі три на основі фактичної структури
Додавання байтів зсуває кожен наступний зсув, тож зафіксовані позиції, вказівник startxref і заявлені довжини потоків перестають відповідати дійсності. Відновлення перебудовує кожен із них на основі того, що відредагований файл насправді містить зараз
var
  Options: THPDFDiagnosticRepairOptions;
  Report: THPDFDiagnosticRepairReport;
begin
  Options := THPDFDiagnosticRepairOptions.Default;
  Options.VerifyOutput := True;         // завантажити результат перед заміною

  if THotPDF.RepairDiagnosticDocument('invoice-diagnostic.pdf',
       'invoice-fixed.pdf', Options, Report) then
    Writeln(Format('%d objects, %d streams, %d lengths corrected, ' +
      '%d xref rows rebuilt, %d pages verified',
      [Report.ObjectCount, Report.StreamCount,
       Report.RepairedStreamLengthCount, Report.RebuiltXRefEntryCount,
       Report.VerifiedPageCount]))
  else
    Writeln(Format('repair failed (%d): %s',
      [Ord(Report.Status), string(Report.Diagnostic)]));
end;

Копіювати байти, а не перевиконувати серіалізацію

Архітектурне рішення, що робить цей інструмент надійним, полягає в тому, що він ніколи не завантажує документ і не записує його заново. Він копіює діапазони байтів об'єктів, які перевірив, змінює лише неправильні значення /Length і перебудовує навколо них таблицю, трейлер і startxref

Завантаження й повторна серіалізація реалізувалися б значно простіше, але непомітно змінювали б вміст. Невідомий синтаксис об'єктів був би нормалізований, екранування рядків переписане в бажаній формі засобу запису, а вже закодовані корисні дані потоків могли б бути перекодовані. Для інструмента налагодження це неприйнятно: ви змінили одне число, і різниця (diff) має показувати одне число, а не тисячу випадкових змін форматування. Якщо потрібно точно знати, що змінилося між двома файлами, це вже завдання порівняння, описане в статті порівняння двох PDF-файлів за структурою та пікселями

Схема, що порівнює відновлення HotPDF, яке копіює перевірені діапазони байтів дослівно, зі шляхом завантаження й повторної серіалізації, що нормалізує синтаксис, переписує екранування рядків і перекодовує корисні дані потоків
Відновлення копіює перевірені діапазони байт-у-байт і виправляє лише неправильні значення /Length, тож ваша рукописна різниця (diff) залишається незмінною. Повторне завантаження й серіалізація непомітно переписали б синтаксис, екранування та кодування, яких ви не торкалися

Два правила парсингу, що запобігають хибним об'єктам

Сканер за діапазонами байтів стикається з очевидною пасткою: текст, що виглядає як заголовок об'єкта, може з'явитися всередині корисних даних потоку або всередині рядка. Наївне сканування видало б рядок перехресних посилань, що вказує в середину потоку вмісту, і результуючий файл був би зламаний по-новому — і ще заплутанішим чином

HotPDF уникає цього, пропускаючи весь діапазон байтів кожного перевіреного об'єкта, використовуючи його зафіксований кінцевий зсув, тож ніщо всередині тіла об'єкта ніколи не розглядається як кандидат. Між об'єктами дозволені лише пробіли PDF та коментарі; усе інше означає, що ця область не розпізнана, і її байти не повинні потрапляти у вивід

Довжини потоків вимагають не меншої обережності. Реальна довжина корисних даних не включає роздільник кінця рядка, який засіб запису вставляє перед endstream. Якщо заявлена довжина точно потрапляє на маркер, така заява приймається. Якщо ні — інструмент відновлення виконує обмежений пошук пари endstream та endobj і віднімає рівно один роздільник: CR, LF або CRLF. Помилка тут на один байт дає файл, який відкривається й рендерить сторінку з одним зайвим оператором наприкінці потоку вмісту

Що дозволено зберегти в трейлері

Перебудований трейлер — це не копія старого. /Size, /Prev та /XRefStm описують попередню структуру файлу й перераховуються заново або відкидаються. Виживають лише записи, чиї цілі перевірено: /Info, /ID та /Encrypt. А /Root повинен розв'язуватися рівно в один об'єкт Catalog — не в нуль і не в кілька, що зазвичай трапляється у файлі, зібраному копіюванням-вставленням блоків об'єктів

Безпека виводу реалізована так, як і має бути для інструмента, що працює з файлами, які людям небайдужі. Результат записується у тимчасовий файл у цільовому каталозі, потім перевіряється завантаженням через звичайний шлях із вимкненим відновленням перехресних посилань, тож файл, який відкривається лише завдяки відновленню, зараховується як невдача. Лише після того, як дерево сторінок пройде перевірку, тимчасовий файл атомарно замінює цільовий. Невдале відновлення ніколи не перезаписує наявний вихідний файл

Бюджети та чесні коди відмов

Роботу обмежують три межі: кількість вхідних байтів, кількість об'єктів-кандидатів і максимальний номер об'єкта, які за замовчуванням становлять 8 ГіБ, два мільйони та два мільйони. Стеля номера об'єкта важливіша, ніж здається, адже розріджена таблиця з кількома об'єктами, пронумерованими в мільярдах, інакше надзвичайно роздула б перебудовану таблицю перехресних посилань. Класичні таблиці перехресних посилань також відхиляють зсуви, що потребували б понад десяти десяткових цифр — це обмеження формату, а не рішення реалізації

Про збої повідомляється конкретний статус, а не булеве значення: недійсні аргументи, надто великий вихідний файл, файл не є діагностичним, перевищено ліміт об'єктів, недійсний об'єкт, дубльований об'єкт, відсутній каталог, недійсний трейлер, помилка виводу або невдала перевірка. Кожен статус відповідає своєму способу виправлення. Дубльований об'єкт означає, що ви двічі вставили блок об'єкта. Відсутній каталог зазвичай означає, що об'єкт із /Type /Catalog пошкоджено. Невдала перевірка означає, що структура тепер узгоджена, але дерево сторінок не завантажується — зазвичай через масив /Kids, відредагований неузгоджено з його /Count

Дві практичні примітки завершують картину. Зберігайте діагностичний файл, а не лише відновлений, щоб наступне редагування починалося з читабельної основи. І не постачайте діагностичний вивід кінцевим користувачам: він більший за звичайне збереження, адже нічого не стиснуто в потоки об'єктів, а виробничим шляхом для компактного файлу є звичайний шлях збереження, описаний у статті потоки об'єктів та прирощені оновлення. Коли мета — знайти, що не так, а не змінити це, автоматизований шлях у статті автоматизація preflight-звітів дає відповідь швидше, ніж читання байтів

Діагностичне збереження, відновлення, preflight та порівняння працюють з тією самою моделлю завантаженого документа в Delphi та C++Builder; повний список можливостей наведено на сторінці компонента HotPDF Delphi PDF