Technical Article

Обработка на големи PDF файлове в Delphi с HotPDF Direct File API

Преброяването на страниците в сканиран архив с размер 1,4 GB би трябвало да бъде евтина операция. Извикването на LoadFromFile за такъв файл обаче променя това: HotPDF анализира данните за кръстосани препратки (cross-reference data) и изгражда обект в паметта за всеки един от няколкото стотици хиляди косвени обекта в документа, като 32-битов процес достига лимита на адресното пространство от 2 GB някъде по средата на този анализ. Операцията, която искате (броят на страниците), всъщност не се нуждае от нито един от тези обекти. Нуждае се единствено от дървото на страниците (page tree). Тази разлика между това, което дадена задача изисква, и това, което пълното зареждане предоставя, е причината за съществуването на Direct File API.

Direct File API предоставя на Delphi и C++Builder достъп до PDF на ниво файл: брой страници, копиране, дешифриране, инкрементално добавяне, всичко това чрез четене от диска само на необходимото, вместо да се възстановява целият модел на документа в RAM паметта. Умението се състои в това да съпоставите всяка задача с най-лекото ниво (tier), което може да я изпълни. Направете това правилно и вашата услуга ще поддържа стабилна консумация на памет независимо от размера на входящите данни. Сгрешете и първият извънгабаритен файл ще срине работния процес.

Какво ви струва пълното зареждане

LoadFromFile не е враг. То оправдава паметта си: след като дървото е в RAM паметта, имате произволен достъп (random access) до всяка страница и всеки обект, което е точно това, което изискват InsertPagesFromDocument, MovePage и ресериализацията чрез SaveLoadedDocument. Няма съкратен път за истинско реструктуриране: трябва да заредите целия документ, за да го пренаредите.

Проблемите започват, когато нямате контрол над размера на входящите данни. Качванията от клиенти, резултатите от скенери и архивите отпреди десетилетие не се съобразяват с предположенията на тестовия ви набор. Ако зареждате всяка входяща информация безусловно, лимитът на паметта ви ще се определя от най-големия файл, който някой някога ще изпрати. Времето за анализ следва броя на обектите, а заетата RAM памет се установява на стойност, няколко пъти по-голяма от размера на файла след отчитане на структурите на обектите и декодираните потоци, така че един гигабайт на диска може да означава няколко гигабайта в паметта.

Рекомпилирането за 64-битова архитектура премахва ограничението на адресното пространство, но запазва разхода на ресурси. Работният процес все още изразходва секунди процесорно време и памет, многократно надвишаваща размера на файла, за да отговори на въпрос, на който собствената структура на файла би могла да отговори за милисекунди. При паралелна обработка сметката става неблагоприятна: четири големи зареждания, изпълнявани едновременно, споделят един бюджет от памет и производителността се срива точно когато опашката е най-дълга.

Четене на файл чрез манипулатор

Нивото за четене (read-only tier) отваря файл като манипулатор (handle), отговаря на структурни въпроси за него и го затваря. Без дърво на обектите, без рендериране на страници и без разход на памет, която расте с размера на входящите данни.

var
  Pdf: THotPDF;
  Handle, PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Handle := Pdf.DAOpenFileReadOnly('archive-2026-06.pdf', '');
    if Handle > 0 then
    try
      PageCount := Pdf.DAGetPageCount(Handle);
      RouteByPageCount('archive-2026-06.pdf', PageCount);
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;

Три навика помагат да поддържате това ниво ефективно. Първо, проверявайте върнатата стойност. Неположителен манипулатор означава, че отварянето е неуспешно, а извикването на DAGetPageCount с невалиден манипулатор е вид грешка, която остава скрита до деня, в който клиент изпрати повреден файл. Второ, свързвайте всяко успешно отваряне с DACloseFile в блок finally. Услуга, която изпуска манипулатори (leaks handles), не се срива веднага, а постепенно губи ресурси, което е по-лошо. Трето, разберете какво точно прави параметърът за парола. DAOpenFileReadOnly приема парола, но при шифрирани данни той тихомълком преминава към пълен анализ за определяне на броя страници, така че гаранцията за стабилна памет изчезва. Пренасочвайте защитените файлове първо през DecryptFile, за да запазите останалата част от процеса икономична.

Същата проверка служи и за филтриране при постъпване. Файловете често пристигат с грешни разширения, полузаредени или преименувани от съвсем друг формат, а проверката с DAOpenFileReadOnly отхвърля всички тях на входа за милисекунди, свързвайки грешката с конкретния файл. Алтернативата е да оставите повреден файл да навлезе дълбоко в опашката и да се срине там, където разплитането на причината може да ви коства цял следобед.

Копиране, дешифриране и шифриране на цели файлове

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

// Structural copy: validate-and-move without parsing the object tree
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Decrypt while copying: the Direct File route into protected inputs
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Encrypt while copying: protect an output without a full load
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

Всяко извикване има своята роля. DACopyFile е валидираното копие от карантинна директория в управлявано хранилище: то отваря и индексира PDF структурата в движение, така че непълен или не-PDF файл се проваля още тук, а не три етапа по-нататък по веригата. DecryptFile записва дешифрирано копие по директен път на пренаписване с AES-256, който заобикаля дървото на обектите, когато входящите данни го позволяват, това е еквивалентът за големи файлове на процеса по зареждане и повторно записване, описан в статията за AES-256 шифриране. EncryptFile изпълнява същата операция в обратен ред, като прилага защита с парола по време на копиране на ниво файл с параметрите за тип ключ и нива на достъп, използвани от пътя в паметта.

Добавяне на промени вместо пренаписване

Инкременталното актуализиране, дефинирано в ISO 32000-1 §7.5.6, е третото ниво. Оригиналните байтове остават на диска, а всички нови или променени обекти се добавят след тях, последвани от нова секция за кръстосани препратки, която се свързва с оригинала. За архив от 900 MB, към който трябва да се добави една страница, цената на запис е само разликата (delta), а не целият файл.

// Append an audit page to a large archive without rewriting it
Pdf.BeginIncrementalUpdate('archive-2026-06.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Processed by intake service 2026-06-11');
Pdf.SaveIncrementalUpdate('archive-2026-06-stamped.pdf');  // original bytes + delta

Тук са важни две правила. BeginIncrementalUpdate трябва да сочи към оригиналния файл, тъй като добавените данни за кръстосани препратки се свързват с байтови отмествания вътре в него. Освен това моделът е проектиран само за добавяне (append-only): всяко инкрементално записване увеличава файла, но никога не го свива. Документ, който се подпечатва всяка вечер, ще се разраства неограничено, докато периодична ресериализация â€?зареждането му и записването му обратно чрез SaveLoadedDocument â€?не го компресира. Същото свойство прави инкременталното актуализиране единствения безопасен начин за промяна на цифрово подписан документ â€?ограничение, разгледано в статията за цифрови подписи и PAdES. Базовата система за кръстосани препратки е разгледана в статията за потоци от обекти и инкрементални актуализации.

Има капан в записите от тип „самÐ?добавянеâ€? който убягва на повечето прегледи. Оригиналните байтове остават във файла, достъпни за всеки, който реши да потърси. Инкрементална актуализация, която „заменяâ€?страница, всъщност не изтрива старата. Тя я заменя в текущата версия, докато предишната версия остава там, напълно възстановима. Ето защо инкременталните актуализации са грешният инструмент за премахване на чувствително съдържание. За да премахнете напълно историята, която получателят не трябва да вижда, ви е необходима пълна ресериализация: LoadFromFile, последвано от SaveLoadedDocument, което записва само текущото състояние и оставя скритите версии зад себе си.

Съпоставяне на нивото с операцията

Логиката на избор е достатъчно кратка, за да я помните, и е полезно да я кодирате като изрично решение за маршрутизация в началото на вашия процес, вместо да оставяте всяка задача да импровизира свой собствен път. Нужната операция определя нивото:

  • Преброяване, инспекция или класификация отваря манипулатор: DAOpenFileReadOnly, DAGetPageCount, DACloseFile.
  • Преместване, дешифриране или шифриране на цял файл остава на ниво файл с DACopyFile, DecryptFile или EncryptFile.
  • Реструктуриране на страници или сливане на документи изисква пълно зареждане: LoadFromFile, след това InsertPagesFromDocument или MovePage, и накрая SaveLoadedDocument.
  • Добавяне на малка разлика към огромен или подписан файл извиква BeginIncrementalUpdate и го записва.

Смесените работни процеси се възползват добре от поставянето на праг за размер преди пътя на пълно зареждане. Изпращайте всичко над няколкостотин мегабайта през нивата на Direct File и резервирайте пълното зареждане за същинско реструктуриране на 64-битов процес с бюджет за памет. Този праг преобразува срива поради недостиг на памет в контролируемо решение за маршрутизация.

Което и ниво да обработва задачата, записвайте резултата под временно име и го преименувайте на крайното място едва след като се валидира. Наполовина записан файл, намиращ се под крайното име, изглежда точно като валиден за следващия етап от процеса, а извикванията на Direct File правят тази проверка изключително икономична: потвърждаването на изходния резултат е проверка на манипулатора с един ред код.

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