Откройте 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, поэтому программа чтения версии 1.4 просто пропускает их и никогда не сталкивается со структурой, которую не может проанализировать. Их реальное положение содержится только в потоке перекрестных ссылок. Сигнатурой такого файла является его окончание: короткий классический раздел, часто состоящий лишь из xref с заголовком подраздела 0 0, трейлер которого указывает на /XRefStm, где находятся фактические данные восстановления
Почему правильное количество страниц ничего не доказывает
Поскольку каталог и дерево страниц намеренно доступны из классической таблицы, загрузчик, считывающий только эту таблицу, находит /Root, обходит дерево страниц и сообщает правильное количество страниц. Все необходимое для старой программы чтения присутствует, поэтому файл кажется исправным. Отсутствуют объекты, упакованные в потоки объектов: словари полей AcroForm, элементы структуры тегированного PDF, а также длинный хвост небольших словарей, которые не требовались устаревшим средствам просмотра
Вы не заметите отсутствия этих объектов до тех пор, пока что-то не обратится к ним, а полное повторное сохранение затрагивает их все. Обход документа для повторного шифрования или перезаписи требует поочередного запроса номера каждого объекта, поэтому симптом проявляется при сохранении, а не при загрузке, далеко от вызвавшей его причины
Ловушка кроется в детекторе, который останавливается при обнаружении xref
Простой способ определить тип индексации файла состоит в том, чтобы перейти по адресу startxref и проверить первые байты, на которые он указывает. Ключевое слово xref означает классическую таблицу, а объект потока указывает на поток перекрестных ссылок. Этот тест верен для любого файла, использующего только одну схему. Он ошибочен для гибридного файла, чей параметр startxref указывает на классический раздел исключительно для совместимости со старыми программами чтения, тогда как /XRefStm в трейлере этого раздела указывает на место фактической индексации большей части документа. Детектор, возвращающий значение классической таблицы при первой встрече с 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]);
Деталь, которую важно учитывать, находится на более раннем этапе работы. Файлы, поступающие из Word, Excel, PowerPoint и множества других инструментов с функцией сохранения в PDF, часто являются гибридными, поэтому загрузчик, тестируемый только на файлах собственного генератора, может никогда не столкнуться с ними. Добавляйте в тестовые наборы документы, экспортированные из реальных офисных приложений, а не только созданные вашим собственным кодом
Проверка подозрительного файла
Два простых действия помогут быстро прояснить этот вопрос. Откройте файл в шестнадцатеричном редакторе и прочитайте байты после последнего startxref: гибридный файл покажет короткий классический раздел, словарь трейлера которого содержит параметр /XRefStm. Также можно сравнить количество объектов при полном анализе с наибольшим номером объекта, указанным в /Size в трейлере. Большая разница означает, что объекты скрыты в потоках, которые загрузчик не открывал, что впоследствии приведет к сбою при сохранении
Сторона записи, включая процесс создания потоков объектов и сжатых перекрестных ссылок, описана в нашей статье о потоках объектов и инкрементных обновлениях. Если гибридный файл оказывается очень большим, методы загрузки из руководства по Direct File API для работы с большими PDF позволят вам проверить его без считывания всего содержимого в память. Оба подхода дополняют описанную здесь процедуру восстановления, которая поставляется как часть компонента HotPDF для Delphi и C++Builder вместе с API для загрузки, редактирования, шифрования и подписи документов