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

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;          // whole container record, tens of megabytes
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // your container parser

  // No Copy(Frame, Offset, Size) here - the window is addressed in place
  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

// WRONG: Index + Count is evaluated in Integer and can wrap negative
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// RIGHT: reject signs first, then bound each term separately,
// with the only arithmetic done as a subtraction that cannot wrap
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

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;   // owns the backing storage for as long as FPdf is loaded
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame must outlive the loaded document
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

destructor TFrameSession.Destroy;
begin
  FPdf.UnloadDocument;   // release the borrow first
  FFrame := nil;         // only now may the storage go
  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