Technical Article

Združevanje in razdeljevanje gigabajtnih PDF v Delphiju z neposrednim dostopom PDFlibPas

Združevanje ali razdeljevanje dvogigabajtnega PDF-ja na očiten način vas stane dejavnosti hkrati: časa in naslovnega prostora. Očiten način je, da naložite vsak vhod, izvedete delo in zapišete izhod. Pri nalaganju se zadeva ustavi. Arhiv skeniranih dokumentov, ki se premakne s 300 na 600 DPI, podvoji svojo linearno ločljivost in se približno štirikrat poveča na disku, zato isto opravilo sestavljanja, ki je vse leto obdelovalo 400-megabajtne datoteke, začne pretirano porabljati vire v trenutku, ko vhod preseže gigabajt, pogosto celo pri navadnem štetenju strani. Naloga ni postala nič težja. Odpiranje, štetje, izbira obsegov in združevanje je vse, kar je treba storiti. Nalaganje celotnega objektnega drevesa preprosto ni več primerno pri tej velikosti. PDFlibPas, losLabova knjižnica za PDF za Delphi in C++Builder, na to odgovarja s svojim slojem neposrednega dostopa (Direct Access): družino funkcij s predpono DA, podprto s pretočnim bralnikom, ki prehodno pregleduje navzkrižno tabelo (xref) na mestu, namesto da bi celoten dokument zgradil v pomnilniku.

Kam gre pomnilnik pri polnem nalaganju

Nalaganje PDF-ja "običajno" pomeni razčlenjevanje xref tabele, razreševanje vsakega posrednega objekta v pomnilniško drevo, dekodiranje tokov objektov ter povezovanje drevesa strani, pisav in opomb v objekte, ki jih lahko upravljate. Pri urejanju je to pravilna odločitev. Pri združevanju, razdeljevanju in pregledovanju pa je to večinoma potrata. Arhiv s 30.000 skeniranimi stranmi lahko vsebuje milijone posrednih objektov, opravilo razdeljevanja pa mora prebrati le nekaj sto izmed njih: vozlišča strani v zahtevanem obsegu in tisto, kar ta vozlišča referencirajo.

Sloj neposrednega dostopa obrne ta model. Metodi DAOpenFile in DAOpenFileReadOnly razčlenita zaključni del (trailer) in xref tabelo, nekaj kilobajtov na koncu datoteke, ter vrneta ročaj datoteke. Objekti se pridobivajo lenobno (lazily), ko jih klic potrebuje. Praktična posledica je, da odpiranje večgigabajtne datoteke traja približno toliko časa kot odpiranje majhne, poraba pomnilnika pa sledi tistemu, česar se dotaknete, in ne celotni vsebini datoteke.

Pregledovanje ogromne datoteke brez nalaganja

Spodnji vzorec izhaja iz lastnega preizkusa delovanja knjižnice za velike datoteke: odprite samo za branje, pridobite informacije, zaprite. Objektno drevo dokumenta nikoli ne nastane.

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;

Načinu samo za branje je priporočljivo dati prednost, kadar koli je to mogoče: omogoča izvajanje faze sprejema, medtem ko drugi procesi držijo datoteko, in jasno izraža namen. Faza pregledovanja, ki bi pomotoma poklicala funkcijo za spreminjanje, hitro spodleti, namesto da bi poškodovala arhiv.

PageRef je ročaj objekta in ne številka strani

Najpogostejša napaka pri uporabi API-ja DA je posredovanje številke strani tam, kjer funkcija pričakuje PageRef. Skoraj vsak klic DA na posamezno stran sprejme referenčni ročaj objekta strani in ne številke strani: DAExtractPageText, DARenderPageToFile, DARotatePage in DACapturePage vsi pričakujejo referenco. To dobite s prevajanjem številke strani prek 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;

Posredovanje same številke 250 ne sproži napake. Namesto tega nagovori kateri koli objekt, ki se nahaja za to vrednostjo ročaja, kar ob dobrem dnevu vidno spodleti, ob slabem pa izvleče besedilo z napačne strani v dokument za stranko. Če sloj DA ovijete v lastno storitveno kodo, naredite to pretvorbo neobhodno: na meji sprejmite številke strani, takoj pokličite DAFindPage in interno posredujte le reference.

Združevanje stotin datotek z imenom seznama

Za dve datoteki zadošča MergeFiles(First, Second, Output). Paketno sestavljanje se bolje prilagaja prek seznamov datotek: registrirajte vhode pod imenom seznama in nato združite seznam v enem koraku.

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);

Družina združevanja ima tri različice, razlika pa ni le v hitrosti. MergeFileListFast preskoči ohranjanje drevesa strukture; MergeFileListStrict uveljavlja strogi način; različica brez pripone pa je uravnotežena privzeta možnost. Operativno pravilo: če je kateri koli vhod označeni PDF (Tagged PDF), katerega struktura dostopnosti mora preživeti (kar je pri izdelavi za PDF/UA očiten primer), uporabite privzeto ali Strict različico, saj različica Fast tiho zavrže strukturo. Odločite se na ravni cevovoda in ne po razpoloženju razvijalca ter zabeležite uporabljeno različico v dnevnik opravil.

Razdeljevanje brez nalaganja: izločanje obsegov

Razdeljevanje sledi enaki filozofiji brez nalaganja. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) potegne obseg strani neposredno iz datoteke v datoteko, z uporabo seznama obsega, kot so '1-500', '501-1000' ali z vejico ločene izbire, pri čemer se vir nikoli ne pretvori v objektno drevo. Ko je dokument že naložen iz drugih razlogov, ExtractPageRanges ustvari nov dokument v pomnilniku iz trenutnega, CopyPageRanges pa prenese obsege iz drugega naloženega dokumenta prek ID-ja. Za razdeljevanje obsežnih tiskalnih tokov po izpiskih je oblika iz datoteke v datoteko tista, ki prepreči, da bi se 4 GB vhod kdaj napihnil v RAM.

Datoteke, ki lažejo o svoji geometriji

Cevovodi za velike datoteke se srečujejo s poškodovanimi datotekami v obsegu, ki ga cevovodi za majhne datoteke nikoli ne vidijo, preprosto zato, ker vhodi prehajajo skozi več sistemov. Dve obliki napak si zaslužita eksplicitno obravnavo.

Prvič, premaknjene glave. Poštni prehodi in tiskalniški strežniki včasih pred PDF dodajo bajte, zato se oznaka %PDF ne nahaja več na odmiku 0 in je vsak odmik xref v datoteki napačen za enak znesek. Pretočni bralnik to zazna in izpostavi (DAShiftedHeader na ravni ravnega API, ShiftedHeader na TSmartPDFReader) ter to kompenzira med branjem. Doma narejena aritmetika odmikov tega običajno ne stori, kar je razlog za klasičen simptom "deluje na vsaki datoteki, ki jo ustvarimo, spodleti pa na datotekah stranke X".

Drugič, pokvarjene navzkrižne tabele. Metoda DACopyFile(InputFileName, OutputFileName, PageCount) pretoči celotno datoteko v novo kopijo in hkrati ponovno zgradi xref tabelo, pri čemer kot stranski produkt vrne število strani. Uporaba te metode kot faze normalizacije pred izbirčnim porabnikom pretvori razred občasnih napak pri razčlenjevanju v en predvidljiv korak popravila. Ko pa morajo biti shranjeni vaši lastni popravki, DAAppendFile zapiše te spremembe kot inkrementalno posodobitev, pri čemer doda novo revizijo, namesto da bi ponovno zapisoval gigabajte, kar ohranja stroške shranjevanja sorazmerne s spremembo in ne z velikostjo datoteke.

Podrobnosti o dostavi: linearizacija in kompozicija

Dve sorodne zmogljivosti dopolnjujeta cevovod za velike datoteke. Ko se sestavljeni izhod ponuja prek HTTP za ogled v brskalniku, LinearizeFile reorganizira datoteko za pretakanje po delih (byte-range streaming), tako da se prva stran prikaže, še preden se zaključi prenos preostalega dela 500-megabajtnega paketa. Zaženite jo kot zadnjo fazo po vsem združevanju, saj vsaka kasnejša sprememba ponovno odstrani linearizacijo. Ko pa paketi potrebujejo sestavo namesto preprostega združevanja (na primer naslovnica, odtisnjena za vsakim izpiskom, ali dve izvorni strani, nameščeni na en izhodni list), DACapturePage pretvori katero koli stran v predlogo za ponovno uporabo, ki jo DADrawCapturedPage postavi na ciljno stran na poljuben pravokotnik, še vedno brez polnega nalaganja dokumenta na večgigabajtnem viru.

Omejitve in kaj ostaja samo za branje

Format sam ostane brez prostora veliko prej kot Direct Access. Odmiki so tipa Int64 skozi celoten sloj DA, zato so resnične meje razpoložljiv disk in 10-mestno polje odmika xref v klasičnih (nepretočnih) navzkrižnih tabelah. Večgigabajtni arhivi so v praksi povsem običajni, poraba pomnilnika pa ostaja omejena ne glede na velikost datoteke, saj se objekti preberejo le takrat, ko klic to zahteva.

Dve vprašanji se pojavljajo dovolj pogosto, da nanju odgovorimo neposredno. Združevanje prek privzete poti prenaša strukturo dokumenta, zato zaznamki in povezave preživijo; različica Fast je tista, ki zamenja drevo strukture za hitrost, kar je edini razlog, da jo rezerviramo za neoznačene vhode. Varna navada je, da odprete združeni izhod, se sprehodite po njegovem kazalu in pred pošiljanjem na hitro preverite nekaj notranjih povezav. Kar zadeva urejanje: obstaja uporabna vmesna pot med pregledovanjem samo za branje in polnim nalaganjem. Operacije na ravni strani delujejo neposredno na ročaju, med njimi DARotatePage, DAMovePage in DAHidePage, skupaj z branjem obraznih polj, metoda DAAppendFile pa te popravke shrani kot inkrementalno revizijo. Urejanje na ravni vsebine, vse kar spreminja označevalne operatorje znotraj strani, pa še vedno sodi v polni sloj dokumenta.

Če mora vaš združeni izhod ostati dostopen, je ozadje drevesa strukture pokrito v članku Dostopnost označenih PDF, ki pojasnjuje, kaj natanko bi različica Fast zavrgla. Za pridobivanje vsebine iz obsegov, ki jih razdelite, si oglejte priročnik za izločanje besedila, slik in pisav. Celoten seznam funkcij neposrednega dostopa je priložen knjižnici; izdaje in poskusni prenosi se nahajajo na produktni strani PDFlibPas.