Technical Article

Потоци от обекти и инкрементални актуализации в Delphi с HotPDF

PDF 1.5 въведе две структури за съхранение, които по-ранният файлов формат не можеше да изрази: поток от обекти (object stream) и поток от кръстосани препратки (cross-reference stream). Потокът от обекти е единичен Flate-компресиран контейнер, маркиран с /Type /ObjStm, който съдържа много малки косвени обекти, пакетирани един след друг, вместо да ги разпръсква в тялото на файла. Потокът от кръстосани препратки представлява справочната таблица на файла, пренаписана като компресиран двоичен код с полета с променлива ширина, замествайки ASCII таблицата с фиксирана ширина, която завършваше всеки PDF документ до версия 1.4. Те се движат заедно. След като обектите се сгънат в поток, старата текстова таблица вече не може да ги адресира, така че двоичната xref таблица трябва да върви с тях.

Сравнете това с класическото оформление и спестените ресурси стават лесни за забелязване. Във PDF 1.4 файл всеки косвен обект се намира некомпресиран зад собствено obj заглавие, а таблицата в края заема точно 20 байта ASCII код на запис, като компресирането е забранено. Документ с 200 000 обекта носи около 4 MB данни за кръстосани препратки, преди да бъде изчертан дори един символ, като всички некомпресирани тела на речници са разположени отгоре. PDF 1.5 атакува и двете стойности едновременно: речниците се сгъват във Flate контейнери, а таблицата от 4 MB се свива до няколкостотин килобайта двоичен код. Стандартът ISO 32000-1 дефинира тези две структури в §7.5.7 и §7.5.8.

Къде всъщност се усеща спестяването

Потоците от обекти засягат само обекти, които не са потоци, така че те компресират структурата, а не пикселите. Съдържанието на страниците вече беше Flate-компресирано преди версия 1.5, а данните за изображенията носят свои собствени кодеци, поради което брошура с много изображения почти не се променя по размер. Файловете, които намаляват драстично, са тези с тежка структура: AcroForms с хиляди речници на полета, дълбоки дървовидни структури на раздели (outlines), структурни елементи на маркиран PDF (tagged PDF). Тези обекти са малки, многобройни и почти идентични помежду си, и това повторение е точно това, което Flate използва, след като те попаднат в един буфер, вместо да се разпръснат из тялото с вместени заглавия между тях.

Лесно е да се подцени колко голяма част от стария файл е излишно натоварване (overhead). Архив с формуляри, който е претърпял години редакции, може да изразходва над половината от байтовете си за заглавия на речници, xref запълване и версии, които никой читател никога няма да погледне. Двете характеристики тук възстановяват първите две от тях. Третата, натрупаните версии, се поддава само на компресиране (compaction), когато файлът вече не трябва да помни собствената си история.

В HotPDF активирате и двете опции чрез двойка свойства, като начинът, по който те зависят едно от друго, има по-голямо значение от реда, в който ги записвате:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog-2026.pdf';
    Pdf.UseXRefStream := True;      // binary xref, prerequisite for ObjStm
    Pdf.UseObjectStreams := True;   // pack objects into /Type /ObjStm
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Compressed structure demo');
    Pdf.EndDoc;                     // emits XRefStm + ObjStm containers
  finally
    Pdf.Free;
  end;
end;

Свойството UseObjectStreams изисква UseXRefStream да бъде зададено на True. До компресиран обект се достига чрез xref запис от тип 2, който записва номера на потока от обекти плюс индекс, а класическият 20-байтов текстов ред няма място за съхранение на тази двойка. Така че UseObjectStreams само по себе си не води до видим резултат. Конфигурацията, която работи, изисква и двата флага да бъдат зададени преди BeginDoc. Задайте ги след BeginDoc и HotPDF вече ще се е обвързал с по-старото оформление.

Защо и двете настройки по подразбиране са изключени

HotPDF оставя и двете свойства зададени на False по подразбиране, а причината се крие в съвместимостта със стари системи по-нататък по веригата. Четец, който разбира само PDF 1.4, не съобщава, че не може да обработва компресирани обекти. Той се сблъсква с xref поток, не намира нито една от очакваните ключови думи в края на файла (trailer keywords) и съобщава за повредена таблица с кръстосани препратки или просто отказва да отвори файла. Ако изходният ви поток отива към стара факс система, физически принтер с вграден интерпретатор или парсер, написан по спецификацията 1.4 преди десетилетие, оставете и двата флага изключени за този канал и се примирете с по-големия размер на файла. За архивно съхранение и разпространение в мрежата, където всеки масов четец поддържа PDF 1.5 от двадесет години насам, включването им е компресия, която получавате почти безплатно.

Има и вторичен ефект, за който си струва да информирате екипа си за поддръжка. След като речниците са пакетирани в потоци от обекти, сравняването на два генерирани файла байт по байт губи всякакъв смисъл, тъй като промяната на едно поле може да прекомпресира (re-Flate) целия контейнер и да размести всичко след него. Сравнявайте такива файлове по съдържание на обектите, а не чрез двоично сравнение.

Инкрементални актуализации и байтовите отмествания, които те защитават

Цифровият подпис обхваща изричен диапазон /ByteRange: два интервала от физическия файл, зададени като абсолютни байтови отмествания (byte offsets), върху които е изчислен дайджестът (digest) на CMS. Пренапишете файла, дори в нещо, което изглежда идентично на екрана, и всички тези отмествания ще се преместят. Дайджестът вече не съвпада и подписът се отчита като повреден. Точно този подпис решава стандартът ISO 32000-1 §7.5.6 чрез инкременталните актуализации. Новите и променените обекти се добавят след съществуващия маркер %%EOF, след което се записва нова секция за кръстосани препратки, чийто запис /Prev сочи обратно към предишния. Оригиналните байтове никога не се променят, така че подписаната версия остава проверима и Acrobat може да представи всяка подписана версия поотделно в панела за подписи.

HotPDF предоставя достъп до това чрез своя собствена точка на влизане:

Pdf.BeginIncrementalUpdate('contract-signed.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Addendum recorded 2026-06-11');
Pdf.SaveIncrementalUpdate('contract-updated.pdf');  // appends the delta only

Две неща често затрудняват разработчиците. Методът BeginIncrementalUpdate трябва да получи оригиналното име на файла, тъй като добавената xref секция записва отмествания, които имат смисъл само спрямо тези точни оригинални байтове. Насочете го към преименувано или отново записано копие и отместванията ще описват файл, който вече не съществува. Освен това записът по структура е само за добавяне (append-only), така че резултатът винаги е по-голям от входящия файл. Този растеж не е излишен разход, който трябва да се оптимизира. Това е същото свойство, което запазва предишните подписани версии непокътнати.

Модифицирането на зареден файл става чрез LoadFromFile

Разработчиците, които се запознават с HotPDF за първи път чрез неговия API за генериране, често се сблъскват с конкретна трудност. BeginDoc отваря напълно нов документ, което е грешният инструмент, когато искате да промените вече съществуващ такъв. Редактирането на съществуващ файл се извършва вместо това чрез извиквания за зареден документ:

PageCount := Pdf.LoadFromFile('base.pdf');
Pdf.InsertPagesFromDocument(OtherDoc, '1-3', 5);  // pages 1-3 after page 5
Pdf.MovePage(2, 5);
Pdf.SaveLoadedDocument('modified.pdf');

Смесете двете действия и резултатът ще бъде изходен файл, който съдържа вашето ново съдържание, но нищо от оригинала, тъй като BeginDoc с лекота изгражда нов документ до този, който вярвате, че редактирате. Възприемайте LoadFromFile със SaveLoadedDocument като един набор от команди, а BeginDoc с EndDoc â€?като друг. Рутина, която използва и двата набора срещу един и същ файл, почти винаги е погрешна.

Кога да се компресира добавен файл

Записването тип „самÐ?добавянеâ€?води до постепенно натрупване на обем. Ежедневна задача, която поставя един ред за статус върху един и същ PDF, произвежда 365 версии за една година и всяка версия влачи нова xref секция зад себе си. Когато тази история вече не е полезна и никой подпис във файла не трябва да бъде запазен, можете да премахнете версиите (flatten), като презапишете целия файл през пътя за зареден документ:

Pdf.LoadFromFile('stamped.pdf');
Pdf.SaveLoadedDocument('compacted.pdf');

Това повторно записване е пълно пренаписване. То изрично изхвърля предишните версии и поврежда всеки подпис, останал във файла, така че го поставете зад същите контроли за достъп, които прилагате към всяка друга деструктивна стъпка. Едно производствено правило, което се е доказало: компресирайте, когато броят на версиите премине определен праг или когато добавеното излишно натоварване надхвърли определен дял от основния файл, и никога не компресирайте документ, в чийто панел за подписи има регистрирани данни.

Проверка на резултата преди изпращане

Проверката на тези две функции е съвсем конкретна. Отворете резултата в Adobe Acrobat и потвърдете три неща: свойствата на документа показват PDF 1.5 или по-нова версия, след като потоците от обекти са включени; панелът за подписи все още валидира всяка предишна подписана версия след инкрементално актуализиране; броят на страниците и отметките (bookmarks) са преминали невредими през цикъла на зареждане, промяна и записване. За изходни документи за архивиране изпратете файла и през veraPDF, тъй като компресираната xref таблица е точно типа структура, която строгият валидатор изследва по-внимателно, отколкото всеки по-толерантен четец. Ако работата ви включва и много големи входящи данни, методите за инспекция в нашето ръководство за Direct File API за големи PDF работни процеси се комбинират отлично с инкременталното записване, а механиката на подписите зад байтовите диапазони, споменати по-горе, е разгледана подробно в статията за цифрови подписи и PAdES в HotPDF.

И двете функции се доставят като част от компонента HotPDF за Delphi и C++Builder, заедно с API за генериране, формуляри, шифриране и подписване, разгледани на други места в този блог. Продуктовата страница съдържа връзка към пълния справочник за API, ако искате да съпоставите горните извиквания с вашия собствен тръбопровод за обработка на документи.