Technical Article

Obrada velikih PDF-ova u Delphiju pomoću HotPDF Direct File API-ja

Brojanje stranica u skeniranoj arhivi veličine 1,4 GB trebalo bi biti jeftino. Pozovite LoadFromFile na toj datoteci i to prestaje biti jeftino: HotPDF analizira unakrsne referentne podatke i gradi objekt u memoriji za svaki od nekoliko stotina tisuća neizravnih objekata dokumenta, a 32-bitni proces pogađa gornju granicu adresnog prostora od 2 GB negdje u sredini te analize. Operacija koju ste željeli, broj stranica, nikada nije trebala niti jedan od tih objekata. Trebala je stablo stranica i ništa više. Taj jaz, između onoga što zadatak traži i onoga što potpuno učitavanje isporučuje, cijeli je razlog zašto Direct File API postoji.

Direct File API omogućuje Delphiju i C++Builderu pristup PDF-u na razini datoteke: broj stranica, kopije, dešifriranje, inkrementalno dodavanje, pri čemu se s diska čita samo ono što je stvarno potrebno umjesto rekonstrukcije cijelog modela dokumenta u RAM-u. Vještina je uskladiti svaki zadatak s najlakšom razinom koja ga može riješiti. Postignite to usklađivanje i usluga će zadržati stabilnu potrošnju memorije bez obzira na veličinu unosa. Ako pogriješite, prva prevelika datoteka srušit će radni proces.

Koliko vas košta potpuno učitavanje

LoadFromFile nije neprijatelj. On opravdava svoju memoriju: jednom kada je stablo u RAM-u, imate nasumičan pristup svakoj stranici i svakom objektu, što je točno ono što InsertPagesFromDocument, MovePage i ponovna serijalizacija putem SaveLoadedDocument zahtijevaju. Nema prečaca za stvarno restrukturiranje; morate držati dokument u memoriji kako biste ga presložili.

Nevolje počinju kada veličine unosa nisu pod vašom kontrolom. Korisnički prijenosi, izlazni podaci skenera i arhive od prije deset godina zanemaruju sve što je vaš testni korpus pretpostavljao. Učitajte svaki unos bezuvjetno i vaš će memorijski limit biti određen najvećom pojedinačnom datotekom koju će itko ikada poslati. Vrijeme parsiranja prati broj objekata, a zauzeta memorija se nakon zbrajanja struktura objekata i dekodiranih tokova postavlja na višestruku veličinu datoteke, pa gigabajt na disku može značiti nekoliko gigabajta u RAM-u.

Ponovno kompajliranje za 64-bitne sustave podiže granicu adresnog prostora, ali račun ostaje isti. Radni proces i dalje troši sekunde procesorskog vremena i višestruku veličinu datoteke u RAM-u kako bi odgovorio na pitanje na koje je struktura same datoteke mogla odgovoriti u milisekundama. Pri konkurentnom radu matematika postaje nepovoljna: četiri velika učitavanja koja se izvode istovremeno dijele isti memorijski proračun, a propusnost dramatično opada upravo kada je red čekanja najduži i kada si to najmanje možete priuštiti.

Čitanje datoteke putem rukovatelja

Razina samo za čitanje otvara datoteku kao rukovatelj (handle), odgovara na strukturna pitanja o njoj i zatvara je. Nema stabla objekata, nema renderiranja stranica, nema memorije koja raste s unosom.

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 ovu razinu pouzdanom. Prvo, provjerite povratnu vrijednost. Nepozitivan rukovatelj znači da otvaranje nije uspjelo, a pozivanje DAGetPageCount na neaktivnom rukovatelju je vrsta buga koji ostaje skriven sve do dana kada korisnik pošalje oštećenu datoteku. Drugo, uparite svako uspješno otvaranje s DACloseFile unutar bloka finally; usluga koja gubi rukovatelje ne ruši se odmah, već postupno propada, što je još gore. Treće, poštujte ono što parametar lozinke zapravo radi. DAOpenFileReadOnly je prihvaća, ali za šifrirane unose tiho se spušta na potpuno parsiranje kako bi pročitao broj stranica, pa jamstvo stabilne potrošnje memorije nestaje. Prvo usmjerite zaštićene datoteke kroz DecryptFile i ostatak cjevovoda ostaje jeftin.

Ista provjera služi i kao trijažni filter. Datoteke se pojavljuju s pogrešnim oznakama, polovično prenesene ili preimenovane iz nekog sasvim drugog formata, a provjera pomoću DAOpenFileReadOnly odbacuje sve takve datoteke na samom početku u roku od nekoliko milisekundi, pri čemu se pogreška povezuje s problematičnom datotekom. Alternativa je dopustiti da neispravna datoteka ode duboko u radni red čekanja i tamo eksplodira, gdje otkrivanje koji je unos uzrokovao problem može koštati cijelo popodne.

Kopiranje, dešifriranje i šifriranje cijelih datoteka

Druga razina premješta i transformira cjelovite datoteke bez izlaganja njihovih unutarnjih dijelova. To su pozivi na koje se cjevovodi za unos 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 opravdava svoje mjesto. DACopyFile je provjerena kopija iz karantenskog direktorija u upravljano spremište: on otvara i indeksira PDF strukturu dok radi, pa skraćeni ili ne-PDF unos zakazuje već ovdje, a ne tri faze kasnije. DecryptFile zapisuje dešifriranu kopiju izravnim putem prepisivanja AES-256 koji preskače stablo objekata kad god unos to dopušta, što je pandan tijeku dešifriranja učitavanja i ponovnog spremanja za velike datoteke opisanom u članku o AES-256 enkripciji. EncryptFile izvodi isti postupak unatrag, primjenjujući zaštitu lozinkom tijekom kopiranja na razini datoteke s parametrima vrste ključa i dopuštenja koje staza u memoriji već koristi.

Dodavanje promjena umjesto ponovnog pisanja

Inkrementalno ažuriranje, definirano u ISO 32000-1 §7.5.6, treća je razina. Izvorni bajtovi ostaju tamo gdje jesu na disku, a svi novi ili izmijenjeni objekti dodaju se nakon njih, nakon čega slijedi novi odjeljak unakrsnih referenci koji se povezuje natrag s izvornikom. Za arhivu od 900 MB kojoj je potrebno dodati jednu stranicu, trošak pisanja je samo razlika (delta), a ne cijela 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

Ovdje su važne dvije točke discipline. BeginIncrementalUpdate mora pokazivati na izvornu datoteku jer se dodani podaci unakrsnih referenci povezuju s bajtovnim pomacima unutar nje. Također, model je po dizajnu predviđen samo za dodavanje (append-only): svako inkrementalno spremanje povećava datoteku, nikada je ne smanjuje. Dokument koji se svake noći pečati rasti će bez ograničenja dok ga periodična ponovna serijalizacija, učitavanjem i ponovnim pisanjem natrag kroz SaveLoadedDocument, ne sažme. Ta ista priroda dodavanja na kraj je ono što inkrementalno ažuriranje čini jedinim sigurnim načinom rada s digitalno potpisanim dokumentom, što je ograničenje proučeno u članku o digitalnim potpisima i PAdES-u. Temeljni mehanizam unakrsnih referenci opisan je u članku o tokovima objekata i inkrementalnim ažuriranjima.

Postoji zamka u spremanju samo s dodavanjem koja promakne većini recenzija. Izvorni bajtovi ostaju u datoteci, čitljivi svakome tko ih poželi pogledati. Inkrementalno ažuriranje koje "replaces" a page does not delete the old one; it supersedes it in the current revision while the previous revision sits there, fully recoverable. Stoga su inkrementalna ažuriranja pogrešan alat za uklanjanje osjetljivog sadržaja. Da biste doista odbacili povijest koju primatelj nikada ne bi trebao vidjeti, trebate potpunu ponovnu serijalizaciju: LoadFromFile nakon čega slijedi SaveLoadedDocument, što zapisuje samo trenutačno stanje i ostavlja skrivene revizije iza sebe.

Usklađivanje razine s operacijom

Logika odabira dovoljno je jednostavna da je držite u glavi i isplati se kodirati je kao izričitu odluku o usmjeravanju na vrhu cjevovoda umjesto da dopustite da svaki zadatak improvizira vlastiti put. Operacija koja vam je potrebna određuje razinu:

  • Brojanje, pregled ili klasifikacija otvara rukovatelj: DAOpenFileReadOnly, DAGetPageCount, DACloseFile.
  • Premještanje, dešifriranje ili šifriranje cijele datoteke ostaje na razini datoteke uz DACopyFile, DecryptFile ili EncryptFile.
  • Restrukturiranje stranica ili spajanje dokumenata zahtijeva potpuno učitavanje: LoadFromFile, zatim InsertPagesFromDocument ili MovePage, pa SaveLoadedDocument.
  • Dodavanje male razlike (delta) golemoj ili potpisanoj datoteci poziva BeginIncrementalUpdate i sprema.

Mješoviti cjevovodi trebali bi postaviti prag veličine ispred staze za potpuno učitavanje. Pošaljite sve što premašuje nekoliko stotina megabajta kroz razine Direct File, a potpuno učitavanje rezervirajte za stvarno restrukturiranje na 64-bitnom radnom procesu s odgovarajućim memorijskim proračunom. Prag pretvara rušenje zbog nedostatka memorije u odluku o usmjeravanju koju možete pratiti i podešavati.

Koju god razinu koristili za obradu zadatka, zapišite njezin izlaz pod privremenim nazivom i preimenujte ga na pravo mjesto tek kada se rezultat provjeri. Polovično zapisana datoteka pod konačnim nazivom izgleda točno kao i ispravna za sljedeću fazu cjevovoda, a pozivi Direct File čine tu provjeru jeftinom: potvrda izlaza je provjera rukovatelja u jednoj liniji.

Direct File API se isporučuje kao dio komponente HotPDF Component za Delphi i C++Builder. Stranica proizvoda sadrži poveznicu na cjelovitu referencu funkcija, uključujući ovdje prikazane pozive za inkrementalno ažuriranje.