Tehnički članak

PDFium učitavanje opsega bajtova za ugrađene PDF-ove u Delphi

PDFium Component može otvoriti PDF koji živi unutar većeg bafera direktno iz opsega bajtova. Preopterećenje LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean) adresira prozor na licu mesta, tako da nikakav preliminaran Copy nije potreban. Zauzvrat traži da razumete jedno pravilo: kad je Buffered False, osnovni niz je pozajmljen, ne kopiran

Ovo je drugačiji mehanizam od pristupa vođenog callback-om opisanog u streaming velikih PDF-ova na zahtev sa PDFium VCL, koji daje PDFium-u čitač FPDF_FILEACCESS i pušta ga da povlači blokove sa diska po potrebi. Onaj je za dokumente prevelike da stanu u RAM. Ovaj je za dokumente koji su već u RAM-u, koji sede na poznatom offsetu unutar nečeg drugog. Njih dvoje se dopunjuju, a poslednji odeljak objašnjava koja situacija pripada kojoj

Kopija od 40 MB koju niko nije tražio

Scenario se pojavljuje gde god PDF-ovi putuju unutar drugih formata. Skladište pošte drži tela poruka i priloge u jednom zapisu. Kontejner arhive spaja manifest, nekoliko slika i PDF. Prilagođen žični protokol uokviruje dokument iza zaglavlja sa prefiksom dužine. U svakom slučaju završite sa jednim velikim TBytes i saznanjem da PDF počinje na bajtu 1.182.336 i traje 312 kilobajta

Pre nego što je preopterećenje opsega bajtova postojalo, idiomatski odgovor je bio Copy(Data, Index, Count), koji alocira drugi niz i memcpy-uje prozor u njega. Zatim tu isečku predajete LoadDocument sa Buffered = True, koji je ponovo kopira u privatan bafer komponente. Dve kopije istih bajtova, jedna od njih čist ceremonijal, i na velikom skeniranju poštanskog sandučeta ponavlja se za svaku poruku. Preopterećenje opsega bajtova uklanja prvu kopiju bezuslovno, a drugu opciono

Šta preopterećenje opsega bajtova zapravo radi

Preopterećenje je namerno tanko: validira, izračunava jedan pokazivač, i delegira formi pokazivača LoadDocument kroz koju se cela porodica već sliva. Index je zasnovan na nuli, Count je dužina bajtova, a Buffered je podrazumevano True tačno kao na ostalim preopterećenjima. Jednoargumentni LoadDocument(const Data: TBytes; Buffered: Boolean) je sam sada samo poziv ovom sa Index = 0 i Count = Length(Data), tako da postoji jedna putanja validacije umesto dve

Poziv izgleda kao kod koji ste već pisali, minus isečak

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;

Zašto Index plus Count prekoračuje proveru granica?

Zato što su i Index i Count oba Integer, a zbir dve velike pozitivne Integer vrednosti nije nužno velik pozitivan Integer. Ovo je tehničko jezgro preopterećenja, i jedino mesto gde provera koja prirodno izgleda ispravno je rupa u bezbednosti memorije. Očigledna formulacija je pogrešna

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

Odradite slučaj neuspeha korak po korak. Uzmite Index = 2000000000 i Count = 2000000000. Njihov pravi zbir je četiri milijarde, ali u 32-bitnoj potpisanoj aritmetici rezultat se prelomi tačno na minus 294.967.296. Ta vrednost je udobno manja od Length(Data), tako da pogrešna provera prolazi, @Data[Index] se uzima daleko van niza, a PDFium-u se predaje divlji pokazivač plus dužina od dva gigabajta. Ono što sledi je access violation u dobrom danu, i tiho parsiranje nepovezane memorije procesa u lošem

Ispravan redosled to popravlja tako što nikad ne sabira. Negativne vrednosti se odbacuju pre nego što se bilo šta indeksira, tako da se @Data[Index] nikad ne može uzeti ispod niza. Zatim se Index ograničava sam za sebe naspram Length(Data), što garantuje da je Length(Data) - Index nenegativan Integer. Tek onda se Count poredi naspram tog ostatka. Svaka međuvrednost ostaje unutar reprezentabilnog opsega, tako da nijedna konfiguracija build-a ne može promeniti ishod. Ne dajte se u iskušenje da se oslonite na proveru prekoračenja {$Q+} kao mrežu bezbednosti: release build-ovi se rutinski isporučuju sa njom isključenom, a čak i kad je uključena, pretvorili ste bag bezbednosti memorije u EIntOverflow koji beži iz sredine rutine validacije. PDFium Component tretira nepouzdanu aritmetiku dužine na isti način na koji tretira ostatak granice, disciplina pokrivena šire u otvrdnjavanju PDFium VCL ABI i bezbednosti memorije u Delphi

Zašto prozor nulte dužine mora predati nil?

Zato što @Data[Index] nije legalan izraz za svaki Index koji validacija prihvata. Index = Length(Data) sa Count = 0 je savršeno dobro formiran prazan prozor na kraju bafera, a prazan TBytes daje Index = 0 na nizu koji uopšte nema element nula. Uzimanje adrese u bilo kom slučaju indeksira van kraja, ili dereferencira nil dinamički niz. Tako da se preopterećenje grana: Count = 0 daje nil pokazivač, bilo koji drugi count daje @Data[Index]. Nil tada teče u preopterećenje pokazivača, čija sopstvena straža prihvata nil pokazivač kad je veličina nula, i učitavanje se završava u običnoj grešci "Cannot load PDF document" umesto access violation-a. Pozivalac koji je izračunao prozor od nula bajtova iz deformisanog kontejnera dobija čist, hvatljiv EPdfError kao bilo koji drugi loš ulaz

Pozajmljeno ili kopirano: šta Buffered odlučuje

Buffered bira ugovor vlasništva, i to je jedini parametar ovde sa posledicama van poziva. Sa Buffered = True, PDFium Component kopira izabrani prozor, i samo prozor, u svoj interni bafer pre učitavanja. Kontejner od 40 MB se ne kopira; PDF od 312 KB se kopira. Čim se LoadDocument vrati, možete odmah osloboditi, ponovo koristiti ili prepisati kontejner, jer komponenta ga više ne referencira. Ovo je podrazumevano i ispravan izbor za skoro sav kod

Buffered = False prosleđuje @Data[Index] direktno FPDF_LoadMemDocument64, a PDFium drži taj pokazivač za život dokumenta umesto da kopira bajtove. To čini učitavanje bez alokacije, i čini čitav osnovni TBytes pozajmljenim resursom. Mora ostati živ i nepromenjen dok UnloadDocument ne pokrene ili Active ne postane False. Ne prozor, ceo niz: dinamički niz je referentno brojan kao jedinica, a puštanje poslednje reference bilo gde u vašem kodu oslobađa memoriju koju PDFium i dalje čita. Postavljanje Length na njemu je jednako fatalno, jer realokacija može pomeriti blok. Navedite ovo u sopstvenoj API dokumentaciji gde god izlažete takvo učitavanje, u istom duhu kao bilo koja druga granica pozajmi-naspram-poseduj u Pascal kodu; režim otkaza je identičan opasnostima aliasing-a opisanim u FillChar i curenju result string-a u Delphi, gde bafer izgleda posedovan, a nije

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;

Kad je prozor opsega bajtova pogrešan alat

Budite iskreni oko granice. Preopterećenje opsega bajtova pretpostavlja da je kontejner već u potpunosti u memoriji, a Count je Integer, tako da jedan prozor ne može prekoračiti dva gigabajta. Ako je kontejner arhiva od 6 GB na disku, ili stiže preko soketa koji ne možete premotati, ovo preopterećenje vam ne može pomoći, a čitanje celog toga u TBytes samo da biste adresirali prozor unutar njega poražava poentu. To je upravo gde pripada putanja FPDF_FILEACCESS, a članak o streaming-u na zahtev pokazuje kako izložiti pomeren pogled fajla kao prilagođen izvor dokumenta. Jednako, ako ugrađeni bajtovi trebaju transformaciju pre nego što ih PDFium vidi, dekompresiju, dešifrovanje, korak raspakivanja, onda je prava kopija neizbežna i Buffered = True na transformisanom nizu je iskren odgovor. Prozor opsega bajtova se isplati u tačno jednom obliku: susedni, nepromenjeni PDF bajtovi, već rezidentni, na poznatom offsetu

Ako ovo procenjujete za pregledač, panel pregleda ili pipeline unosa serijskih poslova, preopterećenje opsega bajtova i streaming loader su dve od strategija učitavanja koje PDFium Component isporučuje pored učitavanja iz fajla, toka i sirovog pokazivača. Kompletna površina API-ja, licenciranje i podrška Delphi i C++Builder verzija dokumentovani su na stranici proizvoda PDFium Component