Teknisk artikel

PDFium byteintervall-laddning för inbäddade PDF-filer i Delphi

PDFium Component kan öppna en PDF som lever inuti en större buffert direkt från ett byteintervall. Överlagringen LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresserar ett fönster på plats, så ingen preliminär Copy behövs. I gengäld ber den dig förstå en regel: när Buffered är False är den bakomliggande arrayen lånad, inte kopierad

Det här är en annan mekanism än den callback-drivna metoden beskriven i strömning av stora PDF-filer på begäran med PDFium VCL, som ger PDFium en FPDF_FILEACCESS-läsare och låter den dra block från disk allt eftersom den behöver dem. Den är för dokument för stora för att hålla i RAM. Den här är för dokument redan i RAM, som sitter vid en känd offset inuti något annat. De två är komplement, och sista avsnittet förklarar vilken situation som hör till vilken

40 MB-kopian ingen bad om

Scenariot dyker upp varhelst PDF-filer reser inuti andra format. En e-postlagring håller meddelandekroppar och bilagor i en post. En arkivcontainer sammanfogar ett manifest, några bilder och en PDF. Ett anpassat trådprotokoll ramar in ett dokument bakom ett längdprefixerat huvud. I varje fall hamnar du med att hålla en stor TBytes och veta att PDF:en börjar vid byte 1 182 336 och löper i 312 kilobyte

Innan byteintervallöverlagringen fanns var det idiomatiska svaret Copy(Data, Index, Count), som allokerar en andra array och memcpyar fönstret in i den. Du ger sedan den skivan till LoadDocument med Buffered = True, som kopierar den igen in i komponentens privata buffert. Två kopior av samma byte, en av dem ren ceremoni, och på en stor brevlådeskanning upprepad för varje meddelande. Byteintervallöverlagringen tar bort den första kopian ovillkorligen och den andra valfritt

Jämförelsediagram som visar den gamla dubbelkopieringsvägen för en inbäddad PDF inuti en container i Delphi, kontra PDFium Component byte-range LoadDocument-överladdning som adresserar fönstret på plats
Utan överlagringen kopierades samma 312 KB-fönster två gånger via memcpy; byte-range-anropet adresserar det på plats och Buffered avgör om något kopieras alls

Vad byteintervallöverlagringen faktiskt gör

Överlagringen är tunn med avsikt: den validerar, beräknar en pekare, och delegerar till pekarformen av LoadDocument som hela familjen redan kanaliseras genom. Index är nollbaserat, Count är en bytelängd, och Buffered är som standard True exakt som på de andra överlagringarna. Enargument-LoadDocument(const Data: TBytes; Buffered: Boolean) är själv nu bara ett anrop till den här med Index = 0 och Count = Length(Data), så det finns en valideringsväg snarare än två

Att anropa den ser ut som koden du redan skrev, minus skivan

var
  Frame: TBytes;          // hela container-posten, tiotals megabyte
  Offset, Size: Integer;
begin
  Frame := LoadContainerRecord('mailbox.dat');
  LocateEmbeddedPdf(Frame, Offset, Size);   // din container-tolk

  // Ingen Copy(Frame, Offset, Size) här - fönstret adresseras på plats
  Pdf.LoadDocument(Frame, Offset, Size, True);
  try
    RenderPreview(Pdf);
  finally
    Pdf.UnloadDocument;
  end;
end;

Varför flödar Index plus Count över gränskontrollen?

Eftersom Index och Count båda är Integer, och summan av två stora positiva Integer-värden inte nödvändigtvis är en stor positiv Integer. Det här är den tekniska kärnan i överlagringen, och det är det enda stället där en naturligt utseende kontroll är ett minnessäkerhetshål. Den uppenbara formuleringen är fel

// FEL: Index + Count beräknas som Integer och kan slå om till negativt
if Index + Count <= Length(Data) then
  DataPtr := @Data[Index];

// RÄTT: avvisa tecken först, begränsa sedan varje term separat,
// med endast aritmetik som en subtraktion som inte kan slå om
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');

Arbeta igenom det felande fallet. Ta Index = 2000000000 och Count = 2000000000. Deras verkliga summa är fyra miljarder, men i 32-bitars signerad aritmetik vrider sig resultatet till exakt minus 294 967 296. Det värdet är bekvämt mindre än Length(Data), så den felaktiga kontrollen godkänns, @Data[Index] tas långt utanför arrayen, och PDFium ges en vild pekare plus en tvågigabyte-längd. Vad som följer är en åtkomstöverträdelse på en bra dag och tyst tolkning av orelaterat processminne på en dålig

Den korrekta ordningen fixar detta genom att aldrig addera. Negativa värden avvisas innan något indexeras, så @Data[Index] kan aldrig tas under arrayen. Sedan begränsas Index på egen hand mot Length(Data), vilket garanterar att Length(Data) - Index är en icke-negativ Integer. Först då jämförs Count mot den återstoden. Varje mellanliggande värde förblir inom det representerbara intervallet, så ingen byggkonfiguration kan ändra utfallet. Bli inte frestad att förlita dig på {$Q+}-överflödeskontroll som säkerhetsnätet heller: release-byggen levereras rutinmässigt med den avstängd, och även när den är på har du omvandlat en minnessäkerhetsbugg till ett EIntOverflow som flyr från mitten av en valideringsrutin. PDFium Component behandlar opålitlig längdaritmetik på samma sätt som den behandlar resten av gränsen, en disciplin täckt bredare i härdning av PDFium VCL ABI och minnessäkerhet i Delphi

Flödesdiagram som kontrasterar den överflödande Index plus Count-gränskontrollen med den subtraktion-endast-valideringsordning som håller PDFium byte-range-inläsning minnessäker i Delphi
Att addera först sveper förbi MaxInt och besegrar vakten; att avvisa tecken och begränsa termer före en enda subtraktion utan svep håller varje mellanliggande värde representerbart

Varför måste ett nolllängdsfönster skicka nil?

Eftersom @Data[Index] inte är ett lagligt uttryck för varje Index valideringen accepterar. Index = Length(Data) med Count = 0 är ett perfekt välformat tomt fönster vid svansen av bufferten, och en tom TBytes ger Index = 0 på en array som inte har något element noll alls. Att ta adressen i endera fallet indexerar förbi slutet, eller dereferar en nil dynamisk array. Så överlagringen förgrenar sig: Count = 0 ger en nil-pekare, varje annat antal ger @Data[Index]. Nil:et flödar sedan in i pekaröverlagringen, vars egen vakt accepterar en nil-pekare när storleken är noll, och laddningen slutar i det vanliga "Cannot load PDF document"-felet snarare än en åtkomstöverträdelse. En anropare som beräknat ett nollbytes-fönster från en missformad container får ett rent, fångbart EPdfError som vilken annan dålig indata som helst

Lånad eller kopierad: vad Buffered avgör

Buffered väljer ägarskapskontraktet, och det är den enda parametern här med konsekvenser bortom anropet. Med Buffered = True kopierar PDFium Component det valda fönstret, och bara fönstret, in i sin interna buffert innan laddning. 40 MB-containern kopieras inte; 312 KB-PDF:en gör det. När LoadDocument returnerar får du frigöra, återanvända eller skriva över containern omedelbart, eftersom komponenten inte längre refererar den. Det här är standarden och rätt val för nästan all kod

Buffered = False skickar @Data[Index] rakt till FPDF_LoadMemDocument64, och PDFium behåller den pekaren under dokumentets liv snarare än att kopiera byten. Det gör laddningen allokeringsfri, och det gör hela den bakomliggande TBytes till en lånad resurs. Den måste förbli levande och omodifierad tills UnloadDocument körs eller Active blir False. Inte fönstret, hela arrayen: en dynamisk array är referensräknad som en enhet, och att låta den sista referensen försvinna någonstans i din kod frigör minnet PDFium fortfarande läser. Att sätta Length på den är lika ödesdigert, eftersom en omallokering kan flytta blocket. Ange detta i din egen API-dokumentation varhelst du exponerar en sådan laddning, i samma anda som varje annan låna-kontra-äga-gräns i Pascal-kod; felläget är identiskt med aliasing-farorna beskrivna i FillChar och resultatsträngläckan i Delphi, där en buffert ser ägd ut och inte är det

type
  TFrameSession = class
  private
    FFrame: TBytes;   // äger bakgrundslagringen så länge FPdf är laddad
    FPdf: TPdf;
  public
    procedure OpenEmbedded(Offset, Size: Integer);
    destructor Destroy; override;
  end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  // Buffered = False: FFrame måste överleva det laddade dokumentet
  FPdf.LoadDocument(FFrame, Offset, Size, False);
end;

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

När byteintervallfönstret är fel verktyg

Var ärlig om gränsen. Byteintervallöverlagringen antar att containern redan är fullt i minnet, och Count är en Integer, så ett enda fönster kan inte överstiga två gigabyte. Om containern är ett 6 GB-arkiv på disk, eller anländer över ett uttag du inte kan spola tillbaka, kan den här överlagringen inte hjälpa dig och att läsa in hela grejen i TBytes bara för att adressera ett fönster inuti den motverkar hela poängen. Det är precis där FPDF_FILEACCESS-vägen hör hemma, och artikeln om strömning på begäran visar hur man exponerar en offsetförskjuten vy av en fil som en anpassad dokumentkälla. På samma sätt, om de inbäddade byten behöver transformation innan PDFium ser dem, dekomprimering, dekryptering, ett uppackningssteg, då är en riktig kopia oundviklig och Buffered = True på den transformerade arrayen är det ärliga svaret. Byteintervallfönstret lönar sig i exakt en form: sammanhängande, omodifierade PDF-byte, redan residenta, vid en känd offset

Om du utvärderar det här för en visare, en förhandsgranskningspanel eller en batch-intagningspipeline är byteintervallöverlagringen och den strömmande laddaren två av laddningsstrategierna PDFium Component levereras med tillsammans med fil-, ström- och rå-pekare-laddningar. Den fullständiga API-ytan, licensiering och Delphi- och C++Builder-versionsstöd är dokumenterade på PDFium Component-produktsidan

Tidslinjediagram över de två Buffered-lägena när en inbäddad PDF läses in från ett byteintervall i Delphi: kopiera bara PDF-fönstret och frigör containern omedelbart, kontra låna hela den bakomliggande TBytes till UnloadDocument körs
Buffered = True kopierar bara fönstret så att behållaren är engångs, medan Buffered = False lånar hela bakomliggande arrayen fram till urladdningen