Technical Article

Didelių PDF failų apdorojimas „Delphi“ aplinkoje su HotPDF Direct File API

Puslapių skaičiavimas nuskaitytame 1,4 GB archyve turėtų būti pigi operacija. Tačiau iškvietus LoadFromFile šiam failui, ji tokia nebėra: HotPDF apdoroja kryžminių nuorodų duomenis ir sukuria atmintyje objektą kiekvienam iš kelių šimtų tūkstančių netiesioginių dokumento objektų, o 32 bitų procesas kur nors įpusėjus nuskaitymui pasiekia 2 GB adresų erdvės ribą. Puslapių skaičiaus nustatymui nereikėjo nei vieno iš tų objektų. Reikėjo tik puslapių medžio ir nieko daugiau. Šis skirtumas tarp to, ko reikalauja užduotis, ir to, ką pateikia pilnas įkėlimas, yra pagrindinė priežastis, kodėl egzistuoja Direct File API.

Direct File API suteikia „Delphi“ ir „C++Builder“ failų lygio prieigą prie PDF: puslapių skaičiavimas, kopijavimas, dešifravimas, laipsniškas papildymas – visa tai atliekama skaitant iš disko tik tai, ko reikia, užuot atkūrus visą dokumento modelį operatyviojoje atmintyje (RAM). Pagrindinė užduotis yra parinkti kiekvienam darbui pačią paprasčiausią struktūrą, galinčią pateikti atsakymą. Teisingai parinkus, paslauga išlaiko pastovų atminties suvartojimą nepriklausomai nuo įvesties failo dydžio. Suklydus, pirmasis per didelis failas gali sutrikdyti viso proceso veikimą.

Kiek kainuoja pilnas failo įkėlimas

LoadFromFile nėra priešas. Atminties sąnaudos yra pateisinamos: kai medis yra operatyviojoje atmintyje, turite tiesioginę prieigą prie kiekvieno puslapio ir objekto, o to kaip tik ir reikia tokioms funkcijoms kaip InsertPagesFromDocument, MovePage bei pakartotiniam serijavimui per SaveLoadedDocument. Tikram dokumento struktūros pertvarkymui nėra trumpesnių kelių; turite laikyti dokumentą atmintyje, kad jį pertvarkytumėte.

Problemos prasideda tada, kai negalite kontroliuoti gaunamų failų dydžio. Klientų įkelti failai, skenerių rezultatai ir dešimtmečio senumo archyvai nepaiso jūsų testavimo metu darytų prielaidų. Jei besąlygiškai įkelsite kiekvieną failą, jūsų atminties riba bus nustatyta pagal patį didžiausią failą, kurį kas nors atsiųs. Apdorojimo laikas priklauso nuo objektų skaičiaus, o naudojama atmintis, įvertinus objektų struktūras ir iškoduotus srautus, gali kelis kartus viršyti failo dydį diske – gigabaitas diske gali virsti keliais gigabaitais atmintyje.

Programos perkompiliavimas į 64 bitų versiją panaikina adresų erdvės apribojimą, tačiau atminties sąnaudos išlieka tokios pačios. Procesas vis tiek eikvoja procesoriaus sekundes ir kelis kartus didesnį RAM kiekį nei pats failas, kad atsakytų į klausimą, į kurį paties failo struktūra galėjo atsakyti per milisekundes. Vykdant kelias užduotis lygiagrečiai, matematika tampa negailestinga: keturi vienu metu vykdomi dideli įkėlimai dalijasi tą patį atminties biudžetą, o pralaidumas krenta būtent tada, kai eilė yra didžiausia ir negalite sau leisti delsimo.

Failo skaitymas naudojant deskriptorių

Tik skaitymui skirtas lygis atidaro failą ir sukuria deskriptorių, atsako į struktūrinius klausimus apie jį ir jį uždaro. Jokio objektų medžio, jokio puslapių atvaizdavimo ir jokio atminties suvartojimo didėjimo priklausomai nuo įvesties.

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;

Trys įpročiai padeda užtikrinti šio lygio stabilumą. Pirmiausia, patikrinkite grąžinamą reikšmę. Neigiamas arba nulinis deskriptorius reiškia, kad failo atidaryti nepavyko, o funkcijos DAGetPageCount iškvietimas su negaliojančiu deskriptoriumi yra klaida, kuri gali likti nepastebėta iki tos dienos, kai klientas atsiųs sugadintą failą. Antra, kiekvieną sėkmingą atidarymą susiekite su DACloseFile naudodami finally bloką; paslauga, kurioje nuteka deskriptoriai, neužlūžta iškart, o tiesiog palaipsniui eikvoja išteklius. Trečia, atkreipkite dėmesį į slaptažodžio parametrą. DAOpenFileReadOnly jį priima, tačiau šifruotų įvesčių atveju biblioteka nepastebimai pereina prie pilno failo apdorojimo, kad perskaitytų puslapių skaičių, zodžiu, pastovios atminties garantija išnyksta. Nukreipkite apsaugotus failus per DecryptFile ir likusi proceso dalis išliks pigi.

Tas pats patikrinimas atlieka ir pirminio filtravimo funkciją. Failai gali būti pateikti su klaidingomis etiketėmis, nepilnai įkelti arba tiesiog pervardyti iš kito formato, o DAOpenFileReadOnly patikra atmeta juos iškart per kelias milisekundes, susiedama klaidą su konkrečiu sugadintu failu. Alternatyva yra leisti netinkamam failui nukeliauti giliai į eilės apdorojimo procesą ir ten sukelti klaidą, kurios priežasties ieškojimas gali kainuoti pusdienį.

Visų failų kopijavimas, iššifravimas ir užšifravimas

Antrasis lygis perkelia ir keičia ištisus failus, niekada neatskleisdamas jų vidinės struktūros. Tai yra iškvietimai, kuriais dažniausiai remiasi duomenų priėmimo procesai.

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

Kiekvienas iškvietimas turi savo paskirtį. DACopyFile atlieka patvirtintą kopijavimą iš karantino katalogo į valdomą saugyklą: kopijuojant atidaroma ir indeksuojama PDF struktūra, todėl nebaigtas įkelti arba ne PDF failas atmetamas iškart, o ne vėlesniuose etapuose. DecryptFile įrašo iššifruotą kopiją tiesioginiu AES-256 perrašymo keliu, kuris praleidžia objektų medį, kai įvestis tai leidžia; tai yra didelių failų atitikmuo įkėlimo ir pakartotinio išsaugojimo dešifravimo procesui, aprašytam AES-256 šifravimo straipsnyje. EncryptFile atlieka tą patį veiksmą atvirkštine tvarka, pritaikydama slaptažodžio apsaugą failo lygio kopijavimo metu su tais pačiais raktų ir leidimų parametrais, kuriuos naudoja atminties kelias.

Pakeitimų pridėjimas vietoj perrašymo

Laipsniškas atnaujinimas (angl. incremental update), apibrėžtas ISO 32000-1 §7.5.6, yra trečasis lygis. Pradiniai baitai lieka diske ten, kur buvę, o visi nauji ar pakeisti objektai pridedami po jų, pridedant naują kryžminių nuorodų sekciją, susietą su pradine. 900 MB archyvui, į kurį reikia pridėti vieną puslapį, įrašymo sąnaudas sudaro tik pridedamas skirtumas, o ne visas failas.

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

Čia svarbūs du disciplinos aspektai. Pasirinkimas BeginIncrementalUpdate turi nurodyti pradinį failą, nes pridėti kryžminių nuorodų duomenys susiejami su baitų poslinkiais jo viduje. Be to, šis modelis pagal savo struktūrą leidžia tik pridėti duomenis: kiekvienas laipsniškas išsaugojimas didina failą ir niekada jo nesumažina. Kasnakt žymimas dokumentas plėsis be ribų, kol periodinis pakartotinis serijavimas, įkeliant jį ir įrašant atgal per SaveLoadedDocument, jį suspaus. Dėl šios savybės laipsniškas atnaujinimas yra vienintelis saugus būdas keisti skaitmeniniu parašu pasirašytą dokumento versiją – šis apribojimas nagrinėjamas straipsnyje apie skaitmeninius parašus ir PAdES. Kryžminių nuorodų mechanizmas išsamiau aprašytas straipsnyje apie objektų srautus ir laipsniškus atnaujinimus.

Pridėjimo režimas turi pavojų, kurio daugelis nepastebi. Pradiniai baitai lieka faile ir yra matomi visiems, kas nori juos peržiūrėti. Laipsniškas atnaujinimas, kuris „pakeičia“ puslapį, neištrina senojo puslapio; jis tiesiog pakeičia jį dabartinėje versijoje, o ankstesnė versija lieka faile ir gali būti lengvai atkurta. Todėl laipsniški atnaujinimai nėra tinkamas įrankis konfidencialiam turiniui šalinti. Norint visiškai pašalinti istoriją, kurios gavėjas neturėtų matyti, reikia pilno pakartotinio serijavimo: iškviesti LoadFromFile, o po to – SaveLoadedDocument, kuri įrašo tik dabartinę būseną, o ankstesnes versijas palieka praeityje.

Atitinkamo lygio parinkimas pagal operaciją

Pasirinkimo logika yra pakankamai paprasta, todėl naudinga ją užkoduoti kaip aiškų maršruto parinkimo sprendimą duomenų apdorojimo pradžioje, užuot leidus kiekvienai užduočiai pačiai pasirinkti kelią. Reikalinga operacija nustato atitinkamą lygį:

  • puslapių skaičiavimui, patikrai ar klasifikavimui atidaromas deskriptorius: DAOpenFileReadOnly, DAGetPageCount, DACloseFile.
  • viso failo perkėlimui, dešifravimui ar užšifravimui naudojamas failo lygis: DACopyFile, DecryptFile arba EncryptFile.
  • puslapių struktūros keitimui ar dokumentų sujungimui reikalingas pilnas įkėlimas: LoadFromFile, tada InsertPagesFromDocument arba MovePage, ir galiausiai SaveLoadedDocument.
  • nedidelio pakeitimo pridėjimui prie didelio ar pasirašyto failo naudojama funkcija BeginIncrementalUpdate ir išsaugoma.

Mišriuose procesuose naudinga nustatyti dydžio ribą prieš pilno įkėlimo kelią. Siųskite visus failus, viršijančius kelis šimtus megabaitų, per Direct File lygius, o pilną įkėlimam palikite tikram struktūros pertvarkymui 64 bitų aplinkoje su pakankamu atminties biudžetu. Ši riba leidžia išvengti programos lūžimo dėl atminties trūkumo ir paverčia tai valdomu maršruto parinkimo sprendimu.

Nepriklausomai nuo to, kuris lygis apdoroja užduotį, įrašykite jos rezultatą laikinu pavadinimu ir pervardykite jį į nuolatinį tik tada, kai rezultatas bus sėkmingai patikrintas. Pusiau įrašytas failas, pavadintas galutiniu vardu, kitam apdorojimo etapui atrodys kaip tvarkingas failas, o Direct File iškvietimai leidžia pigiai atlikti patikrą: norint patvirtinti rezultatą, pakanka vienos eilutės deskriptoriaus patikrinimo.

Direct File API pateikiama kaip HotPDF komponento dalis, skirta „Delphi“ ir „C++Builder“. Produkto puslapyje pateikiama nuoroda į pilną funkcijų aprašymą, įskaitant čia parodytus laipsniško atnaujinimo iškvietimus.