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

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;          // 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;

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

// 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');

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

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;   // 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 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