Spájanie alebo rozdeľovanie dvojgigabajtového PDF bežným spôsobom vás stojí dve veci naraz: čas a adresný priestor. Bežným spôsobom je načítať každý vstup, vykonať prácu a zapísať výstup. Problém nastáva pri načítaní. Archív skenov, ktorý prejde z rozlíšenia 300 na 600 DPI, zdvojnásobí svoje lineárne rozlíšenie a približne zoštvornásobí veľkosť na disku, takže rovnaká operácia spájania, ktorá po celý rok bez problémov spracovávala 400 MB súbory, začne zlyhávať v moemente, keď vstup prekročí jeden gigabajt, často už pri obyčajnom počítaní strán. Úloha pritom nie je zložitejšia – otvoriť, spočítať, vybrať rozsahy a spojiť. Načítanie kompletného objektového stromu skrátka prestalo byť pri tejto veľkosti rozumným predvoleným nastavením. Knižnica PDFlibPas od losLab pre Delphi a C++Builder to rieši svojou vrstvou priameho prístupu (Direct Access): rodinou funkcií s predponou DA, ktoré sú podporované streamovacou čítačkou prechádzajúcou tabuľku krížových odkazov (cross-reference table) priamo na mieste namiesto budovania celého dokumentu v pamäti.
Kam mizne pamäť pri plnom načítaní
„Normálne“ načítanie PDF znamená analyzovanie xrefu, rozlíšenie každého nepriameho objektu do stromu v pamäti, dekódovanie objektových streamov a prepojenie stromu strán, písiem a anotácií do objektov, s ktorými môžete manipulovať. Pre úpravu dokumentov je to správny prístup. Pre spájanie, rozdeľovanie a inšpekčné práce je to však väčšinou plytvanie. Archív skenov s 30 000 stranami môže obsahovať milióny nepriamych objektov, pričom úloha rozdelenia potrebuje prečítať iba niekoľko stoviek z nich: uzly strán v požadovanom rozsahu a to, na čo tieto uzly odkazujú.
Vrstva priameho prístupu (Direct Access) tento model obracia. Metódy DAOpenFile and DAOpenFileReadOnly analyzujú trailer a xref, čo je len niekoľko kilobajtov na konci súboru, a vrátia popisovač súboru. Objekty sa načítavajú lenivo (lazy loading) vtedy, keď ich volanie reálne potrebuje. Praktickým dôsledkom je, že otvorenie viacgigabajtového súboru trvá približne rovnako dlho ako otvorenie malého súboru a spotreba pamäte závisí od toho, s čím pracujete, nie od celkovej veľkosti súboru.
Skúmanie veľkého súboru bez načítania
Nižšie uvedený vzor pochádza z vlastného testu knižnice pre veľké súbory: otvoriť len na čítanie, získať informácie, zatvoriť. Žiadny objektový strom dokumentu sa nevytvára.
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;
Režim len na čítanie je vhodné uprednostniť všade, kde je to možné: umožňuje spustiť fázu čítania, kým iné procesy držia súbor, a jasne dokumentuje zámer. Fáza skúmania, ktorá by omylom zavolala modifikačnú funkciu, zlyhá okamžite namiesto toho, aby poškodila archív.
PageRef je popisovač objektu, nie číslo stránky
Najčastejšou chybou pri práci s DA API je odovzdanie čísla stránky tam, kde funkcia očakáva PageRef. Takmer každé volanie DA na úrovni stránky vyžaduje referenčný popisovač na objekt stránky, nie jej číslo: DAExtractPageText, DARenderPageToFile, DARotatePage a DACapturePage – všetky očakávajú referenciu. Získate ju prevedením používateľského čísla stránky pomocou metódy DAFindPage:
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;
Odovzdanie samotného čísla 250 namiesto referencie nevyvolá chybu. Zacieli na akýkoľvek objekt, ktorý sa náhodou nachádza pod touto hodnotou popisovača, čo v lepšom prípade zlyhá viditeľnou chybou, no v horšom extrahuje text z nesprávnej stránky do dokumentu smerujúceho k zákazníkovi. Ak obalíte vrstvu DA vo vlastnom kóde služby, urobte tento prevod povinným: prijímajte čísla stránok na vstupe, okamžite zavolajte DAFindPage a interne pracujte výhradne s referenciami.
Spájanie stoviek súborov pomocou pomenovaného zoznamu
Pre dva súbory postačuje metóda MergeFiles(First, Second, Output). Hromadné spájanie sa lepšie škáluje pomocou zoznamov súborov: zaregistrujte vstupy pod názvom zoznamu a potom zoznam spojte v jednom kroku.
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);
Rodina funkcií na spájanie má tri varianty a rozdiel nie je len v rýchlosti. MergeFileListFast vynecháva zachovanie stromu štruktúry; MergeFileListStrict vynucuje prísny režim; verzia bez prípony predstavuje vyváženú predvolenú možnosť. Prevádzkové pravidlo znie: ak je niektorý vstup označený ako Tagged PDF (štruktúrované PDF), ktorého prístupnosť musí zostať zachovaná – čo je zjavný prípad pri výstupoch pre PDF/UA – siahnite po predvolenom variante alebo Strict, pretože variant Fast strom štruktúry potichu zahodí. Pre obyčajné archívy skenov bez tagov predstavuje variant Fast výkon zadarmo. Rozhodujte sa podľa typu spracovania, nie podľa nálady, a použitý variant zaznamenajte do logu úlohy.
Rozdeľovanie bez načítania: extrakcia rozsahov
Rozdeľovanie sa riadi rovnakou filozofiou bez načítania dokumentu. Metóda ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) vytiahne rozsah strán priamo zo súboru do súboru podľa zoznamu rozsahov ako '1-500', '501-1000' alebo čiarkami oddelených hodnôt, pričom zdroj sa nikdy neprevedie na objektový strom dokumentu. Ak je dokument už načítaný z iných dôvodov, ExtractPageRanges vytvorí nový dokument v pamäti z aktuálneho a CopyPageRanges kopíruje rozsahy z iného načítaného dokumentu podľa ID. Pre rozdeľovanie zlúčených tlačových dát je forma zo súboru do súboru tou správnou cestou, ktorá zabráni tomu, aby 4 GB vstup zaplnil operačnú pamäť.
Súbory, ktoré klamú o svojej štruktúre
Systémy spracovávajúce veľké súbory sa stretávajú s poškodenými súbormi oveľa častejšie ako tie, ktoré spracovávajú malé súbory, pretože vstupy prechádzajú cez viacero systémov. Dva druhy zlyhaní vyžadujú výslovné riešenie.
Po prvé, posunuté hlavičky (shifted headers). E-mailové brány a tlačové procesy niekedy predradia pred PDF bajty naviac, takže značka %PDF sa už nenachádza na offsete 0 a každý offset xrefu v súbore je posunutý o rovnakú hodnotu. Streamovacia čítačka to dokáže zistiť a sprístupniť (DAShiftedHeader na plochej úrovni, ShiftedHeader na TSmartPDFReader) a následne to kompenzovať pri čítaní. Vlastnoručne napísaná matematika offsetov to zvyčajne nedokáže, čoho typickým príznakom je stav: „funguje na každom súbore, ktorý vygenerujeme, ale zlyháva na súboroch od zákazníka X“.
Po druhé, poškodené tabuľky krížových odkazov. Metóda DACopyFile(InputFileName, OutputFileName, PageCount) prenáša celý súbor do novej kópie a zároveň prebudováva xref, pričom ako vedľajší produkt vracia počet strán. Jej zaradenie ako normalizačnej fázy pred náročného spracovateľa mení skupinu občasných chýb analýzy na jeden predvídateľný krok opravy. A keď potrebujete uložiť vlastné úpravy, DAAppendFile ich zapíše ako prírastkovú revíziu – pripojí novú revíziu namiesto prepisovania gigabajtov dát, čím udrží náklady na ukladanie úmerné zmene a nie celkovej veľkosti súboru.
Detaily doručenia: linearizácia a kompozícia
Dve súvisiace schopnosti dopĺňajú systém na spracovanie veľkých súborov. Keď sa vytvorený výstup poskytuje cez HTTP na prezeranie v prehliadači, metóda LinearizeFile ho reorganizuje na streamovanie podľa rozsahu bajtov (byte-range streaming), takže prvá strana sa zobrazí skôr, než sa dokončí sťahovanie zvyšku 500 MB balíka. Spustite ju ako finálny krok po každom zlúčení, pretože akákoľvek neskoršia úprava súbor opäť de-linearizuje. A keď balíky vyžadujú kompozíciu a nie iba jednoduché reťazenie – napríklad titulný list umiestnený za každým výpisom alebo umiestnenie dvoch zdrojových strán na jeden výstupný list – metóda DACapturePage premení akúkoľvek stranu na opakovane použiteľnú šablónu, ktorú DADrawCapturedPage umiestni na cieľovú stranu do ľubovoľného obdĺžnika, a to stále bez plného načítania viacgigabajtového zdroja.
Limity a čo zostáva len na čítanie
Samotný formát PDF narazí na svoje limity oveľa skôr ako Direct Access. Offsety sú typu Int64 v celej vrstve DA, takže skutočnými stropmi sú dostupný disk a 10-miestne pole offsetu xref klasických tabuliek krížových odkazov. Viacgigabajtové archívy skenov sú v praxi bežné a pamäť zostáva ohraničená bez ohľadu na veľkosť súboru, pretože objekty sa načítavajú len vtedy, keď si ich vyžiada konkrétne volanie.
Dve otázky sa objavujú dosť často na to, aby sme na ne odpovedali priamo. Spájanie cez predvolenú cestu prenáša aj štruktúru dokumentu, takže záložky a odkazy zostanú zachované; variant Fast je ten, ktorý vymieňa strom štruktúry za rýchlosť, čo je dôvodom, prečo by sa mal používať iba na neštruktúrované vstupy. Bezpečným zvykom je otvoriť zlúčený výstup, prejsť jeho osnovu a pred odoslaním náhodne skontrolovať niekoľko interných odkazov. Čo sa týka úprav: existuje užitočný stred medzi skúmaním len na čítanie a plným načítaním. Operácie na úrovni stránky pracujú priamo s popisovačom, okrem iného DARotatePage, DAMovePage a DAHidePage, spoločne s čítaním formulárových polí, a DAAppendFile ukladá tieto úpravy ako prírastkovú revíziu. Úpravy na úrovni obsahu, teda čokoľvek, čo prepisuje vykresľovacie operátory vo vnútri stránky, stále patria do plnej vrstvy dokumentu.
Súvisiace články
Ak musí váš zlúčený výstup zostať prístupný, problematika stromu štruktúry je popísaná v článku o prístupnosti Tagged PDF, ktorý vysvetľuje, čo presne by variant Fast pri zlúčení zahodil. Informácie o získavaní obsahu z rozdelených častí nájdete v sprievodcovi extrakciou textu, obrázkov a písiem.
Kompletný zoznam funkcií Direct Access sa dodáva s knižnicou; verzie a stiahnutia sú k dispozícii na produktovej stránke PDFlibPas.