Teknisk artikel

PDFium byte-interval-indlæsning af indlejrede PDF'er i Delphi

PDFium Component kan åbne en PDF, der lever inde i en større buffer, direkte fra et byte-interval. Overloadet LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresserer et vindue på plads, så ingen indledende Copy er nødvendig. Til gengæld beder det dig forstå én regel: når Buffered er False, er det bagvedliggende array lånt, ikke kopieret

Dette er en anden mekanisme end den callback-drevne tilgang beskrevet i streaming af store PDF'er on-demand med PDFium VCL, som giver PDFium en FPDF_FILEACCESS-læser og lader den trække blokke fra disk efter behov. Den er til dokumenter for store til at holde i RAM. Denne er til dokumenter, der allerede er i RAM, siddende ved en kendt offset inde i noget andet. De to komplementerer hinanden, og sidste afsnit forklarer, hvilken situation der hører til hvilken

De 40 MB kopi, ingen bad om

Scenariet dukker op, hvor end PDF'er rejser inde i andre formater. En mailstore holder beskedkroppe og vedhæftninger i én post. En arkivcontainer sammenkæder et manifest, nogle billeder og en PDF. En brugerdefineret wire-protokol indrammer et dokument bag et længdeprefikset hoved. I hvert tilfælde ender man med at holde ét stort TBytes og vide, at PDF'en starter ved byte 1 182 336 og løber i 312 kilobytes

Før byte-interval-overloadet fandtes, var det idiomatiske svar Copy(Data, Index, Count), som allokerer et andet array og memcpy'er vinduet ind i det. Man afleverer det så til LoadDocument med Buffered = True, som kopierer det igen ind i komponentens private buffer. To kopier af de samme bytes, én af dem ren ceremoni, og på en stor mailboks-scanning gentaget for hver besked. Byte-interval-overloadet fjerner den første kopi ubetinget og den anden kopi valgfrit

Sammenligningsdiagram, der viser den gamle dobbeltkopi-sti til en indlejret PDF inde i en container i Delphi versus PDFium Component byte-range LoadDocument-overloadet, der adresserer vinduet in place
Uden overloaden blev det samme 312 KB-vindue memcpyiet to gange; byte-range-kaldet adresserer det in place, og Buffered afgør, om noget overhovedet kopieres

Hvad byte-interval-overloadet egentlig gør

Overloadet er tyndt af design: det validerer, beregner én pointer og delegerer til pointer-formen af LoadDocument, som hele familien allerede kanaliserer gennem. Index er nul-baseret, Count er en bytelængde, og Buffered er som standard True nøjagtig som på de andre overloads. Enkelt-argument-LoadDocument(const Data: TBytes; Buffered: Boolean) er selv nu blot et kald til denne med Index = 0 og Count = Length(Data), så der er én valideringssti frem for to

At kalde den ligner den kode, man allerede skrev, minus udsnittet

var
  Frame: TBytes;          // hele container-posten, titusindvis af megabyte
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // din container-parser

  // Ingen Copy(Frame, Offset, Size) her - vinduet adresseres på stedet
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

Hvorfor giver Index plus Count overløb i grænsetjekket?

Fordi Index og Count begge er Integer, og summen af to store positive Integer-værdier ikke nødvendigvis er en stor positiv Integer. Dette er den tekniske kerne af overloadet, og det er det ene sted, hvor et naturligt udseende tjek er et hukommelsessikkerhedshul. Den oplagte formulering er forkert

// FORKERT: Index + Count beregnes i Integer og kan vende til negativ
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// RIGTIG: afvis fortegn først, og afgræns derefter hvert led separat,
// hvor den eneste aritmetik er en subtraktion, der ikke kan vende
Check(Index >= 0,  'PDF byte range index cannot be negative');
Check(Count >= 0,  'PDF byte range count cannot be negative');
Check(Index <= Length(Data), 'PDF byte range index exceeds data length');
Check(Count <= Length(Data) - Index, 'PDF byte range exceeds data length');

Følg det fejlende tilfælde igennem. Tag Index = 2000000000 og Count = 2000000000. Deres reelle sum er fire milliarder, men i 32-bit signeret aritmetik ombukkes resultatet til nøjagtig minus 294 967 296. Den værdi er komfortabelt mindre end Length(Data), så det forkerte tjek består, @Data[Index] tages langt uden for arrayet, og PDFium overdrages en vild pointer plus en to-gigabyte-længde. Hvad der følger, er en access violation på en god dag og stiltiende parsing af urelateret proceshukommelse på en dårlig

Den korrekte rækkefølge retter dette ved aldrig at lægge sammen. Negative værdier afvises, før noget indekseres, så @Data[Index] aldrig kan tages under arrayet. Så begrænses Index alene mod Length(Data), hvilket garanterer, at Length(Data) - Index er en ikke-negativ Integer. Først derefter sammenlignes Count mod den rest. Hver mellemliggende værdi forbliver inden for det repræsenterbare interval, så ingen build-konfiguration kan ændre resultatet. Lad dig ikke friste til at stole på {$Q+}-overløbstjek som sikkerhedsnet heller: release-builds skibes rutinemæssigt med det slået fra, og selv når det er slået til, har man konverteret en hukommelsessikkerhedsfejl til en EIntOverflow, der flygter fra midten af en valideringsrutine. PDFium Component behandler upålidelig længdearitmetik på samme måde, den behandler resten af grænsen, en disciplin dækket bredere i hærdning af PDFium VCL ABI og hukommelsessikkerhed i Delphi

Flowdiagram, der kontrasterer den overløbende Index plus Count-grænsekontrol med den kunsubtraktive valideringsrækkefølge, der holder PDFium byte-range-indlæsning hukommelsessikker i Delphi
At lægge til først wrappes forbi MaxInt og besejrer vagten; at afvise fortegn og indgærde led før én wrap-fri subtraktion holder hver mellemregning repræsenterbar

Hvorfor skal et nul-længde-vindue give nil?

Fordi @Data[Index] ikke er et lovligt udtryk for hvert Index, valideringen accepterer. Index = Length(Data) med Count = 0 er et helt velformet tomt vindue ved halen af bufferen, og en tom TBytes giver Index = 0 på et array, der slet ikke har noget element nul. At tage adressen i begge tilfælde indekserer forbi enden eller dereferencerer et nil dynamisk array. Så forgrener overloadet sig: Count = 0 giver en nil-pointer, enhver anden count giver @Data[Index]. Nil'en strømmer så ind i pointer-overloadet, hvis egen vagt accepterer en nil-pointer, når størrelsen er nul, og indlæsningen ender i den almindelige "Cannot load PDF document"-fejl frem for en access violation. En kalder, der beregnede et nul-byte-vindue fra en misdannet container, får en ren, opfangelig EPdfError som enhver anden dårlig input

Lånt eller kopieret: hvad Buffered afgør

Buffered vælger ejerskabskontrakten, og det er den eneste parameter her med konsekvenser ud over kaldet. Med Buffered = True kopierer PDFium Component det valgte vindue, og kun vinduet, ind i sin interne buffer, før den indlæser. De 40 MB-containeren kopieres ikke; de 312 KB PDF er. Så snart LoadDocument returnerer, kan man frigive, genbruge eller overskrive containeren straks, fordi komponenten ikke længere refererer til den. Dette er standarden og det rigtige valg for næsten al kode

Buffered = False sender @Data[Index] direkte til FPDF_LoadMemDocument64, og PDFium beholder den pointer for dokumentets levetid frem for at kopiere bytene. Det gør indlæsningen allokeringsfri, og det gør hele det bagvedliggende TBytes til en lånt ressource. Det skal forblive levende og uændret, indtil UnloadDocument kører, eller Active bliver False. Ikke vinduet, hele arrayet: et dynamisk array er reference-tællet som en enhed, og at lade den sidste reference gå et sted i ens kode frigiver den hukommelse, PDFium stadig læser. At sætte Length på det er lige så fatalt, fordi en reallokering kan flytte blokken. Skriv dette ind i din egen API-dokumentation, hvor du eksponerer en sådan indlæsning, i samme ånd som enhver anden lån-versus-eje-grænse i Pascal-kode; fejltilstanden er identisk med de aliasing-farer, der er beskrevet i FillChar og result-streng-lækket i Delphi, hvor en buffer ser ejet ud og ikke er det

type
  TFrameSession = class
  private
    FFrame: TBytes;   // ejer backing-lagringen så længe FPdf er indlæst
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame skal overleve det indlæste dokument
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // frigør lånet først
  FFrame := nil;         // kun nu må lagringen frigives
  inherited;
end;

Når byte-interval-vinduet er det forkerte værktøj

Vær ærlig om grænsen. Byte-interval-overloadet antager, at containeren allerede er fuldt i hukommelsen, og Count er en Integer, så ét vindue kan ikke overstige to gigabyte. Er containeren et 6 GB-arkiv på disk, eller ankommer over en socket, man ikke kan spole tilbage, kan dette overload ikke hjælpe, og at læse det hele ind i TBytes bare for at adressere et vindue indeni det besejrer pointen. Det er præcis der, FPDF_FILEACCESS-stien hører til, og on-demand-streaming-artiklen viser, hvordan man eksponerer en offset-forskudt visning af en fil som en brugerdefineret dokumentkilde. Tilsvarende, hvis de indlejrede bytes behøver transformation, før PDFium ser dem, dekomprimering, dekryptering, et unwrapping-trin, så er en reel kopi uundgåelig, og Buffered = True på det transformerede array er det ærlige svar. Byte-interval-vinduet betaler sig i præcis én form: sammenhængende, uændrede PDF-bytes, allerede residente, ved en kendt offset

Evaluerer man dette til en viewer, en preview-rude eller en batch-intake-pipeline, er byte-interval-overloadet og den strømmende loader to af de indlæsningsstrategier, PDFium Component tilbyder sammen med fil-, strøm- og rå pointer-indlæsninger. Den fulde API-flade, licensering og understøttede Delphi- og C++Builder-versioner er dokumenteret på PDFium Component-produktsiden

Tidslinjediagram over de to Buffered-tilstande ved indlæsning af en indlejret PDF fra et byteområde i Delphi: kun at kopiere PDF-vinduet og straks frigive containeren versus at låne hele det bagvedliggende TBytes, indtil UnloadDocument kører
Buffered = True kopierer kun vinduet, så containeren er engangs, mens Buffered = False efterlader hele backing-arrayet lånt til unload