Technical Article

Obdelava velikih datotek PDF v Delphi s HotPDF Direct File API

Štetje strani v skeniranem arhivu velikosti 1,4 GB bi moralo biti procesno nezahtevno. Če pa na tej datoteki pokličete LoadFromFile, to takoj postane potratno: HotPDF razčleni navzkrižne sklice (cross-reference) in v pomnilniku zgradi objekt za vsakega od več sto tisoč posrednih objektov dokumenta, pri čemer 32-bitni delovni proces nekje sredi razčlenjevanja doseže mejo 2 GB adresnega prostora. Dejanje, ki ste ga želeli, torej štetje strani, ni nikoli potrebovalo nobenega od teh objektov. Potrebovalo je drevo strani in nič drugega. Ta razkorak med tistim, kar delo zahteva, in tistim, kar prinaša celotno nalaganje, je edini razlog za obstoj vmesnika Direct File API.

Vmesnik Direct File API omogoča okoljema Delphi in C++Builder dostop do datotek PDF na ravni same datoteke: štetje strani, kopiranje, dešifriranje in inkrementalno dodajanje, pri čemer se iz diska prebere le tisto, kar se dejansko potrebuje, namesto da bi se celoten model dokumenta znova gradil v pomnilniku RAM. Ključ do uspeha je uskladitev vsake naloge z najlažjo ravnjo, ki jo lahko izvede. Če to uskladite pravilno, storitev ohranja stabilno porabo pomnilnika ne glede na velikost vnosa. Če pa se zmotite, bo že prva prevelika datoteka zrušila delovni proces.

Kakšna je cena celotnega nalaganja

Metoda LoadFromFile ni sovražnik. Svoj pomnilnik si zasluži: ko je drevo enkrat v pomnilniku RAM, imate naključen dostop do vsake strani in vsakega objekta, razporeda, kar je natanko tisto, kar zahtevajo metode InsertPagesFromDocument, MovePage in ponovna serializacija prek SaveLoadedDocument. Za resnično prestrukturiranje ni bližnjic; dokument morate imeti v pomnilniku, da ga lahko preuredite.

Težava se začne, ko nimate nadzora nad velikostjo vhodnih podatkov. Prenosi strank, izhodi skenerjev in arhivski dokumenti izpred desetih let se ne ozirajo na to, kar je predvideval vaš testni nabor. Če brezpogojno naložite vsak vnos, bo vaša zgornja meja pomnilnika določena z največjo posamezno datoteko, ki jo bo kdaj kdo poslal. Čas razčlenjevanja sledi številu objektov, porabljen pomnilnik pa se po preštetju struktur objektov in dekodiranih tokov ustali pri večkratniku velikosti datoteke, tako da lahko gigabajt na disku pomeni več gigabajtov v delovnem pomnilniku.

Ponovno prevajanje za 64-bitne sisteme sicer odpravi mejo adresnega prostora, vendar strošek ostaja enak. Delovni proces še vedno porablja sekunde procesorskega časa in večkratnik velikosti datoteke v pomnilniku RAM za odgovor na vprašanje, na katerega bi struktura same datoteke lahko odgovorila v milisekundah. Pri sočasnem izvajanju matematika postane neusmiljena: štiri velika nalaganja, ki tečejo hkrati, si delijo isti pomnilniški proračun, prepustnost pa strmoglavi ravno takrat, ko je čakalna vrsta najdaljša in si tega najmanj želite.

Branje datoteke prek ročaja

Raven samo za branje odpre datoteko kot ročaj (handle), odgovori na strukturna vprašanja o njej in jo zapre. Brez drevesa objektov, brez izrisovanja strani in brez pomnilnika, ki bi naraščal z velikostjo vnosa.

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;

Tri navade zagotavljajo zanesljivost te ravni. Prvič, preverite povratno vrednost. Nepozitiven ročaj pomeni, da odpiranje ni uspelo, izvajanje metode DAGetPageCount na neveljavnem ročaju pa je vrsta hrošča, ki ostane skrit do dneva, ko stranka pošlje poškodovano datoteka. Drugič, vsako uspešno odpiranje povežite z metodo DACloseFile znotraj bloka finally; storitev, ki ji uhajajo ročaji (leak handles), se ne zruši takoj, temveč počasi propada, kar je še slabše. Tretjič, spoštujte namen parametra za geslo. Metoda DAOpenFileReadOnly ga sicer sprejme, vendar pri šifriranih vnosih tiho preide na celotno razčlenjevanje, da prebere število strani, s čimer garancija za nizko porabo pomnilnika izgine. Zaščitene datoteke najprej usmerite skozi DecryptFile in preostali del postopka bo ostal nezahteven.

Ta ista sonda služi tudi kot vrata za triažo. Datoteke se lahko pojavijo napačno označene, le delno naložene ali preimenovane iz povsem drugega formata, preizkus z metodo DAOpenFileReadOnly pa vse takšne datoteke zavrne že na vratih v nekaj milisekundah z napako, ki je neposredno povezana s problematično datoteko. Alternativa je, da poškodovano datoteko spustite globoko v delovni proces vrste in jo pustite, da tam povzroči napako, pri čemer lahko iskanje vzroka traja celo popoldne.

Kopiranje, dešifriranje in šifriranje celotnih datotek

Druga raven premika in spreminja celotne datoteke, ne da bi sploh razkrila njihovo notranjost. To so klici, na katere se sprejemni procesi najbolj zanašajo.

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

Vsak klic ima svoj namen. Metoda DACopyFile opravi preverjeno kopiranje iz karantenskega imenika v upravljano shrambo: sproti odpira in indeksira strukturo PDF, tako da okrnjena datoteka ali datoteka, ki ni PDF, odpove že tukaj in ne tri korake kasneje v verigi. Metoda DecryptFile zapiše dešifrirano kopijo po neposredni poti prepisovanja AES-256, ki preskoči drevo objektov, kadar vnos to dopušča. To je različica za velike datoteke, ki ustreza poteku dešifriranja z nalaganjem in ponovnim shranjevanjem, opisanem v članku o šifriranju AES-256. Metoda EncryptFile izvaja isto gibanje v obratni smeri in uporabi zaščito z geslom med kopiranjem na ravni datoteke z enakimi parametri za vrsto ključa in dovoljenja, kot jih uporablja pot v pomnilniku.

Dodajanje sprememb namesto ponovnega zapisovanja

Inkrementalna posodobitev, opredeljena v standardu ISO 32000-1 §7.5.6, predstavlja tretjo raven. Prvotni bajti ostanejo tam, kjer so na disku, vsi novi ali spremenjeni objekti pa se dodajo za njimi, čemur sledi nov del z navzkrižnimi sklici, ki se povezuje z izvirnikom. Za arhiv velikosti 900 MB, ki potrebuje dodano eno samo stran, je strošek pisanja le razlika (delta) in ne celotna datoteka.

// 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

Tukaj sta pomembni dve točki discipline. Metoda BeginIncrementalUpdate mora kazati na izvirno datoteko, saj se priloženi podatki o navzkrižnih sklicih povezujejo z odkloni bajtov znotraj nje. Model pa je že po zasnovi namenjen le dodajanju na konec: vsako inkrementalno shranjevanje datoteko poveča in je nikoli ne pomanjša. Dokument, ki se žigosa vsako noč, bo naraščal brez omejitev, dokler ga občasna ponovna serializacija, ki ga naloži in zapiše nazaj prek SaveLoadedDocument, ne stisne. Ta ista narava dodajanja na konec je tisto, zaradi česar je inkrementalna posodobitev edini varen način za spreminjanje digitalno podpisanega dokumenta, kar je omejitev, preučena v članku o digitalnih podpisih in PAdES. Temeljni mehanizem navzkrižnih sklicev je podrobneje obravnavan v članku o tokovih objektov in inkrementalnih posodobitev.

V shranjevanju z dodajanjem na konec se skriva past, ki jo večina pregledov spregleda. Prvotni bajti ostanejo v datoteki in so berljivi vsakomur, ki si jih želi ogledati. Inkrementalna posodobitev, ki "nadomesti" stran, ne izbriše stare strani; le nadomesti jo v trenutni različici, medtem ko prejšnja različica ostaja tam, popolnoma obnovljiva. Zato so inkrementalne posodobitve napačno orodje za odstranjevanje občutljivih vsebin. Če želite resnično odstraniti zgodovino, ki je prejemnik ne bi smel nikoli videti, potrebujete popolno ponovno serializacijo: LoadFromFile, ki ji sledi SaveLoadedDocument, ki zapiše samo trenutno stanje in pusti zakopane različice zadaj.

Uskladitev ravni z operacijo

Izbira je dovolj preprosta, da si jo zapomnite, in pametno jo je zapisati kot eksplicitno odločitev o usmerjanju na začetku postopka, namesto da bi vsako opravilo improviziralo svojo pot. Zahtevana operacija določa raven:

  • Štetje, pregledovanje ali razvrščanje odpre ročaj: DAOpenFileReadOnly, DAGetPageCount in DACloseFile.
  • Premikanje, dešifriranje ali šifriranje celotne datoteke ostane na ravni datoteke z DACopyFile, DecryptFile ali EncryptFile.
  • Prestrukturiranje strani ali združevanje dokumentov zahteva celotno nalaganje: LoadFromFile, nato InsertPagesFromDocument ali MovePage in nato SaveLoadedDocument.
  • Dodajanje majhne razlike (delta) v ogromno ali podpisano datoteko pokliče BeginIncrementalUpdate in shrani spremembe.

V mešanih cevovodih je priporočljivo postaviti prag velikosti pred potjo za celotno nalaganje. Vse datoteke, ki presegajo nekaj sto megabajtov, pošljite skozi ravni Direct File, celotno nalaganje pa rezervirajte za resnično prestrukturiranje na 64-bitnem delovnem procesu z ustreznim proračunom pomnilnika. Ta prag pretvori zrušitev zaradi pomanjkanja pomnilnika v usmerjevalno odločitev, ki jo lahko spremljate in prilagajate.

Ne glede na to, katera raven obravnava opravilo, zapišite njen izhod pod začasno ime in ga preimenujte na končno mesto šele, ko je rezultat potrjen. Napol zapisana datoteka z dokončnim imenom je za naslednjo stopnjo postopka videti povsem enako kot dobra datoteka, klici Direct File pa omogočajo poceni preverjanje: potrditev izhoda je enovrstični preizkus ročaja.

Vmesnik Direct File API je na voljo kot del komponente HotPDF za Delphi in C++Builder. Stran izdelka vsebuje povezavo do celotne reference funkcij, vključno s klici za inkrementalno posodobitev, prikazanimi tukaj.