Teknisk artikkel

PDFium byteintervall-innlasting for innebygde PDF-er i Delphi

PDFium Component kan åpne en PDF som bor inne i en større buffer direkte fra et byteintervall. Overloaden LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresserer et vindu på stedet, så ingen innledende Copy trengs. I bytte ber den deg forstå én regel: når Buffered er False, er det bakenforliggende arrayet lånt, ikke kopiert

Dette er en annen mekanisme enn den tilbakekallsdrevne tilnærmingen beskrevet i strømming av store PDF-er ved behov med PDFium VCL, som gir PDFium en FPDF_FILEACCESS-leser og lar den hente blokker fra disk etter behov. Den er for dokumenter for store til å holdes i RAM. Denne er for dokumenter allerede i RAM, sittende ved en kjent offset inne i noe annet. De to er utfyllende, og siste seksjon forklarer hvilken situasjon som hører til hvilken

40 MB-kopien ingen ba om

Scenarioet dukker opp overalt hvor PDF-er reiser inne i andre formater. En e-postlagring holder meldingskropper og vedlegg i én post. En arkivcontainer kjeder sammen et manifest, noen bilder og en PDF. En tilpasset ledningsprotokoll rammer inn et dokument bak en lengde-prefikset header. I hvert tilfelle ender du opp med å holde én stor TBytes og vite at PDF-en starter ved byte 1 182 336 og løper i 312 kilobyte

Før byteintervall-overloaden eksisterte, var det idiomatiske svaret Copy(Data, Index, Count), som allokerer et andre array og memcpy-er vinduet inn i det. Du gir deretter den skiven til LoadDocument med Buffered = True, som kopierer den igjen inn i komponentens private buffer. To kopier av samme bytes, den ene ren seremoni, og på et stort postboksskann gjentatt for hver melding. Byteintervall-overloaden fjerner den første kopien ubetinget og den andre valgfritt

Hva byteintervall-overloaden faktisk gjør

Overloaden er tynn med hensikt: den validerer, beregner én peker, og delegerer til pekerformen av LoadDocument som hele familien allerede kanaliseres gjennom. Index er nullbasert, Count er en bytelengde, og Buffered er som standard True nøyaktig som på de andre overloadene. Den ene-argument LoadDocument(const Data: TBytes; Buffered: Boolean) er nå selv bare et kall til denne med Index = 0 og Count = Length(Data), så det er én valideringssti i stedet for to

Å kalle den ser ut som koden du allerede skrev, minus skiven

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 flyter Index pluss Count over grensesjekken?

Fordi Index og Count begge er Integer, og summen av to store positive Integer-verdier er ikke nødvendigvis en stor positiv Integer. Dette er den tekniske kjernen i overloaden, og det er det ene stedet hvor en naturlig utseende sjekk er et minnesikkerhetshull. Den opplagte formuleringen er feil

// 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 mislykkede tilfellet gjennom. Ta Index = 2000000000 og Count = 2000000000. Den sanne summen er fire milliarder, men i 32-bit signert aritmetikk pakker resultatet rundt til nøyaktig minus 294 967 296. Den verdien er komfortabelt mindre enn Length(Data), så den feil sjekken passerer, @Data[Index] tas langt utenfor arrayet, og PDFium gis en villfarende peker pluss en to-gigabyte lengde. Det som følger er et tilgangsbrudd på en god dag og stille parsing av urelatert prosessminne på en dårlig en

Den korrekte rekkefølgen fikser dette ved aldri å legge til. Negative verdier avvises før noe indekseres, så @Data[Index] kan aldri tas under arrayet. Deretter er Index alene avgrenset mot Length(Data), som garanterer at Length(Data) - Index er en ikke-negativ Integer. Først da sammenlignes Count mot den resten. Hver mellomverdi holder seg innenfor det representerbare intervallet, så ingen byggekonfigurasjon kan endre utfallet. La deg ikke friste til å stole på {$Q+}-overflytsjekking som sikkerhetsnett heller: utgivelsesbygg leveres rutinemessig med det av, og selv når det er på, har du gjort om en minnesikkerhetsfeil til en EIntOverflow som slipper unna midt i en valideringsrutine. PDFium Component behandler upålitelig lengdearitmetikk på samme måte som den behandler resten av grensen, en disiplin dekket mer bredt i herding av PDFium VCL ABI og minnesikkerhet i Delphi

Hvorfor må et vindu med null lengde sende nil?

Fordi @Data[Index] ikke er et lovlig uttrykk for hver Index valideringen aksepterer. Index = Length(Data) med Count = 0 er et helt velformet tomt vindu på halen av bufferen, og en tom TBytes gir Index = 0 på et array som ikke har noe element null i det hele tatt. Å ta adressen i begge tilfeller indekserer forbi slutten, eller dereferer et nil dynamisk array. Så overloaden forgrener seg: Count = 0 gir en nil-peker, ethvert annet antall gir @Data[Index]. Nil-en flyter deretter inn i pekeroverloaden, hvis egen vakt aksepterer en nil-peker når størrelsen er null, og innlastingen ender i den vanlige «Cannot load PDF document»-feilen i stedet for et tilgangsbrudd. En som kaller og har beregnet et null-byte-vindu fra en feilformet container får en ren, fangbar EPdfError som enhver annen dårlig inndata

Lånt eller kopiert: hva Buffered avgjør

Buffered velger eierskapskontrakten, og det er den eneste parameteren her med konsekvenser utover kallet. Med Buffered = True kopierer PDFium Component det valgte vinduet, og bare vinduet, inn i sin interne buffer før innlasting. 40 MB-containeren kopieres ikke; 312 KB-PDF-en er det. Når LoadDocument returnerer kan du frigi, gjenbruke eller overskrive containeren umiddelbart, fordi komponenten ikke lenger refererer til den. Dette er standarden og det riktige valget for nesten all kode

Buffered = False sender @Data[Index] rett til FPDF_LoadMemDocument64, og PDFium beholder den pekeren for dokumentets levetid i stedet for å kopiere bytene. Det gjør innlastingen allokeringsfri, og det gjør hele det bakenforliggende TBytes-arrayet til en lånt ressurs. Det må holdes i live og uendret til UnloadDocument kjøres eller Active blir False. Ikke vinduet, hele arrayet: et dynamisk array er referansetalt som en enhet, og å la den siste referansen forsvinne noe sted i koden din frigir minnet PDFium fortsatt leser. Å sette Length på det er like fatalt, fordi en reallokering kan flytte blokken. Angi dette i din egen API-dokumentasjon overalt hvor du eksponerer en slik innlasting, i samme ånd som enhver annen lån-versus-eie-grense i Pascal-kode; feilmodusen er identisk med aliasing-farene beskrevet i FillChar- og resultatstrenglekkasjen i Delphi, hvor en buffer ser eid ut 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 byteintervall-vinduet er feil verktøy

Vær ærlig om grensen. Byteintervall-overloaden antar at containeren allerede er fullt i minnet, og Count er en Integer, så et enkelt vindu kan ikke overstige to gigabyte. Hvis containeren er et 6 GB-arkiv på disk, eller ankommer over en socket du ikke kan spole tilbake, kan denne overloaden ikke hjelpe deg, og å lese hele greia inn i TBytes bare for å adressere et vindu inni den ødelegger hele poenget. Det er nøyaktig hvor FPDF_FILEACCESS-stien hører hjemme, og artikkelen om strømming ved behov viser hvordan du eksponerer en offset-forskjøvet visning av en fil som en tilpasset dokumentkilde. Likeledes, hvis de innebygde bytene trenger transformasjon før PDFium ser dem, dekomprimering, dekryptering, et opppakkingstrinn, da er en reell kopi uunngåelig, og Buffered = True på det transformerte arrayet er det ærlige svaret. Byteintervall-vinduet lønner seg i nøyaktig én form: sammenhengende, uendrede PDF-bytes, allerede boende, ved en kjent offset

Hvis du vurderer dette for en viser, et forhåndsvisningspanel eller en batch-mottakspipeline, er byteintervall-overloaden og den strømmende lasteren to av lastestrategiene PDFium Component leverer sammen med fil-, strøm- og rå peker-innlastinger. Den fullstendige API-flaten, lisensiering og støtte for Delphi- og C++Builder-versjoner er dokumentert på PDFium Component-produktsiden