Articol tehnic

Procesarea PDF-urilor mari în Delphi cu API-ul HotPDF Direct File

Numărarea paginilor dintr-o arhivă scanată de 1,4 GB ar trebui să fie ieftină. Apelați LoadFromFile pe acel fișier și încetează să mai fie ieftină: HotPDF parsează datele de referințe încrucișate și construiește un obiect în memorie pentru fiecare dintre cele câteva sute de mii de obiecte indirecte ale documentului, iar un worker pe 32 de biți lovește plafonul de 2 GB al spațiului de adrese undeva la mijlocul acestui parsing. Operația pe care ați dorit-o, numărul de pagini, nu a avut niciodată nevoie de niciunul dintre acele obiecte. Avea nevoie de arborele de pagini și de nimic altceva. Acest decalaj, dintre ceea ce cere o operație și ceea ce livrează o încărcare completă, este întregul motiv pentru care există Direct File API

Direct File API oferă Delphi și C++Builder acces la nivel de fișier la un PDF: numărul de pagini, copieri, decriptare, adăugări incrementale, toate citind de pe disc exact ceea ce au nevoie, în loc să reconstruiască întregul model de document în RAM. Abilitatea constă în a potrivi fiecare operație cu cel mai ușor nivel care o poate rezolva. Faceți această potrivire corect și un serviciu menține memorie constantă indiferent de dimensiunea intrării. Greșiți-o și primul fișier supradimensionat doboară worker-ul

Diagramă de rutare a joburilor pentru HotPDF Direct File API în Delphi: sonde de handle doar-citire, copiere și criptare a fișierului întreg, adăugări incrementale și încărcări complete în memorie, clasate după profilul de memorie
Potrivește fiecare operație cu cel mai ușor nivel care o poate răspunde, menținând memoria rezidentă plată indiferent de dimensiunea input-ului

Cât vă costă o încărcare completă

LoadFromFile nu este dușmanul. Își câștigă memoria: odată ce arborele este în RAM, aveți acces aleatoriu la fiecare pagină și fiecare obiect, ceea ce este exact ce necesită InsertPagesFromDocument, MovePage și reserializarea prin SaveLoadedDocument. Nu există nicio scurtătură pentru o restructurare autentică; trebuie să dețineți documentul ca să îl reorganizați

Problemele încep atunci când dimensiunile intrărilor nu sunt sub controlul dumneavoastră. Încărcările de la clienți, rezultatele scanerelor și arhivele vechi de un deceniu ignoră orice a presupus corpusul dumneavoastră de teste. Încărcați fiecare intrare necondiționat, iar plafonul de memorie este stabilit de cel mai mare fișier pe care cineva îl va trimite vreodată. Timpul de parsare urmărește numărul de obiecte, iar memoria rezidentă se stabilizează la un multiplu al dimensiunii fișierului, după ce sunt numărate structurile de obiecte și fluxurile decodate, astfel încât un gigaoctet pe disc poate însemna mai mulți gigaocteți rezidenți

Recompilarea pentru 64 de biți ridică plafonul spațiului de adrese, dar lasă nota de plată intactă. Worker-ul tot arde secunde de CPU și un multiplu al fișierului în RAM pentru a răspunde la o întrebare la care propria structură a fișierului ar fi putut răspunde în milisecunde. Sub concurență, matematica devine ostilă: patru încărcări mari care rulează simultan partajează un singur buget de memorie, iar throughput-ul se prăbușește exact atunci când coada este cea mai adâncă și vă puteți permite cel mai puțin acest lucru

Citirea unui fișier printr-un handle

Nivelul doar-citire deschide un fișier ca handle, răspunde la întrebări structurale despre el și îl închide. Niciun arbore de obiecte, nicio randare de pagini, nicio memorie care crește odată cu intrarea

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;

Trei obiceiuri mențin acest nivel corect. Primul, verificați valoarea returnată. Un handle nepozitiv înseamnă că deschiderea a eșuat, iar declanșarea DAGetPageCount pe un handle mort este genul de bug care rămâne ascuns până în ziua în care un client trimite un fișier malformat. Al doilea, împerecheați fiecare deschidere reușită cu DACloseFile într-un bloc finally; un serviciu care scapă handle-uri nu se prăbușește, ci doar putrezește, ceea ce este mai rău. Al treilea, respectați ce face de fapt parametrul de parolă. DAOpenFileReadOnly acceptă unul, dar pentru intrări criptate coboară pe tăcute la un parsing complet ca să citească numărul de pagini, așa că garanția de memorie constantă se evaporă. Rutați mai întâi fișierele protejate prin DecryptFile și restul fluxului rămâne ieftin

Aceeași sondă funcționează și ca poartă de triaj. Fișierele apar etichetate greșit, încărcate pe jumătate sau redenumite dintr-un cu totul alt format, iar o verificare DAOpenFileReadOnly le respinge pe toate chiar la ușa de intrare, în milisecunde, cu eroarea fixată pe fișierul vinovat. Alternativa este să lăsați un fișier defect să meargă adânc într-un worker din coadă și să explodeze acolo, unde descâlcirea intrării care l-a cauzat poate costa o după-amiază întreagă

Copierea, decriptarea și criptarea fișierelor întregi

Al doilea nivel mută și transformă fișiere complete fără a le expune vreodată internele. Acestea sunt apelurile pe care fluxurile de recepție se bazează cel mai mult

// Copiere structurală: validează-și-mută fără a parsa arborele de obiecte
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Decriptează în timpul copierii: ruta Direct File pentru intrări protejate
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Criptează în timpul copierii: protejează o ieșire fără o încărcare completă
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

Fiecare apel își câștigă locul. DACopyFile este copia validată dintr-un director de carantină în stocarea gestionată: deschide și indexează structura PDF pe măsură ce înaintează, astfel încât o intrare trunchiată sau care nu este PDF eșuează chiar aici, nu trei etape mai încolo. DecryptFile scrie o copie decriptată printr-o cale de rescriere AES-256 directă, care sare peste arborele de obiecte ori de câte ori intrarea permite, echivalentul pentru fișiere mari al fluxului de decriptare încarcă-și-resalvează tratat în articolul despre criptarea AES-256. EncryptFile rulează aceeași mișcare invers, aplicând protecție prin parolă în timpul unei copieri la nivel de fișier, cu parametrii de tip de cheie și permisiune pe care calea din memorie îi folosește deja

Adăugarea modificărilor în loc de rescriere

Actualizarea incrementală, definită în ISO 32000-1 §7.5.6, este al treilea nivel. Octeții originali rămân acolo unde sunt pe disc, iar orice obiecte noi sau modificate sunt adăugate după ele, urmate de o secțiune proaspătă de referințe încrucișate care se înlănțuie înapoi la original. Pentru o arhivă de 900 MB căreia trebuie să i se adauge o singură pagină, costul de scriere este delta, nu întregul fișier

Anatomia unei actualizări incrementale PDF produse de HotPDF: octeții originali rămân neatinși, în timp ce se adaugă un delta de obiecte noi și o secțiune de referințe încrucișate înlănțuită, iar reviziile anterioare rămân recuperabile până când o rescriere completă SaveLoadedDocument le elimină
Salvările incrementale adaugă doar delta lor și păstrează reviziile anterioare, deci compactarea este o rescriere separată și deliberată
// Adaugă o pagină de audit la o arhivă mare fără a o rescrie
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');  // octeții originali + delta

Două puncte de disciplină contează aici. BeginIncrementalUpdate trebuie să indice fișierul original, deoarece datele de referințe încrucișate adăugate se înlănțuiesc înapoi la offset-uri de octeți din interiorul lui. Iar modelul este append-only prin proiectare: fiecare salvare incrementală mărește fișierul, niciodată nu îl micșorează. Un document ștampilat în fiecare noapte se va umfla nelimitat până când o reserializare periodică, încărcându-l și rescriindu-l prin SaveLoadedDocument, îl compactează. Aceeași natură append-only este ceea ce face din actualizarea incrementală singurul mod sigur de a atinge un document semnat digital, o constrângere examinată în articolul despre semnăturile digitale și PAdES. Mecanismul de referințe încrucișate de dedesubt primește propriul tratament în articolul despre fluxuri de obiecte și actualizări incrementale

Există o capcană în salvările append-only care scapă majorității revizuirilor. Octeții originali rămân în fișier, lizibili pentru oricine este dispus să caute. O actualizare incrementală care "înlocuiește" o pagină nu o șterge pe cea veche; o înlocuiește în revizia curentă, în timp ce revizia anterioară stă acolo, complet recuperabilă. Așa că actualizările incrementale sunt instrumentul greșit pentru eliminarea conținutului sensibil. Pentru a elimina cu adevărat un istoric pe care un destinatar nu ar trebui să îl vadă niciodată, aveți nevoie de o reserializare completă: LoadFromFile urmat de SaveLoadedDocument, care scrie doar starea curentă și lasă în urmă reviziile îngropate

Potrivirea nivelului cu operația

Logica de selecție este suficient de scurtă încât să încapă în minte, iar merită codificată ca o decizie explicită de rutare în vârful unui flux, în loc să lăsați fiecare operație să-și improvizeze propria cale. Operația de care aveți nevoie decide nivelul:

  • Numărare, inspectare sau clasificare deschide un handle: DAOpenFileReadOnly, DAGetPageCount, DACloseFile
  • Mutarea, decriptarea sau criptarea unui fișier întreg rămâne la nivel de fișier cu DACopyFile, DecryptFile sau EncryptFile
  • Restructurarea paginilor sau combinarea documentelor necesită încărcarea completă: LoadFromFile, apoi InsertPagesFromDocument sau MovePage, apoi SaveLoadedDocument
  • Adăugarea unei mici delta la un fișier uriaș sau semnat apelează BeginIncrementalUpdate și salvează

Fluxurile mixte fac bine să pună un prag de dimensiune înaintea căii de încărcare completă. Trimiteți orice depășește câteva sute de megaocteți prin nivelurile Direct File și rezervați încărcarea completă pentru restructurarea autentică, pe un worker de 64 de biți cu un buget real de memorie. Pragul transformă o prăbușire out-of-memory într-o decizie de rutare pe care o puteți vedea și ajusta

Indiferent ce nivel gestionează o operație, scrieți rezultatul acesteia sub un nume temporar și redenumiți-l la locul final abia după ce rezultatul este validat. Un fișier scris pe jumătate, aflat sub numele final, arată exact ca unul bun pentru următoarea etapă a fluxului, iar apelurile Direct File fac verificarea ieftină: confirmarea unei ieșiri este o sondă de handle de o singură linie

Direct File API vine ca parte din HotPDF Delphi Component pentru Delphi și C++Builder. Pagina de produs face legătura către referința completă a funcțiilor, inclusiv apelurile de actualizare incrementală prezentate aici