Technical Article

Strujanje ogromnih PDF-ova na zahtjev s PDFium-om u Delphi-ju

Skenirani arhiv mo?e iznositi nekoliko gigabajta u jednom PDF-u. Preglednik koji otvara takvu datoteku obi?no ?eli prikazati jednu stranicu, mo?da tablicu sadr?aja ili stranicu na koju je korisnik sko?io iz kn?ne oznake. U?itavanje cijele datoteke u memoriju radi prikazivanja dviju stranica je rasipno na svim osima: tro?i adresni prostor, usporava korisnika iza dugog po?etnog ?itanja, a na 32-bitnom Delphi procesu mo?e potpuno zakazati prije nego ?to se pojavi ijedna stranica. PDFium je napravljen s tim na umu. Mo?e u?itati dokument putem povratnog poziva koji tra?i specifi?ne raspone bajtova koji su mu potrebni, kada su mu potrebni, i nikada ne zahtijeva cijelu datoteku odjednom

Komponenta izla?e tu putanju kroz adapter toka. Predajete joj bilo koji TStream, a PDFium povla?i blokove iz tog toka na zahtjev. Datoteka se mo?e nalaziti na disku, u blob polju baze podataka ili iza bilo kojeg drugog potomka klase TStream, a ni?ta od toga se ne kopira unaprijed u memoriju

Kako PDFium tra?i bajtove

C API PDFium-a u?itava dokument iz objekta koji isporu?uje pozivatelj, a koji je opisan strukturom FPDF_FILEACCESS. Ukupna struktura ima tri dijela koja su ovdje va?na: polje duljine, povratni poziv za ?itanje i neprozirni korisni?ki parametar. Ulazna to?ka koja ga tro?i je FPDF_LoadCustomDocument. Nakon ?to PDFium zadr?i tu strukturu, on analizira najavu, locira tablicu unakrsnih referenci i od tada ?ita samo ono ?to odre?ena operacija zahtijeva. Otvaranje dokumenta doti?e kraj datoteke i nekoliko objekata kataloga. Prikazivanje stranice 400 ?ita tokove sadr?aja i resurse za tu stranicu i ni?ta vi?e

To je razlika izme?u me?uspremljenog u?itavanja i strujnog u?itavanja. Me?uspremljeno u?itavanje ?ita datoteku od po?etka do kraja prije nego ?to PDFium vidi bajt nula. Strujno u?itavanje preokre?e odnos: PDFium upravlja ?itanjem, a bajtovi koji se nikada ne dotaknu nikada se i ne pro?itaju. Za vi?egigabajtnu datoteku koja se pregledava stranicu po stranicu, to je razlika izme?u neupotrebljivog u?itavanja i trenutnog

Adapter toka

Adapter koji premo??uje Delphi TStream na FPDF_FILEACCESS je TPdfStreamAdapter. Njegov konstruktor uzima tok i zastavicu vlasni?tva, bilje?i duljinu toka jednom, popunjava zapis FPDF_FILEACCESS i povezuje povratni poziv za ?itanje. Kada PDFium kasnije pozove natrag s pomakom i veli?inom, adapter pozicionira tok na taj pomak i kopira to?no taj raspon u spremnik koji je PDFium osigurao

// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
  inherited Create;
  if AStream = nil then
    raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
  FStream := AStream;
  FOwnsStream := AOwnsStream;

  // FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
  // that would silently truncate past 4 GiB.
  if AStream.Size > High(FPDF_DWORD) then
    raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');

  FillChar(FFileAccess, SizeOf(FFileAccess), 0);
  FFileAccess.m_FileLen  := FPDF_DWORD(AStream.Size);
  FFileAccess.m_GetBlock := GetBlockCallback;
  FFileAccess.m_Param    := Self;
end;

Zastavica vlasni?tva odlu?uje tko osloba?a tok. Proslijedite False i pozivatelj zadr?ava tok i mora ga odr?avati aktivnim tijekom cijelog ?ivota dokumenta. Proslijedite True i adapter preuzima kontrolu, osloba?aju?i tok kada se dokument zatvori. U svakom slu?aju, tok mora nad?ivjeti svako ?itanje koje ?e PDFium izvr?iti, jer PDFium dr?i pokaziva? FPDF_FILEACCESS i pozvat ?e natrag u bilo kojem trenutku dok je dokument otvoren, a ne samo tijekom po?etnog u?itavanja

Za?to je povratni poziv stati?ka funkcija

Povratni poziv za ?itanje koji PDFium pohranjuje u m_GetBlock je obi?an pokaziva? C funkcije s konvencijom pozivanja cdecl. Delphi metoda ne mo?e se koristiti izravno, jer metoda nosi skriveni argument Self o kojem C pozivatelj ne zna ni?ta i nikada ga ne?e dostaviti. Stoga adapter deklarira povratni poziv kao klasnu funkciju (class function) ozna?enu s cdecl; static, ?to se kompajlira u samostalnu funkciju s rasporedom okvira C koji PDFium o?ekuje i bez implicitnog Self

To rje?ava konvenciju pozivanja, ali postavlja drugo pitanje: bez Self, kako povratni poziv dolazi do specifi?nog toka iz kojeg bi trebao ?itati? Odgovor je neprozirni korisni?ki parametar. Kada adapter gradi zapis, on pohranjuje pokaziva? vlastite instance u m_Param. PDFium vra?a taj isti pokaziva? kao prvi argument svakog povratnog poziva. Stati?ka funkcija ga vra?a natrag u TPdfStreamAdapter i ?alje ?itanje prema toku te instance. Ovo je standardni trampolin za predaju konteksta objekta preko C granice koja nema pojma o objektima

// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
  param   : Pointer;
  position: FPDF_DWORD;
  pBuf    : PByte;
  size    : FPDF_DWORD): Integer; cdecl;
var
  Adapter: TPdfStreamAdapter;
begin
  Result := 0;
  if (param = nil) or (pBuf = nil) or (size = 0) then
    Exit;
  Adapter := TPdfStreamAdapter(param);   // recover the instance from m_Param
  if Adapter.FStream = nil then
    Exit;
  try
    Adapter.FStream.Position := Int64(position);
    Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
    Result := 1;
  except
    Result := 0;  // report failure by return value, never by raising
  end;
end;

Gornja granica od 4 GiB i za?to joj treba za?tita

Polje duljine m_FileLen u FPDF_FILEACCESS je 32-bitna neozna?ena vrijednost. Njezina najve?a prikaziva duljina je za jedan bajt kra?a od 4 GiB. TStream izvje?tava o svojoj veli?ini kao Int64, tako da tok mo?e opisati mnogo vi?e bajtova nego ?to polje mo?e primiti. U trenutku kada veli?ina toka prije?e tu granicu, nema po?tenog na?ina da se PDFium-u ka?e koliko je datoteka duga?ka

Pogre?na reakcija je dodijeliti veli?inu i pustiti je da se omota. Skra?ivanje duljine od 5 GiB na 32-bitno polje daje mali broj koji izgleda uvjerljivo, a PDFium ?e tada analizirati datoteku vjeruju?i da ona zavr?ava otprilike na jednom gigabajtu. Najava i tablica unakrsnih referenci nalaze se na stvarnom kraju datoteke, daleko iza skra?ene duljine, pa analiza ne uspijeva na na?in koji nema nikakve veze sa stvarnim uzrokom. Debugirali biste pogre?ku unakrsne reference na datoteci koja je savr?eno valjana, bez naznake da se cijeli broj omotao dva sloja iznad

Umjesto toga, adapter odbija ulaz. Konstruktor uspore?uje veli?inu toka s High(FPDF_DWORD) i podi?e EPdfError onog trenutka kada je tok prevelik za opisivanje. Eksplicitna, trenutna pogre?ka imenuje stvarni problem na to?ki konstrukcije. Tiho skra?ivanje skriva ga iza zavaravaju?eg simptoma koji biste tra?ili mnogo kasnije. Ograni?enje od 4 GiB je stvarno ograni?enje ove putanje u?itavanja, a po?teno je iznijeti ga glasno, a ne zata?kavati aritmetikom koja se slu?ajno kompajlira

Neuspjesi ne smiju prije?i granicu

?itanje mo?e zakazati. Tok mo?e biti objekt podr?an mre?om koji ima vremensko ograni?enje, blob ru?ka koja je zatvorena ispod vas, ili datoteka koja je skra?ena nakon ?to je dokument otvoren. PDFium-ov ugovor za povratni poziv za ?itanje je povratna vrijednost: razli?ita od nule za uspjeh, nula za neuspjeh. To je C okvir i nema mehanizam za hvatanje ili ?irenje Pascal iznimke

Zbog toga trampolin omotava pozicioniranje i ?itanje u try/except koji guta iznimku i vra?a nulu. Ako bi se Delphi iznimci dopustilo da se pro?iri izvan povratnog poziva, ona bi se odmotavala kroz PDFium-ove cdecl okvire stoga, koji nikada nisu izgra?eni da budu odmotani Pascal mehanizmom iznimaka. Rezultat je nedefinirano pona?anje u najboljem slu?aju i te?ko ru?enje u najgorem, duboko unutar parsera PDF-a bez upotrebljivog stoga. Vra?anje nule dr?i neuspjeh unutar ugovora. PDFium vidi neuspjelo ?itanje bloka, ?isto prekida operaciju, a FPDF_LoadCustomDocument javlja da se dokument nije mogao u?itati, ?to komponenta prikazuje kao EPdfError na Pascal strani gdje i pripada

Otvaranje dokumenta na ovaj na?in

Metoda komponente koja pokre?e strujnu putanju je LoadCustomDocument, deklarirana kao zasebna metoda umjesto drugog preoptere?enja LoadDocument, tako da proslje?ivanje TMemoryStream nikada slu?ajno ne sleti na me?uspremljenu putanju. Ona gradi adapter, poziva FPDF_LoadCustomDocument i odr?ava adapter aktivnim tijekom ?ivotnog vijeka u?itanog dokumenta

var
  Pdf: TPdf;
  FileStream: TFileStream;
begin
  Pdf := TPdf.Create(nil);
  FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
  try
    // Hand stream ownership to Pdf: it frees FileStream when the document closes.
    Pdf.LoadCustomDocument(FileStream, True);
    // PDFium has read only the trailer and catalog so far.
    // Rendering a page pulls just that page's bytes through the callback.
    // ... render or inspect pages here ...
  finally
    Pdf.Free;  // closes the document, which frees the adapter and the stream
  end;
end;

Isti poziv radi za TMemoryStream, blob tok iz skupa podataka baze podataka ili prilago?enog potomka klase TStream. U?itavanje na zahtjev opravdava svoje postojanje kada je datoteka velika, a ?ita se samo njezin dio: preglednik arhiva, generator sli?ica koji uzima uzorke nekoliko stranica ili indeks pretra?ivanja koji povla?i jednu po jednu stranicu. Kada je datoteka mala ili ?ete ionako pro?itati cijelu, me?uspremljeno u?itavanje je jednostavnije, a strujni mehanizam vam ne donosi ni?ta. Odlu?uju?i faktor je omjer bajtova koje ?ete stvarno dotaknuti u odnosu na bajtove koje datoteka sadr?i

Jednom kada stranice po?nu strujati na zahtjev, sljede?a briga je odr?avanje responzivnosti prikazanih stranica dok korisnik zumira i skrola, ?to je pokriveno u na?oj bilje?ci o predmemoriranju prikaza i performansama zumiranja. Kada je strujani dokument onaj koji bi preglednik trebao prikazati, ali ne i dopustiti korisniku da ga izveze ili izmijeni, tehnike u vodi?u za siguran pregled PDF-a prirodno se povezuju s ovom putanjom u?itavanja. Obje se temelje na strujnom u?itavanju koje je ovdje opisano, a koje se isporu?uje kao dio softvera PDFium Component za Delphi i C++Builder, zajedno s API-jima za prikazivanje, ekstrakciju teksta i bilje?ke koji su pokriveni drugdje na ovom blogu