Teknisk artikel

Fletning og opdeling af gigabyte-PDF'er i Delphi med PDF Library for Delphi Direct Access

At flette eller opdele en to gigabyte stor PDF på den oplagte måde koster dig to ting på én gang: kalendertid og adresserum. Den oplagte måde er at indlæse hvert input, udføre arbejdet og skrive outputtet. Det er indlæsningen, der knækker. Et scanningsarkiv, der går fra 300 til 600 DPI, fordobler sin lineære opløsning og bliver omtrent firedoblet på disken, så det samme samlejob, der klarede filer på 400 MB hele året, begynder at hakke i det, i samme øjeblik et input passerer en gigabyte, ofte mens det ikke laver andet end at tælle sider. Opgaven blev aldrig sværere. Åbn, tæl, vælg intervaller, sammenkæd, det er hele historien. Indlæsning af hele træet holdt bare op med at være et fornuftigt standardvalg ved den størrelse. PDF Library for Delphi, losLabs PDF-bibliotek til Delphi og C++Builder, svarer på det med sit Direct Access-lag: en familie af DA-præfiksede funktioner understøttet af en streaming-læser, der vandrer gennem krydsreferencetabellen på stedet i stedet for at bygge hele dokumentet i hukommelsen

Hvor hukommelsen forsvinder hen ved en fuld indlæsning

At indlæse en PDF "normalt" betyder at parse xref, opløse hvert indirekte objekt til et træ i hukommelsen, afkode objekt-streams og koble sidetræet, skrifttyperne og annotationerne sammen til objekter, du kan manipulere. Til redigeringsarbejdsgange er det den rigtige handel. Til fletning, opdeling og inspektion er det for det meste spild. Et scanningsarkiv på 30.000 sider kan rumme millioner af indirekte objekter, og et opdelingsjob har brug for at læse nogle få hundrede af dem: sidenoderne i det ønskede interval plus det, de noder refererer til

Direct Access-laget vender modellen om. DAOpenFile og DAOpenFileReadOnly parser traileren og xref, nogle få kilobyte i halen af filen, og returnerer et fil-handle. Objekter hentes dovent, når et kald har brug for dem. Den praktiske konsekvens er, at det tager omtrent lige så lang tid at åbne en fil på flere gigabyte som en lille, og at hukommelsen følger det, du rører ved, frem for det, filen indeholder

PDF Library for Delphi-sammenligning af at indlæse en gigabyte-PDF i et fuldt in-memory objekttræ versus at åbne den med direct access, hvor parsing stopper ved trailer og xref, og et handle betjener dovne per-objekt-læsninger
En fuld indlæsning dekoder hvert indirekte objekt, før sammenfletningen kan starte, så RAM og åbningstid skalerer med arkivet. Direkte adgang returnerer et fungerende handle efter at have læst kilobytes og lader hvert kald trække lige netop de objekter, det behøver

At sondere en enorm fil uden at indlæse den

Mønstret nedenfor stammer fra bibliotekets egen benchmark for store filer: åbn skrivebeskyttet, stil spørgsmål, luk. Der findes aldrig noget dokumenttræ

var
  Lib: TPDFlib;
  Handle, Pages: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
    if Handle = 0 then
      raise Exception.Create('Direct access open failed');
    Pages := Lib.DAGetPageCount(Handle);
    Writeln('pages : ', Pages);
    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
    Lib.DACloseFile(Handle);
  finally
    Lib.Free;
  end;
end;

Skrivebeskyttet tilstand er værd at foretrække, hver gang du kan: den lader indtagstrinnet køre, mens andre processer holder filen, og den dokumenterer hensigten. Et sonderingstrin, der ved et uheld kalder en muterende funktion, fejler hurtigt i stedet for at ødelægge arkivet

PageRef er et objekt-handle, ikke et sidetal

Den absolut hyppigste fejl med DA-API'et er at sende et sidetal, hvor en funktion forventer en PageRef. Næsten hvert DA-kald per side tager et referencehandle til sideobjektet frem for et sidetal: DAExtractPageText, DARenderPageToFile, DARotatePage og DACapturePage forventer alle en ref. Du får en ved at oversætte det menneskevendte tal gennem DAFindPage:

PDF Library for Delphi: Sidenummer-til-PageRef-oversættelsesflow, der viser DAFindPage føde de per-side direct access-kald, versus en rå heltalsreference, der lander på et tilfældigt objekt og producerer lydløs forkertsidetekst
Hvert direkte adgangs-kald pr. side bruger en PageRef produceret af DAFindPage, aldrig det menneskelige nummer. Springes denne oversættelse over, udklæder heltallet sig som et objekt-id, og tekst fra forkert side kan sendes usynligt af sted
PageRef := Lib.DAFindPage(Handle, 250);          // sidetal -> objekt-handle
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

At sende det rå tal 250 i stedet udløser ingen fejl. Det adresserer, hvilket objekt der nu tilfældigvis ligger bag den handle-værdi, hvilket på en god dag fejler synligt og på en dårlig dag udtrækker tekst fra den forkerte side ind i et dokument, kunden får at se. Pakker du DA-laget ind i din egen tjenestekode, så gør oversættelsen umulig at springe over: tag imod sidetal ved grænsen, kald DAFindPage med det samme, og send kun refs videre internt

Fletning af hundredvis af filer med en navngiven liste

Til to filer er MergeFiles(First, Second, Output) nok. Batch-samling skalerer bedre gennem fillister: registrér inputtene under et listenavn, og flet så listen i ét gennemløb

PDF Library for Delphi: Navngivet fillistearbejdsgang, hvor januar-, februar- og martsopgørelser registreres under ét listenavn og flettes i ét gennemløb, med Fast-, standard- og strict-varianternes afvejning mellem strukturtræbevarelse og hastighed
Hundreder af registrerede input smelter sammen til ét enkelt MergeFileList-forløb, hvis resultat verificeres på millisekunder via endnu et skrivebeskyttet eftersyn. Varianten er en beslutning pr. pipeline, fordi Fast dropper Tagged PDF-strukturtræet
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Kontrollér resultatet på den billige måde: direct access igen
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

Flettefamilien har tre varianter, og forskellen er ikke hastighed alene. MergeFileListFast springer bevarelsen af strukturtræet over; MergeFileListStrict gennemtvinger streng tilstand; versionen uden suffiks er det afbalancerede standardvalg. Den driftsregel, der falder ud af det: er blot ét input en Tagged PDF, hvis tilgængelighedsstruktur skal overleve, hvad alt produceret til PDF/UA er det oplagte eksempel på, så grib efter standard- eller Strict-varianten, for Fast smider stiltiende strukturtræet væk. Til rene scanningsarkiver uden tagging er Fast gratis ydelse. Beslut det per pipeline, ikke per udviklerhumør, og notér den anvendte variant i joblogen

Opdeling uden indlæsning: udtræk af intervaller

Opdeling følger den samme filosofi om ingen indlæsning. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) trækker et sideinterval direkte fra fil til fil, med en intervalliste som '1-500', '501-1000' eller kommaseparerede udvalg, og kilden bliver aldrig til et dokumenttræ. Når et dokument allerede er indlæst af andre grunde, producerer ExtractPageRanges et nyt dokument i hukommelsen ud fra det aktuelle, og CopyPageRanges trækker intervaller over fra et andet indlæst dokument via ID. Til opdeling af konsoliderede printstrømme per kontoudtog er formen fra fil til fil den, der forhindrer et input på 4 GB i nogensinde at puste sig op i RAM

Filer, der lyver om deres geometri

Pipelines til store filer møder beskadigede filer i et omfang, pipelines til små filer aldrig ser, ganske enkelt fordi inputtene passerer gennem flere systemer. To fejlformer fortjener udtrykkelig håndtering

For det første forskudte headere. Mail-gateways og printkøer sætter undertiden bytes foran en PDF, så %PDF-markøren ikke længere ligger på offset 0, og hvert eneste xref-offset i filen er forkert med det samme beløb. Streaming-læseren opdager det og blotlægger det (DAShiftedHeader på det flade niveau, ShiftedHeaderTSmartPDFReader) og kompenserer derefter for det under læsningerne. Hjemmelavet offset-regnestykke gør typisk ikke, og det er derfor, "virker på hver eneste fil, vi selv genererer, fejler på filer fra kunde X" er det klassiske symptom

For det andet ødelagte krydsreferencetabeller. DACopyFile(InputFileName, OutputFileName, PageCount) streamer hele filen over i en ny kopi, mens den genopbygger xref, og returnerer sideantallet som biprodukt. Kører man den som et normaliseringstrin foran en kræsen aftager længere nede, forvandles en hel klasse af sporadiske parsefejl til ét forudsigeligt reparationstrin. Og når dine egne redigeringer skal gemmes, skriver DAAppendFile dem som en inkrementel opdatering og tilføjer en ny revision frem for at omskrive gigabyte, hvilket holder omkostningen ved at gemme proportional med ændringen i stedet for med filen

Leveringsdetaljer: linearisering og komposition

To beslægtede evner runder en pipeline til store filer af. Når det samlede output leveres over HTTP til visning i browseren, reorganiserer LinearizeFile det til byte range-streaming, så den første side vises, før resten af en pakke på 500 MB er hentet færdig. Kør den som sidste trin, efter al fletning, for enhver senere ændring af-lineariserer filen igen. Og når pakker kræver komposition frem for almindelig sammenkædning, lad os sige et forsatsblad stemplet bag hvert kontoudtog eller to kildesider sat op på ét outputark, forvandler DACapturePage enhver side til en genbrugelig skabelon, som DADrawCapturedPage placerer på en destinationsside i et vilkårligt rektangel, stadig uden en fuld dokumentindlæsning af kilden på flere gigabyte

Grænser, og hvad der forbliver skrivebeskyttet

Selve formatet løber tør for plads længe før Direct Access gør det. Offsets er Int64 hele vejen gennem DA-laget, så de reelle lofter er den tilgængelige disk og det 10-cifrede xref-offsetfelt i klassiske krydsreferencetabeller uden streams. Scanningsarkiver på flere gigabyte er i praksis ganske uinteressante, og hukommelsen forbliver afgrænset uanset filstørrelse, fordi objekter kun læses, når et kald beder om dem

To spørgsmål dukker op ofte nok til at fortjene et direkte svar. Fletning gennem standardstien fører dokumentstrukturen med over, så bogmærker og links overlever; Fast-varianten er den, der bytter strukturtræet for hastighed, hvilket er hele grunden til at reservere den til inputs uden tagging. Den sikre vane er at åbne det flettede output, gennemgå dets disposition og stikprøvekontrollere et par interne links, før det sendes af sted. Hvad angår redigering: der findes en nyttig mellemvej mellem skrivebeskyttet sondering og en fuld indlæsning. Operationer på sideniveau arbejder direkte på handlet, blandt dem DARotatePage, DAMovePage og DAHidePage, sammen med læsning af formularfelter, og DAAppendFile gør de redigeringer varige som en inkrementel revision. Redigering på indholdsniveau, alt der omskriver markeringsoperatorerne inde i en side, hører stadig til i det fulde dokumentlag

Relaterede artikler

Skal dit flettede output forblive tilgængeligt, dækkes baggrunden om strukturtræet i artiklen om tilgængelighed i Tagged PDF, som forklarer nøjagtigt, hvad Fast-flettevarianten ville kassere. Vil du trække indhold ud af de intervaller, du opdeler, så se guiden til udtræk af tekst, billeder og skrifttyper

Den fulde liste over Direct Access-funktioner følger med biblioteket; udgaver og prøveversioner findes på PDF Library for Delphi-produktsiden