Teknisk artikel

HotPDF: Direct File API-behandling för stora PDF-filer i Delphi

Att räkna sidorna i ett 1,4 GB skannat arkiv borde vara billigt. Anropa LoadFromFile på den filen och det slutar vara billigt: HotPDF parsar korsreferensdatan och bygger ett minnesobjekt för vart och ett av dokumentets flera hundra tusen indirekta objekt, och en 32-bitars arbetare slår i taket på 2 GB adressutrymme någonstans mitt i den parsningen. Operationen du ville ha, sidantalet, behövde aldrig något av de objekten. Den behövde sidträdet och inget annat. Den klyftan, mellan vad ett jobb begär och vad en full laddning levererar, är hela skälet till att Direct File API finns

Direct File API ger Delphi och C++Builder filnivåsåtkomst till en PDF: sidantal, kopior, dekryptering, inkrementella tillägg, allt läsande från disk det de faktiskt behöver i stället för att rekonstruera hela dokumentmodellen i RAM. Konsten är att matcha varje jobb mot den lättaste nivå som kan besvara det. Får du den matchningen rätt håller en tjänst platt minne över vilken indatastorlek som helst. Får du den fel tar den första överdimensionerade filen ner arbetaren

Jobbroutingsdiagram för HotPDF Direct File API i Delphi: read-only-handtags-sonderingar, kopia och kryptering av hela filen, inkrementella tillägg, och fullständiga inläsningar i minnet rankade efter minnesprofil
Matcha varje operation mot den lättaste nivån som kan besvara den och håll det residuära minnet platt oavsett indatastorlek

Vad en full laddning kostar dig

LoadFromFile är inte fienden. Den förtjänar sitt minne: när trädet väl är i RAM har du slumpmässig åtkomst till varje sida och varje objekt, vilket är precis vad InsertPagesFromDocument, MovePage och omserialisering via SaveLoadedDocument kräver. Det finns ingen genväg för genuin omstrukturering; du måste hålla dokumentet för att kunna ordna om det

Problemen börjar när indatastorlekar inte är dina att styra över. Kunduppladdningar, skannerutdata och arkiv från ett decennium sedan struntar i vad ditt testkorpus antog. Ladda varje indata ovillkorligt och ditt minnestak sätts av den enskilt största filen någon någonsin kommer att skicka in. Parsningstid följer objektantal, och resident minne slår sig ner på flera gånger filstorleken när objektstrukturer och avkodade strömmar räknas in, så en gigabyte på disk kan betyda flera gigabyte resident

Att kompilera om för 64-bitar lyfter adressutrymmestaket men lämnar räkningen intakt. Arbetaren bränner fortfarande sekunder av CPU och en multipel av filen i RAM för att besvara en fråga filens egen struktur kunde ha besvarat på millisekunder. Under samtidighet blir matematiken fientlig: fyra stora laddningar som körs samtidigt delar en minnesbudget, och genomströmningen kraschar precis när kön är som djupast och du minst har råd med det

Att läsa en fil via ett handtag

Den skrivskyddade nivån öppnar en fil som ett handtag, besvarar strukturella frågor om den, och stänger den. Inget objektträd, ingen sidrendering, inget minne som växer med indatan

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;

Tre vanor håller den här nivån ärlig. Först, kontrollera returvärdet. Ett icke-positivt handtag betyder att öppningen misslyckades, och att avfyra DAGetPageCount mot ett dött handtag är den sortens bugg som förblir dold till den dag en kund skickar en felformad fil. För det andra, para ihop varje lyckad öppning med DACloseFile inuti ett finally-block; en tjänst som läcker handtag kraschar inte, den bara ruttnar, vilket är värre. För det tredje, respektera vad lösenordsparametern faktiskt gör. DAOpenFileReadOnly accepterar en, men för krypterade indata faller den tyst tillbaka till en full parsning för att läsa sidantalet, så det platta minnesgarantiet avdunstar. Dirigera skyddade filer via DecryptFile först och resten av pipelinen förblir billig

Samma sond dubblar som en triagegrind. Filer dyker upp felmärkta, halvuppladdade, eller omdöpta från ett helt annat format, och en DAOpenFileReadOnly-kontroll avvisar alla dessa vid ytterdörren på millisekunder, med felet fastnålat vid den skyldiga filen. Alternativet är att låta en skräpfil rida djupt in i en köarbetare och explodera där, där det kan kosta en eftermiddag att reda ut vilken indata som orsakade det

Kopiera, dekryptera och kryptera hela filer

Den andra nivån flyttar och omvandlar kompletta filer utan att någonsin exponera deras insidor. Det här är anropen intagspipelines lutar sig mest mot

// Strukturell kopia: validera-och-flytta utan att parsa objektträdet
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Dekryptera under kopiering: Direct File-vägen in i skyddad indata
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Kryptera under kopiering: skydda en utdata utan en full laddning
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

Varje anrop förtjänar sin plats. DACopyFile är den validerade kopian från en karantänkatalog in i hanterad lagring: den öppnar och indexerar PDF-strukturen allteftersom, så en trunkerad eller icke-PDF-indata misslyckas precis här snarare än tre steg nedströms. DecryptFile skriver en dekrypterad kopia längs en direkt AES-256-omskrivningsväg som hoppar över objektträdet närhelst indatan tillåter det, storfilsmotsvarigheten till ladda-och-spara-om-dekrypteringsflödet som täcks i artikeln om AES-256-kryptering. EncryptFile kör samma rörelse baklänges, och applicerar lösenordsskydd under en kopiering på filnivå med samma nyckeltyp- och behörighetsparametrar som minnesvägen redan använder

Att lägga till ändringar i stället för att skriva om

Inkrementell uppdatering, definierad i ISO 32000-1 §7.5.6, är den tredje nivån. De ursprungliga byten stannar där de är på disk, och alla nya eller ändrade objekt läggs till efter dem, följt av ett färskt korsreferensavsnitt som kedjar tillbaka till originalet. För ett 900 MB-arkiv som behöver en enda sida tillagd är skrivkostnaden deltat, inte hela filen

Anatomin hos en PDF inkrementell uppdatering skapad av HotPDF: ursprungliga byte förblir orörda medan en delta av nya objekt och ett länkat korsreferensavsnitt läggs till, och tidigare revisioner förblir återhämtningsbara tills en fullständig SaveLoadedDocument-omskrivning släpper dem
Inkrementella sparningar lägger bara till sin delta och bevarar tidigare revisioner, så komprimering är en separat medveten omskrivning
// Lägg till en granskningssida i ett stort arkiv utan att skriva om det
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');  // ursprungliga byte + delta

Två disciplinpunkter spelar roll här. BeginIncrementalUpdate måste peka på originalfilen, eftersom den tillagda korsreferensdatan kedjar tillbaka till byteförskjutningar inuti den. Och modellen är append-only av design: varje inkrementell sparning växer filen, krymper den aldrig. Ett dokument stämplat varje natt sväller obegränsat tills en periodisk omserialisering, att ladda det och skriva tillbaka det via SaveLoadedDocument, komprimerar det. Samma append-only-natur är det som gör inkrementell uppdatering till det enda säkra sättet att röra ett digitalt signerat dokument, en begränsning som undersöks i artikeln om digitala signaturer och PAdES. Det underliggande korsreferensmaskineriet får sin egen behandling i artikeln om objektströmmar och inkrementella uppdateringar

Det finns en fälla i append-only-sparningar som slinker förbi de flesta granskningar. De ursprungliga byten stannar kvar i filen, läsbara för vem som helst villig att titta. En inkrementell uppdatering som "ersätter" en sida raderar inte den gamla; den ersätter den i den aktuella revisionen medan den föregående revisionen ligger kvar, fullt återställningsbar. Så inkrementella uppdateringar är fel verktyg för att strippa känsligt innehåll. För att genuint släppa historik en mottagare aldrig ska se, behöver du en full omserialisering: LoadFromFile följt av SaveLoadedDocument, som bara skriver ut det aktuella tillståndet och lämnar de begravda revisionerna bakom sig

Att matcha nivån mot operationen

Urvalslogiken är kort nog att hålla i huvudet, och det lönar sig att koda den som ett explicit dirigeringsbeslut högst upp i en pipeline i stället för att låta varje jobb improvisera sin egen väg. Operationen du behöver avgör nivån:

  • Räkna, inspektera eller klassificera öppnar ett handtag: DAOpenFileReadOnly, DAGetPageCount, DACloseFile
  • Att flytta, dekryptera eller kryptera en hel fil stannar på filnivå med DACopyFile, DecryptFile eller EncryptFile
  • Att omstrukturera sidor eller slå ihop dokument behöver den fulla laddningen: LoadFromFile, sedan InsertPagesFromDocument eller MovePage, sedan SaveLoadedDocument
  • Att lägga till ett litet delta i en enorm eller signerad fil anropar BeginIncrementalUpdate och sparar

Blandade pipelines gör klokt i att sätta en storlekströskel framför den fulla laddningsvägen. Skicka allt förbi några hundra megabyte genom Direct File-nivåerna, och reservera den fulla laddningen för genuin omstrukturering på en 64-bitars arbetare med en verklig minnesbudget. Tröskeln omvandlar en minnesslutkrasch till ett dirigeringsbeslut du kan se och justera

Oavsett vilken nivå som hanterar ett jobb, skriv dess utdata till ett temporärt namn och byt namn på plats bara när resultatet validerats. En halvskriven fil som ligger under det slutgiltiga namnet ser precis ut som en bra en för pipelinens nästa steg, och Direct File-anropen gör kontrollen billig: att bekräfta en utdata är en enrads handtagssond

Direct File API levereras som en del av HotPDF Delphi Component för Delphi och C++Builder. Produktsidan länkar den fullständiga funktionsreferensen, inklusive de inkrementella uppdateringsanropen som visas här