Teknisk artikel

Behandling af store PDF'er i Delphi med HotPDF Direct File API

At tælle siderne i et 1,4 GB scannet arkiv burde være billigt. Kald LoadFromFile på den fil, og det holder op med at være billigt: HotPDF parser cross-reference-dataene og opbygger et objekt i hukommelsen for hvert eneste af dokumentets flere hundrede tusinde indirekte objekter, og en 32-bit worker rammer 2 GB-adresserumsloftet et sted midt i den parsing. Den operation, du ønskede, sidetallet, havde aldrig brug for nogen af de objekter. Den havde brug for sidetræet og intet andet. Det hul, mellem hvad et job beder om, og hvad en fuld indlæsning leverer, er hele grunden til, at Direct File API'et findes

Direct File API'et giver Delphi og C++Builder fil-niveau-adgang til en PDF: sidetal, kopier, dekryptering, inkrementelle tilføjelser, alt sammen ved at læse fra disk netop det, de rent faktisk har brug for, frem for at genopbygge hele dokumentmodellen i RAM. Kunsten er at matche hvert job med det letteste niveau, der kan besvare det. Får man det match rigtigt, holder en tjeneste flad hukommelse på tværs af enhver inputstørrelse. Får man det galt, tager den første overdimensionerede fil workeren ned

Jobrouting-diagram for HotPDF Direct File API i Delphi: read-only handle-prøver, helfilkopiering og kryptering, inkrementelle appends og fulde in-memory indlæsninger rangeret efter hukommelsesprofil
Match hver operation til det letteste niveau, der kan besvare den, og hold resident hukommelse flad uanset inputstørrelse

Hvad en fuld indlæsning koster dig

LoadFromFile er ikke fjenden. Den fortjener sin hukommelse: når først træet er i RAM, har man tilfældig adgang til hver side og hvert objekt, hvilket er præcis, hvad InsertPagesFromDocument, MovePage og reserialisering via SaveLoadedDocument kræver. Der er ingen genvej til reel omstrukturering; man er nødt til at holde dokumentet for at omarrangere det

Problemerne begynder, når inputstørrelser ikke er noget, du kontrollerer. Kundeuploads, scanneroutput og arkiver fra et årti tilbage er ligeglade med, hvad dit testkorpus antog. Indlæs alt input ubetinget, og dit hukommelsesloft bliver sat af den enkelt største fil, nogen nogensinde indsender. Parsingtiden følger objektantallet, og den resident hukommelse lander på flere gange filstørrelsen, når objektstrukturer og afkodede streams tælles med, så én gigabyte på disk kan betyde flere gigabyte resident

At genkompilere til 64-bit hæver adresserumsloftet, men lader regningen stå urørt. Workeren brænder stadig sekunder af CPU og et multiplum af filen i RAM for at besvare et spørgsmål, filens egen struktur kunne have besvaret på millisekunder. Under samtidighed vender regnestykket sig fjendtligt: fire store indlæsninger, der kører på samme tid, deler ét hukommelsesbudget, og gennemløbet styrter sammen præcis, når køen er dybest, og man har mindst råd til det

At læse en fil gennem et handle

Det skrivebeskyttede niveau åbner en fil som et handle, besvarer strukturelle spørgsmål om den og lukker den igen. Intet objekttræ, ingen sidegengivelse, ingen hukommelse, der vokser med inputtet

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 vaner holder dette niveau ærligt. For det første, tjek returværdien. Et ikke-positivt handle betyder, at åbningen fejlede, og at fyre DAGetPageCount af mod et dødt handle er den slags fejl, der forbliver skjult, indtil den dag en kunde sender en fejlformet fil. For det andet, par hver vellykket åbning med DACloseFile inde i en finally-blok; en tjeneste, der lækker handles, går ikke ned, den bare rådner, hvilket er værre. For det tredje, respektér hvad adgangskodeparameteren rent faktisk gør. DAOpenFileReadOnly accepterer en, men for krypterede inputs falder den stille tilbage til en fuld parsing for at læse sidetallet, så garantien om flad hukommelse fordamper. Rut beskyttede filer gennem DecryptFile først, og resten af pipelinen forbliver billig

Den samme probe fungerer også som en triage-port. Filer dukker op fejlmærkede, halvt uploadede eller omdøbt fra et helt andet format, og et DAOpenFileReadOnly-tjek afviser alle disse ved fordøren på millisekunder, med fejlen fastgjort til den skyldige fil. Alternativet er at lade en skrald-fil ride dybt ind i en køworker og eksplodere der, hvor det at nøste op i, hvilket input der forårsagede det, kan koste en eftermiddag

Kopiér, dekryptér og kryptér hele filer

Det andet niveau flytter og transformerer komplette filer uden nogensinde at eksponere deres indre. Dette er de kald, intake-pipelines læner sig mest op ad

// Strukturel kopi: valider-og-flyt uden at parse objekttræet
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Dekryptér under kopiering: Direct File-vejen ind i beskyttede inputs
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Kryptér under kopiering: beskyt et output uden en fuld indlæsning
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

Hvert kald fortjener sin plads. DACopyFile er den validerede kopi fra en karantænemappe til administreret lagring: den åbner og indekserer PDF-strukturen undervejs, så et afkortet eller ikke-PDF-input fejler lige her frem for tre trin længere nede i strømmen. DecryptFile skriver en dekrypteret kopi via en direkte AES-256-omskrivningssti, der springer objekttræet over, når input tillader det, storfil-modstykket til indlæs-og-gem-igen-dekrypteringsflowet, der dækkes i AES-256-krypteringsartiklen. EncryptFile kører den samme bevægelse omvendt, idet den anvender adgangskodebeskyttelse under en kopiering på filniveau med de nøgletype- og tilladelsesparametre, den in-memory-baserede vej allerede bruger

At tilføje ændringer i stedet for at omskrive

Inkrementel opdatering, defineret i ISO 32000-1 §7.5.6, er det tredje niveau. De oprindelige bytes bliver, hvor de er på disk, og alle nye eller ændrede objekter tilføjes efter dem, efterfulgt af et frisk cross-reference-afsnit, der kæder tilbage til originalen. For et 900 MB-arkiv, der skal have tilføjet en enkelt side, er skriveomkostningen deltaet, ikke hele filen

Anatomi af en PDF inkrementel opdatering produceret af HotPDF: originale bytes forbliver urørte, mens en delta af nye objekter og en lænket krydsreferencesektion tilføjes, og tidligere revisioner forbliver gendannbare, indtil en fuld SaveLoadedDocument-omskrivning dropper dem
Inkrementelle gemninger tilføjer kun deres delta og bevarer tidligere revisioner, så compaction er en separat, bevidst omskrivning
// Tilføj en revisionsside til et stort arkiv uden at omskrive 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');  // oprindelige bytes + delta

To disciplinpunkter betyder noget her. BeginIncrementalUpdate skal pege på den oprindelige fil, da de tilføjede cross-reference-data kæder tilbage til byte-offsets inden i den. Og modellen er append-only af design: hver inkrementel gemning gør filen større, aldrig mindre. Et dokument, der stemples hver nat, vil vokse uden grænser, indtil en periodisk reserialisering, der indlæser det og skriver det tilbage via SaveLoadedDocument, komprimerer det ned. Den samme append-only-natur er, hvad der gør inkrementel opdatering til den eneste sikre måde at røre ved et digitalt signeret dokument på, en begrænsning, der undersøges i artiklen om digitale signaturer og PAdES. Det underliggende cross-reference-maskineri får sin egen behandling i artiklen om objektstrømme og inkrementelle opdateringer

Der er en fælde i append-only-gemninger, der slipper forbi de fleste gennemgange. De oprindelige bytes bliver i filen, læsbare for enhver, der er villig til at kigge. En inkrementel opdatering, der "erstatter" en side, sletter ikke den gamle; den overtrumfer den i den aktuelle revision, mens den forrige revision sidder der, fuldt genopretteligt. Så inkrementelle opdateringer er det forkerte værktøj til at fjerne følsomt indhold. For reelt at droppe historik, en modtager aldrig bør se, har man brug for en fuld reserialisering: LoadFromFile efterfulgt af SaveLoadedDocument, som kun skriver den aktuelle tilstand ud og efterlader de begravede revisioner

At matche niveauet til operationen

Udvælgelseslogikken er kort nok til at kunne holdes i hovedet, og det betaler sig at kode den som en eksplicit routing-beslutning øverst i en pipeline i stedet for at lade hvert job improvisere sin egen vej. Operationen, du har brug for, afgør niveauet:

  • Tælle, inspicere eller klassificere åbner et handle: DAOpenFileReadOnly, DAGetPageCount, DACloseFile
  • At flytte, dekryptere eller kryptere en hel fil forbliver på filniveau med DACopyFile, DecryptFile eller EncryptFile
  • At omstrukturere sider eller flette dokumenter kræver den fulde indlæsning: LoadFromFile, derefter InsertPagesFromDocument eller MovePage, derefter SaveLoadedDocument
  • At tilføje et lille delta til en enorm eller signeret fil kalder BeginIncrementalUpdate og gemmer

Blandede pipelines gør klogt i at sætte en størrelsestærskel foran den fulde indlæsningsvej. Send alt, der overstiger et par hundrede megabyte, gennem Direct File-niveauerne, og reservér den fulde indlæsning til reel omstrukturering på en 64-bit worker med et reelt hukommelsesbudget. Tærsklen omdanner et out-of-memory-nedbrud til en routing-beslutning, man kan se og tune

Uanset hvilket niveau der håndterer et job, så skriv dets output til et midlertidigt navn, og omdøb det først på plads, når resultatet er valideret. En halvt skrevet fil, der ligger under det endelige navn, ser præcis ud som en god en for pipelinens næste trin, og Direct File-kaldene gør tjekket billigt: at bekræfte et output er en enkelt-linje handle-probe

Direct File API'et leveres som en del af HotPDF Delphi Component til Delphi og C++Builder. Produktsiden linker til den fulde funktionsreference, herunder de inkrementel-opdatering-kald, der er vist her