Technical Article

Обединяване и разделяне на гигабайтови PDF файлове в Delphi с директен достъп от PDFlibPas

Обединяването или разделянето на двугигабайтов PDF файл по стандартния начин води до два основни проблема: загуба на време и препълване на адресното пространство. Стандартният начин включва зареждане на всеки входящ файл, обработка и записване на резултата. Процесът се проваля именно при зареждането. Скенер с архив, който преминава от 300 на 600 DPI, удвоява своята разделителна способност и се увеличава около четири пъти на диска. Поради тази причина една и съща задача за сглобяване, която през годината е обработвала 400 MB файлове, започва да претоварва системата веднага щом входящият файл надхвърли един гигабайт, често дори само при преброяване на страниците. Самата задача не е сложна: отваряне, преброяване, избор на диапазони и съединяване. Пълното зареждане на обектното дърво обаче вече не е разумно решение при такъв обем. PDFlibPas, PDF библиотеката на losLab за Delphi и C++Builder, се справя с това слоя за директен достъп (Direct Access): фамилия от функции с префикс DA, поддържани от стрийминг четец, който обхожда таблицата с препратки (xref) на място, вместо да изгражда целия документ в паметта.

Къде отива паметта при пълно зареждане

Нормалното зареждане на PDF означава анализиране на таблицата xref, преобразуване на всеки косвен обект в дървовидна структура в паметта, декодиране на обекти и свързване на страници, шрифтове и анотации в обекти за манипулиране. За процеси на редактиране това е правилното решение. За обединяване, разделяне и проверка обаче това е чиста загуба. Сканиран архив от 30 000 страници може да съдържа милиони косвени обекти, докато задача за разделяне изисква четене само на няколко стотин от тях: възлите на страниците в избрания диапазон и обектите, към които те препращат.

Слоят за директен достъп обръща този модел. DAOpenFile и DAOpenFileReadOnly анализират края на файла (trailer) и таблицата xref (няколко килобайта в края на файла) и връщат дескриптор на файл. Обектите се извличат при поискване (lazy fetch), когато дадено извикване се нуждае от тях. Практическият резултат е, че отварянето на мултигигабайтов файл отнема толкова време, колкото и на малък файл, а консумацията на памет зависи от обектите, които използвате, а не от целия обем на файла.

Проверка на голям файл без зареждане

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

var

  Lib: TPDFlib;

  Handle, Pages: Integer;

begin

  Lib := TPDFlib.Create;

  try

    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');

    if Handle = 0 then

      raise Exception.Create('Direct access open failed');

    Pages := Lib.DAGetPageCount(Handle);

    Writeln('pages : ', Pages);

    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));

    Lib.DACloseFile(Handle);

  finally

    Lib.Free;

  end;

end;

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

PageRef е дескриптор на обект, а не номер на страница

Подаването на самото число 250 вместо дескриптор няма да предизвика грешка. То ще адресира обекта, който случайно съответства на тази стойност на дескриптора. При късмет това ще се провали явно, но в лошия случай ще извлече текст от грешна страница в документ, предназначен за клиенти. Ако обвивате слоя DA в собствена услуга, направете това преобразуване задължително: приемайте номера на страници на входа на API, извиквайте веднага DAFindPage и използвайте само референции (refs) във вътрешния код.

PageRef := Lib.DAFindPage(Handle, 250);          // page number -> object handle

if PageRef <> 0 then

begin

  Text := Lib.DAExtractPageText(Handle, PageRef, 0);

  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');

end;

Обединяване на стотици файлове чрез именован списък

За два файла функцията MergeFiles(First, Second, Output) е достатъчна. Пакетното сглобяване обаче се мащабира по-добре чрез списъци с файлове: регистрирате входящите файлове под определено име на списък и след това ги обединявате с едно извикване.

Lib.AddToFileList('Statements', 'jan.pdf');

Lib.AddToFileList('Statements', 'feb.pdf');

Lib.AddToFileList('Statements', 'mar.pdf');

Lib.MergeFileList('Statements', 'q1-statements.pdf');



// Verify the result the cheap way: direct access again

Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');

Writeln('merged pages: ', Lib.DAGetPageCount(Handle));

Lib.DACloseFile(Handle);

Фамилията за обединяване съдържа три варианта, като разликата не е само в скоростта. MergeFileListFast пропуска запазването на дървото на структурата; MergeFileListStrict налага строг режим; версията без суфикс е балансираният вариант по подразбиране. Оперативното правило е: ако някой от входящите документи е Tagged PDF (маркиран PDF), чиято структура за достъпност трябва да се запази (какъвто е случаят с PDF/UA), използвайте стандартния или Strict вариант, тъй като Fast безшумно премахва дървото на структурата. За обикновени сканирани архиви без маркиране, Fast предоставя безплатна производителност. Вземайте това решение според вида на процеса, а не по усмотрение на разработчика, и записвайте използвания вариант в хронологията на задачата.

Разделяне без зареждане: извличане на диапазони

Разделянето следва същата философия без зареждане. Функцията ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) прехвърля диапазон от страници директно от файл във файл чрез списък с диапазони като '1-500', '501-1000' или селекции, разделени със запетаи. Източникът никога не се превръща в обектно дърво в паметта. Когато документът вече е зареден по други причини, ExtractPageRanges създава нов документ в паметта от текущия, а CopyPageRanges копира диапазони от друг зареден документ по ID. За разделяне на големи печатни потоци на отделни отчети, формата от файл към файл е този, който предотвратява зареждането на 4 GB входящ файл в оперативната памет.

Файлове с несъответстваща структура

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

Първо: изместени заглавни части (headers). Имейл шлюзове и опашки за печат понякога добавят байтове преди PDF съдържанието, така че маркерът %PDF вече не се намира на отместване 0 и всяко отместване на xref във файла е грешно с еднаква стойност. Стрийминг четецът открива това и го докладва (чрез DAShiftedHeader в плоския API или ShiftedHeader в TSmartPDFReader), като автоматично компенсира отместването при четене. Собствено разработените изчисления на отмествания обикновено не предвиждат това, което води до класическия симптом: "кодът работи с всеки файл, който ние генерираме, но се проваля с файлове от клиент Х".

Второ: повредени таблици с препратки (xref). Функцията DACopyFile(InputFileName, OutputFileName, PageCount) предава целия файл към ново копие, докато изгражда наново таблицата xref, като връща броя страници като страничен продукт. Изпълнението на тази стъпка като фаза на нормализиране преди предаване на данни към по-строг софтуер превръща периодичните грешки при анализ в една предвидима стъпка за ремонт. И когато трябва да запишете вашите редакции, функцията DAAppendFile ги записва като инкрементална актуализация, добавяйки нова ревизия, вместо да презаписва гигабайти, което поддържа времето за запис пропорционално на промяната, а не на общия размер на файла.

Детайли за доставка: линеаризация и композиране

Две допълнителни възможности допълват процеса за обработка на големи файлове. Когато сглобеният резултат се хоства през HTTP за преглед в браузър, функцията LinearizeFile реорганизира документа за стрийминг по диапазони от байтове (byte-range), така че първата страница да се покаже преди приключване на изтеглянето на целия 500 MB файл. Изпълнявайте това като финална стъпка, след всяко обединяване, тъй като всяка последваща модификация де-линеаризира файла отново. И когато се нуждаете от композиране на документи, а не от просто съединяване (например поставяне на заглавна страница зад всеки отчет или комбиниране на две изходни страници в един лист), DACapturePage превръща всяка страница в шаблон за многократна употреба, който DADrawCapturedPage поставя върху целева страница в произволен правоъгълник, без да се налага пълно зареждане на мултигигабайтовия източник.

Ограничения и какво остава само за четене

Самият файлов формат се сблъсква с ограничения много преди Direct Access слоя. Отместванията са от тип Int64 в целия слой DA, така че реалните ограничения са наличното дисково пространство и 10-цифреното поле за отместване на xref в класическите таблици с препратки (които не използват потоци). Мултигигабайтовите сканирани архиви са обичайна практика в реалността, а консумацията на памет остава ограничена независимо от размера на файла, тъй като обектите се четат само при повикване.

Два въпроса възникват достатъчно често, за да получат директен отговор. Обединяването по стандартния път запазва структурата на документа, така че отметките и връзките оцеляват; Fast вариантът е този, който премахва дървото на структурата в полза на скоростта, което е и причината да го ограничите до немаркирани документи. Добра практика е да отворите обединения файл, да проверите структурата на отметките и някои вътрешни връзки, преди да го изпратите. Що се отнася до редактирането: съществува полезен баланс между проверката само за четене и пълното зареждане. Операции на ниво страница работят директно с дескриптора (като DARotatePage, DAMovePage, DAHidePage), заедно с четенето на полета на формуляри, а DAAppendFile записва тези редакции като инкрементална ревизия. Редактиране на ниво съдържание (всичко, което презаписва графичните оператори в страницата) все още изисква пълния слой за документи.

Свързани статии

Ако вашият обединен документ трябва да остане достъпен, концепцията за дърво на структурата е разгледана в статията за достъпност на Tagged PDF, която обяснява какво точно би премахнал Fast вариантът на обединяване. За извличане на съдържание от разделени диапазони вижте ръководството за извличане на текст, изображения и шрифтове.

Пълният списък с функции за директен достъп се разпространява с библиотеката; версии и версии за тестване можете да намерите на продуктовата страница на PDFlibPas.