Technical Article

Obrada velikih PDF fajlova u Delphi-ju uz HotPDF Direct File API

Brojanje stranica u skeniranoj arhivi od 1.4 GB trebalo bi da bude računski jeftino. Pozovite LoadFromFile za taj fajl i to prestaje da bude jeftino: HotPDF parsira podatke o unakrsnim referencama i gradi objekat u memoriji za svaki od nekoliko stotina hiljada indirektnih objekata u dokumentu, a 32-bitni radni proces nailazi na ograničenje adresnog prostora od 2 GB negde na sredini tog parsiranja. Operaciji koju ste želeli – broju stranica – nikada nije bio potreban nijedan od tih objekata. Bilo je potrebno stablo stranica (page tree) i ništa više. Taj jaz, između onoga što posao zahteva i onoga što potpuno učitavanje isporučuje, jeste razlog zašto Direct File API uopšte postoji.

Direct File API pruža Delphi-ju i C++Builder-u pristup PDF-u na nivou fajla: broj stranica, kopiranje, dešifrovanje, inkrementalno dodavanje, pri čemu se sa diska čita samo ono što je zaista potrebno, umesto rekonstrukcije celokupnog modela dokumenta u RAM memoriji. Veština se sastoji u tome da svaki zadatak uskladite sa najlakšim nivoom koji može da odgovori na njega. Uradite to kako treba i servis će zadržati stabilnu potrošnju memorije bez obzira na veličinu ulaza. Pogrešite, i prvi preveliki fajl će srušiti radni proces.

Koliko vas košta potpuno učitavanje

Metoda LoadFromFile nije neprijatelj. Ona opravdava svoju potrošnju memorije: kada je stablo učitano u RAM, imate slučajan pristup svakoj stranici i svakom objektu, što je upravo ono što metode poput InsertPagesFromDocument, MovePage i ponovna serijalizacija kroz SaveLoadedDocument zahtevaju. Nema prečice za stvarno restrukturiranje koda – morate držati dokument u memoriji da biste ga reorganizovali.

Nevolja počinje kada nemate kontrolu nad veličinom ulaznih podataka. Korisnički otpremljeni fajlovi, izlazi sa skenera i arhive od pre deset godina ignorišu sve pretpostavke koje ste napravili na osnovu test primera. Učitajte svaki ulaz bezuslovno i vaš maksimalni memorijski prag biće određen jednim najvećim fajlom koji će bilo ko ikada poslati. Vreme parsiranja prati broj objekata, a rezistentna memorija se nakon proračuna struktura objekata i dekodiranih tokova postavlja na vrednost koja je nekoliko puta veća od veličine fajla, pa gigabajt na disku može značiti više gigabajta u RAM-u.

Ponovno kompajliranje za 64-bitnu arhitekturu podiže granicu adresnog prostora, ali račun i dalje ostaje isti. Radni proces i dalje troši sekunde procesorskog vremena i višestruko veću količinu memorije u odnosu na veličinu fajla u RAM-u da bi odgovorio na pitanje na koje je struktura samog fajla mogla odgovoriti u milisekundama. Pri konkurentnom radu računica postaje nepovoljna: četiri velika učitavanja koja se izvršavaju istovremeno dele isti memorijski budžet, a protok opada upravo kada je red za čekanje najduži i kada to najmanje možete priuštiti.

Čitanje fajla preko rukovaoca

Čitanje samo za pregled (read-only nivo) otvara fajl preko rukovaoca (handle), odgovara na strukturna pitanja o njemu i zatvara ga. Bez stabla objekata, bez renderovanja stranica, bez rasta potrošnje memorije sa rastom ulaza.

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 navike održavaju ovaj nivo pouzdanim. Prvo, proverite povratnu vrednost. Nepozitivan rukovalac znači da otvaranje nije uspelo, a pozivanje funkcije DAGetPageCount sa nevažećim rukovaocem je vrsta baga koja ostaje skrivena sve dok klijent ne pošalje neispravan fajl. Drugo, uparite svako uspešno otvaranje sa DACloseFile unutar finally bloka; servis koji gubi rukovaoce ne ruši se odmah, već polako propada, što je još gore. Treće, imajte na umu šta parametar za lozinku zapravo radi. Funkcija DAOpenFileReadOnly prihvata lozinku, ali za šifrovane ulaze tiho prelazi na kompletno parsiranje kako bi pročitala broj stranica, čime garancija niske potrošnje memorije prestaje da važi. Prvo usmerite zaštićene fajlove kroz DecryptFile i ostatak toka obrade će ostati računski jeftin.

Ova ista provera služi i kao filter za trijažu. Fajlovi mogu stići sa pogrešnim oznakama, delimično otpremljeni ili preimenovani iz nekog sasvim drugog formata, a provera preko DAOpenFileReadOnly odbacuje sve takve slučajeve već na samom početku u milisekundama, pri čemu se greška vezuje direktno za problematični fajl. Alternativa je puštanje neispravnog fajla duboko u red za obradu gde će izazvati grešku, pa pronalaženje uzroka može da vam oduzme celo popodne.

Kopiranje, dešifrovanje i šifrovanje celih fajlova

Drugi nivo premešta i transformiše kompletne fajlove bez izlaganja njihovih unutrašnjih struktura. To su pozivi na koje se ulazni tokovi obrade najviše oslanjaju.

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

Svaki poziv ima svoju svrhu. DACopyFile obavlja validaciju i kopiranje iz karantinskog direktorijuma u upravljano skladište: otvara i indeksira PDF strukturu u hodu, pa će oštećeni ili ne-PDF ulaz otkazati odmah ovde, a ne tri faze kasnije. DecryptFile upisuje dešifrovanu kopiju kroz direktnu AES-256 putanju prepisivanja koja preskače stablo objekata kad god ulaz to dozvoljava, što je pandan za velike fajlove toku dešifrovanja sa učitavanjem i ponovnim snimanjem opisanom u članku o AES-256 enkripciji. EncryptFile obavlja isti postupak unazad, primenjujući zaštitu lozinkom tokom kopiranja na nivou fajla sa parametrima za tip ključa i dozvole koje in-memory putanja već koristi.

Dodavanje izmena umesto ponovnog upisivanja celog fajla

Inkrementalno ažuriranje, definisano u standardu ISO 32000-1 §7.5.6, predstavlja treći nivo. Originalni bajtovi ostaju tamo gde jesu na disku, a svi novi ili izmenjeni objekti se dodaju nakon njih, praćeni novim odeljkom unakrsnih referenci koji upućuje na original. Za arhivu od 900 MB kojoj treba dodati samo jednu stranicu, cena upisivanja je samo ta razlika (delta), a ne ceo fajl.

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

Ovde su važne dve tačke discipline. BeginIncrementalUpdate mora pokazivati na originalni fajl, jer se dodati podaci o unakrsnim referencama povezuju sa ofsetima bajtova unutar njega. Takođe, model je po dizajnu predviđen isključivo za dodavanje na kraj (append-only): svako inkrementalno čuvanje povećava fajl, nikada ga ne smanjuje. Dokument koji se potpisuje ili pečatira svake noći rastao bi bez ograničenja sve dok se periodičnom re-serijalizacijom (učitavanjem i ponovnim upisivanjem pomoću SaveLoadedDocument) ne bi ponovo kompaktovao. Ta ista osobina dodavanja na kraj je ono što inkrementalno ažuriranje čini jedinim bezbednim načinom za izmenu digitalno potpisanog dokumenta, što je ograničenje opisano u članku o digitalnim potpisima i PAdES-u. Sam mehanizam unakrsnih referenci detaljno je obrađen u članku o tokovima objekata i inkrementalnim ažuriranjima.

Postoji zamka kod čuvanja dodavanjem na kraj koja često promakne tokom revizije koda. Originalni bajtovi ostaju u fajlu, čitljivi svakome ko želi da ih pogleda. Inkrementalno ažuriranje koje „zamenjuje” stranicu zapravo ne briše staru; ono je samo nadjačava u trenutnoj reviziji, dok prethodna revizija ostaje tamo, potpuno povratna. Zato inkrementalna ažuriranja nisu dobar alat za uklanjanje poverljivog sadržaja. Da biste zaista uklonili istoriju koju primalac nikada ne bi trebalo da vidi, potrebna vam je potpuna re-serijalizacija: poziv LoadFromFile, a zatim SaveLoadedDocument, što upisuje samo trenutno stanje i odbacuje prethodne revizije.

Usklađivanje nivoa sa operacijom

Logika izbora je dovoljno jednostavna da je zapamtite, i isplati se kodirati je kao eksplicitnu odluku o usmeravanju na samom vrhu toka obrade, umesto da dopustite da svaki posao samostalno bira svoj put. Operacija koja vam je potrebna određuje nivo:

  • Brojanje, pregledanje ili klasifikacija otvara rukovalac: DAOpenFileReadOnly, DAGetPageCount, DACloseFile.
  • Premeštanje, dešifrovanje ili šifrovanje celog fajla ostaje na nivou fajla uz DACopyFile, DecryptFile ili EncryptFile.
  • Restrukturiranje stranica ili spajanje dokumenata zahteva potpuno učitavanje: LoadFromFile, zatim InsertPagesFromDocument ili MovePage, pa SaveLoadedDocument.
  • Dodavanje male razlike u ogroman ili potpisan fajl koristi BeginIncrementalUpdate i čuvanje.

Kombinovani tokovi obrade bi trebali da postave prag veličine pre putanje potpunog učitavanja. Pošaljite sve što prelazi nekoliko stotina megabajta kroz Direct File nivoe, a potpuno učitavanje rezervišite za stvarna restrukturiranja na 64-bitnom radnom procesu sa odgovarajućim memorijskim budžetom. Ovaj prag pretvara rušenje aplikacije zbog nedostatka memorije u odluku o usmeravanju koju možete pratiti i podešavati.

Koji god nivo da obrađuje posao, upišite njegov izlaz pod privremenim imenom i preimenujte ga u konačni naziv tek kada se rezultat validira. Delimično zapisan fajl pod konačnim imenom izgleda isto kao i ispravan sledećoj fazi obrade, a Direct File pozivi čine tu proveru jeftinom – potvrda izlaza je provera u jednoj liniji koda.

Direct File API se isporučuje kao deo HotPDF komponente za Delphi i C++Builder. Stranica proizvoda sadrži link do kompletne referentne dokumentacije, uključujući i pozive za inkrementalno ažuriranje koji su ovde prikazani.