Tehnični članak

Združevanje in deljenje gigabajtnih PDF v Delphiju

Združevanje ali deljenje dvogigabajtnega PDF po očitni poti vas stane dvoje hkrati: čas na uri in naslovni prostor. Očitna pot je naložiti vsak vhod, opraviti delo, zapisati izhod. Nalaganje je tisto, kar se zlomi. Arhiv skenov, ki se s 300 premakne na 600 DPI, podvoji linearno ločljivost in se na disku približno početveri, zato isto sestavljalno opravilo, ki je vse leto obvladovalo datoteke po 400 MB, začne mlatiti prazno v trenutku, ko vhod prekorači gigabajt, pogosto medtem ko ne počne nič drugega kot šteje strani. Opravilo ni postalo nič težje. Odpri, preštej, izberi obsege, zlepi — to je vse. Nalaganje celotnega drevesa preprosto ni več smiselna privzeta izbira pri tej velikosti. PDF Library for Delphi, knjižnica losLab za PDF v Delphiju in C++Builderju, na to odgovarja s svojo plastjo neposrednega dostopa: družino funkcij s predpono DA, ki jo podpira pretočni bralnik in ki tabelo navzkrižnih sklicev prehodi na mestu, namesto da bi celoten dokument gradil v pomnilniku

Kam gre pomnilnik pri polnem nalaganju

Nalaganje PDF "na običajen način" pomeni razčlenitev xref, razrešitev vsakega posrednega objekta v drevo v pomnilniku, dekodiranje objektnih tokov ter povezavo drevesa strani, pisav in pripisov v objekte, s katerimi lahko upravljate. Za urejevalne poteke dela je to prava kupčija. Za združevanje, deljenje in pregledovanje je večinoma zapravljanje. Arhiv skenov s 30.000 stranmi lahko drži milijone posrednih objektov, opravilo deljenja pa jih mora prebrati nekaj sto: vozlišča strani v zahtevanem obsegu in tisto, na kar se ta vozlišča sklicujejo

Plast neposrednega dostopa model obrne. DAOpenFile in DAOpenFileReadOnly razčlenita prikolico in xref, torej nekaj kilobajtov na repu datoteke, ter vrneta ročico datoteke. Objekti se pridobijo leno, ko jih klic potrebuje. Praktična posledica je, da odpiranje večgigabajtne datoteke traja približno toliko kot odpiranje majhne, poraba pomnilnika pa sledi temu, česa se dotaknete, ne pa temu, kaj datoteka vsebuje

PDF Library for Delphi: primerjava nalaganja gigabajtnega PDF v polno objektno drevo v pomnilniku z odpiranjem prek neposrednega dostopa, kjer se razčlenjevanje ustavi pri prikolici in xref, ročica pa streže lena branja po posameznih objektih
Polno nalaganje dekodira vsak posredni objekt, preden se združevanje sploh lahko začne, zato pomnilnik in čas odpiranja rasteta z arhivom. Pot neposrednega dostopa vrne delujočo ročico po branju nekaj kilobajtov in vsakemu klicu pusti, da povleče le objekte, ki jih potrebuje

Sondiranje ogromne datoteke brez nalaganja

Spodnji vzorec izhaja iz knjižničinega merila zmogljivosti za velike datoteke: odpri samo za branje, postavi vprašanja, zapri. 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čin samo za branje je vredno imeti raje, kadar koli je mogoče: omogoča, da faza zajema teče, medtem ko datoteko držijo drugi procesi, in dokumentira namen. Faza sondiranja, ki po nesreči pokliče spreminjajočo funkcijo, odpove hitro, namesto da bi pokvarila arhiv

PageRef je ročica objekta, ne številka strani

Najpogostejša napaka pri API-ju DA je podajanje številke strani tam, kjer funkcija pričakuje PageRef. Skoraj vsak klic DA za posamezno stran vzame sklicno ročico objekta strani in ne številke strani: DAExtractPageText, DARenderPageToFile, DARotatePage in DACapturePage vsi pričakujejo sklic. Dobite ga tako, da človeku namenjeno številko prevedete prek DAFindPage:

PDF Library for Delphi: potek prevajanja številke strani v PageRef, kjer DAFindPage napaja klice neposrednega dostopa za posamezno stran, v primerjavi s surovim celoštevilskim sklicem, ki pristane na poljubnem objektu in tiho da besedilo z napačne strani
Vsak klic neposrednega dostopa za posamezno stran zaužije PageRef, ki ga izdela DAFindPage, in nikoli človeku namenjene številke. Če ta prevod preskočite, se celo število pretvarja, da je id objekta, besedilo z napačne strani pa lahko odide k stranki neopazno
PageRef := Lib.DAFindPage(Handle, 250);          // številka strani -> ročica objekta
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Podajanje surove številke 250 namesto tega ne sproži napake. Naslovi objekt, ki pač stoji za to vrednostjo ročice, kar na dober dan odpove vidno, na slab dan pa izvleče besedilo z napačne strani v dokument, namenjen stranki. Če plast DA ovijete v lastno storitveno kodo, poskrbite, da prevoda ne bo mogoče preskočiti: na meji sprejmite številke strani, takoj pokličite DAFindPage in interno podajajte samo sklice

Združevanje stotin datotek s poimenovanim seznamom

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

PDF Library for Delphi: potek dela s poimenovanim seznamom datotek, kjer se januarski, februarski in marčevski izpiski registrirajo pod enim imenom seznama in združijo v enem prehodu, različice Fast, privzeta in stroga pa menjajo ohranitev strukturnega drevesa za hitrost
Stotine registriranih vhodov se zložijo v en sam prehod MergeFileList, katerega izid se v milisekundah preveri z novo sondo samo za branje. Različica je odločitev na ravni cevovoda, ker Fast zavrže strukturno drevo označenega PDF
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Izid preverite po poceni poti: spet neposredni dostop
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

Družina za združevanje ima tri različice in razlika ni le v hitrosti. MergeFileListFast preskoči ohranitev strukturnega drevesa; MergeFileListStrict uveljavi strogi način; različica brez pripone je uravnotežena privzeta izbira. Iz tega izpade operativno pravilo: če je kateri koli vhod označen PDF, čigar zgradba dostopnosti mora preživeti, pri čemer je očiten primer vse, kar nastaja za PDF/UA, sezite po privzeti ali strogi različici, saj Fast strukturno drevo tiho zavrže. Za navadne arhive skenov brez označevanja je Fast zastonj zmogljivost. Odločite se na ravni cevovoda in ne po razpoloženju razvijalca, uporabljeno različico pa zabeležite v dnevnik opravila

Deljenje brez nalaganja: izvleček obsegov

Deljenje sledi isti filozofiji brez nalaganja. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) potegne obseg strani naravnost iz datoteke v datoteko, s seznamom obsegov, kot je '1-500', '501-1000', ali z izbirami, ločenimi z vejicami, vir pa nikoli ne postane drevo dokumenta. Kadar je dokument iz drugih razlogov že naložen, ExtractPageRanges iz trenutnega ustvari nov dokument v pomnilniku, CopyPageRanges pa obsege prenese iz drugega naloženega dokumenta po ID. Za deljenje združenih tiskalniških tokov po posameznih izpiskih je oblika iz datoteke v datoteko tista, ki 4 GB vhodu prepreči, da bi se sploh kdaj napihnil v pomnilnik

Datoteke, ki lažejo o svoji geometriji

Cevovodi za velike datoteke srečujejo poškodovane datoteke pogosteje, kot jih cevovodi za majhne datoteke sploh kdaj vidijo, preprosto zato, ker vhodi prehajajo skozi več sistemov. Dve obliki odpovedi si zaslužita izrecno obravnavo

Prvič, premaknjene glave. Poštni prehodi in tiskalniške vrste PDF včasih spredaj dodajo bajte, zato oznaka %PDF ne stoji več na odmiku 0, vsak odmik xref v datoteki pa je napačen za isto vrednost. Pretočni bralnik to zazna in razkrije (DAShiftedHeader na ploski ravni, ShiftedHeader na TSmartPDFReader), nato pa to med branjem izravna. Domača aritmetika odmikov tega običajno ne počne, in prav zato je "deluje na vsaki datoteki, ki jo ustvarimo sami, odpove pri datotekah stranke X" klasičen simptom

Drugič, pokvarjene tabele navzkrižnih sklicev. DACopyFile(InputFileName, OutputFileName, PageCount) celotno datoteko pretoči v nov izvod in med tem znova zgradi xref, kot stranski izdelek pa vrne število strani. Če ga poganjate kot fazo normalizacije pred izbirčnim porabnikom nižje po toku, razred občasnih napak pri razčlenjevanju pretvorite v en predvidljiv popravljalni korak. Kadar je treba shraniti vaše lastne spremembe, jih DAAppendFile zapiše kot inkrementalno posodobitev, torej doda novo revizijo, namesto da bi prepisoval gigabajte, kar strošek shranjevanja ohrani sorazmeren s spremembo in ne z datoteko

Podrobnosti dostave: linearizacija in sestavljanje

Cevovod za velike datoteke zaokrožita dve sosednji zmožnosti. Kadar se sestavljen izhod streže po HTTP za ogled v brskalniku, ga LinearizeFile preuredi za pretakanje po obsegih bajtov, tako da se prva stran prikaže, preden se ostanek 500 MB paketa prenese do konca. Poganjajte ga kot zadnjo fazo, po vsem združevanju, saj vsaka poznejša sprememba datoteko spet delinearizira. Kadar pa paketi potrebujejo sestavljanje in ne golega lepljenja, denimo naslovnico, odtisnjeno za vsakim izpiskom, ali dve izvorni strani, uprizorjeni na enem izhodnem listu, DACapturePage poljubno stran spremeni v predlogo za večkratno rabo, ki jo DADrawCapturedPage postavi na ciljno stran v poljuben pravokotnik, še vedno brez polnega nalaganja večgigabajtnega vira

Omejitve in kaj ostane samo za branje

Zapisu samemu zmanjka prostora dosti prej kot neposrednemu dostopu. Odmiki so skozi celotno plast DA Int64, zato sta resnična stropa razpoložljivi disk in desetmestno polje za odmik xref v klasičnih (netokovnih) tabelah navzkrižnih sklicev. Večgigabajtni arhivi skenov v praksi niso nič posebnega, poraba pomnilnika pa ostaja omejena ne glede na velikost datoteke, ker se objekti berejo šele, ko jih klic zahteva

Dve vprašanji se pojavljata dovolj pogosto, da nanju odgovorimo naravnost. Združevanje po privzeti poti prenese zgradbo dokumenta, zato zaznamki in povezave preživijo; različica Fast je tista, ki strukturno drevo zamenja za hitrost, in prav to je razlog, da jo prihranite za neoznačene vhode. Varna navada je, da združen izhod odprete, prehodite njegov oris in pred odpremo preverite nekaj notranjih povezav. Glede urejanja: med sondiranjem samo za branje in polnim nalaganjem obstaja koristna vmesna pot. Operacije na ravni strani delujejo neposredno na ročici, med njimi DARotatePage, DAMovePage in DAHidePage, poleg branja polj obrazcev, DAAppendFile pa te spremembe ohrani kot inkrementalno revizijo. Urejanje na ravni vsebine, torej vse, kar prepisuje označevalne operatorje znotraj strani, še vedno sodi v plast celotnega dokumenta

Sorodni članki

Če mora vaš združeni izhod ostati dostopen, je ozadje o strukturnem drevesu obravnavano v članku o dostopnosti označenega PDF, ki natančno pojasni, kaj bi različica Fast zavrgla. Za izvlečenje vsebine iz obsegov, ki jih razdelite, glejte vodnik po izvlečku besedila, slik in pisav

Celoten seznam funkcij neposrednega dostopa je priložen knjižnici; izdaje in preizkusni prenosi so na strani izdelka PDF Library for Delphi