Skannattu arkisto voi viedä useita gigatavuja yhdessä PDF-tiedostossa. Katseluohjelma, joka avaa tällaisen tiedoston, haluaa yleensä näyttää yhden sivun - ehkä sisällysluettelon, ehkä sivun, johon käyttäjä hyppäsi kirjanmerkistä. Koko tiedoston lukeminen muistiin kahden sivun renderöimiseksi on tuhlausta jokaisella akselilla: se kuluttaa osoiteavaruutta, jumittaa käyttäjän pitkän alkulukemisen taakse ja 32-bittisessä Delphi-prosessissa se voi epäonnistua kokonaan ennen kuin yhtäkään sivua ilmestyy. PDFium rakennettiin tämä mielessä. Se voi ladata dokumentin takaisinkutsun kautta, joka pyytää sen tarvitsemat tietyt tavualueet silloin, kun se niitä tarvitsee, eikä se koskaan vaadi koko tiedostoa kerralla.
Komponentti tuo tämän polun näkyviin virta-sovittimen (stream adapter) kautta. Annat sille minkä tahansa TStream-virran, ja PDFium vetää lohkoja kyseisestä virrasta tarpeen mukaan. Tiedosto voi sijaita levyllä, tietokannan blob-kentässä tai minkä tahansa muun TStream-jälkeläisen takana, eikä mitään siitä kopioida muistiin etukäteen.
Miten PDFium pyytää tavuja
PDFiumin C-rajapinta lataa dokumentin kutsujan toimittamasta oliosta, jota kuvailee FPDF_FILEACCESS-rakenne. Rakenteessa on kolme tässä merkityksellistä osaa: pituuskenttä, lukutakaisinkutsu ja läpinäkymätön käyttäjäparametri. Aloituspiste, joka sen kuluttaa, on FPDF_LoadCustomDocument. Kun PDFium hallitsee kyseistä rakennetta, se jäsentää trailerin, paikallistaa ristiviitetaulukon ja lukee siitä lähtien vain sen, mitä tietty operaatio vaatii. Dokumentin avaaminen koskee tiedoston loppuosaan ja kouralliseen katalogiolioita. Sivun 400 renderöinti lukee kyseisen sivun sisältövirrat ja resurssit eikä mitään muuta.
Tämä on ero puskuroidun latauksen ja suoratoistolatauksen välillä. Puskuroitu lataus lukee tiedoston päästä päähän ennen kuin PDFium näkee tavua nolla. Suoratoistolataus kääntää suhteen toisinpäin: PDFium ohjaa lukuja, ja tavuja, joihin ei koskaan kosketa, ei koskaan lueta. Monen gigatavun tiedostolle sivu kerrallaan katseltuna tämä on ero käyttökelvottoman latauksen ja välittömän latauksen välillä.
Virtasovitin
TStream-virran FPDF_FILEACCESS-rakenteeseen siltaava sovitin on TPdfStreamAdapter. Sen muodostin ottaa virran ja omistajuuslipun, kaappaa virran koon kerran, täyttää FPDF_FILEACCESS-tietueen ja kytkee lukutakaisinkutsun. Kun PDFium myöhemmin kutsuu takaisin siirtymällä ja koolla, sovitin hakee virran kyseiseen siirtymään ja kopioi tarkalleen kyseisen alueen PDFiumin toimittamaan puskuriin.
// 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;
Omistajuuslippu päättää, kuka vapauttaa virran. Välitä False, ja kutsuja pitää virran ja sen on pidettävä se elossa dokumentin koko eliniän ajan. Välitä True, ja sovitin ottaa vastuun vapauttaen virran, kun dokumentti sulkeutuu. Joka tapauksessa virran on elättävä pidempään kuin jokainen lukutoiminto, jonka PDFium suorittaa, koska PDFium pitää hallussaan FPDF_FILEACCESS-osoitinta ja kutsuuu takaisin missä tahansa vaiheessa dokumentin ollessa auki, ei vain alkulatauksen aikana.
Miksi takaisinkutsu on staattinen funktio
Lukutakaisinkutsu, jonka PDFium tallentaa kenttään m_GetBlock, on tavallinen C-funktio-osoitin cdecl-kutsukäytännöllä. Delphi-metodia ei voida käyttää suoraan, koska metodi kantaa piilotettua Self-argumenttia, josta C-kutsuja ei tiedä mitään eikä koskaan toimita. Sovitin esittelee takaisinkutsun siksi luokkafunktiona (class function) merkittynä cdecl; static, mikä kääntyy erilliseksi funktioksi C-kehysrakenteella, jota PDFium odottaa ilman implisiittistä Self-argumenttia.
Tämä ratkaisee kutsukäytännön, mutta nostaa toisen kysymyksen: ilman Self-argumenttia, miten takaisinkutsu tavoittaa sen tietyn virran, josta sen pitäisi lukea? Vastaus on läpinäkymätön käyttäjäparametri. Kun sovitin rakentaa tietueen, se tallentaa oman instanssiosoittimensa kenttään m_Param. PDFium antaa saman osoittimen takaisin kunkin takaisinkutsun ensimmäisenä argumenttina. Staattinen funktio tyyppimuuntaa sen takaisin TPdfStreamAdapter-olioksi ja välittää lukutoiminnon kyseisen instanssin virralle. Tämä on standardi trampoliini olioyhteyden välittämiseksi C-rajan yli, jolla ei ole käsitystä olioista.
// 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;
4 GiB:n katto ja miksi se vaatii suojauksen
Pituuskenttä m_FileLen FPDF_FILEACCESS-rakenteessa on 32-bittinen etumerkitön arvo. Sen suurin esitettävä pituus on yhtä tavua vailla 4 GiB. TStream ilmoittaa kokonsa Int64-tyyppinä, joten virta voi kuvata huomattavasti enemmän tavuja kuin kenttään mahtuu. Heti kun virran koko ylittää tämän katon, ei ole mitään rehellistä tapaa kertoa PDFiumille, kuinka pitkä tiedosto on.
Väärä vastaus on määrittää koko ja antaa sen kiertyä (wrap). 5 GiB:n pituuden katkaiseminen 32-bittiseen kenttään tuottaa pienen, uskottavalta näyttävän luvun, ja PDFium jäsentää tiedoston uskoen sen päättyvän noin gigatavun kohdalla. Traileri ja ristiviitetaulukko sijaitsevat tiedoston todellisessa päässä, kaukana katkaistun pituuden ohi, joten jäsennys epäonnistuu tavalla, jolla ei ole mitään tekemistä todellisen syyn kanssa. Päätyisit selvittämään ristiviitevirhettä tiedostossa, joka on täysin kelvollinen, ilman vihjettä siitä, että kokonaisluku kiertyi kaksi kerrosta ylempänä.
Sovitin kieltäytyy sen sijaan ottamasta syötettä vastaan. Muodostin vertaa virran kokoa arvoon High(FPDF_DWORD) ja nostaa EPdfError-poikkeuksen heti, kun virta on liian suuri kuvattavaksi. Nimenomainen, välitön virhe nimeää todellisen ongelman rakennushetkellä. Hiljainen katkaisu piilottaa sen harhaanjohtavan oireen taakse, jota selvittäisit paljon myöhemmin. 4 GiB:n raja on tämän latauspolun aito rajoitus, ja rehellistä on tuoda se äänekkäästi esiin sen sijaan, että se peiteltäisiin kääntymään sattuvalla matematiikalla.
Epäonnistumiset eivät saa ylittää rajaa
Lukutoiminto voi epäonnistua. Virta voi olla verkkopohjainen olio, joka aikakatkaistaan, blob-kahva, joka suljettiin allasi, tai tiedosto, jota tynkittiin dokumentin avaamisen jälkeen. PDFiumin sopimus lukutakaisinkutsulle on paluuarkki: ei-nolla onnistumiselle, nolla epäonnistumiselle. Kyseessä on C-kehys, eikä siinä ole koneistoa Pascal-poikkeuksen nappaamiseksi tai levittämiseksi.
Tämän vuoksi trampoliini käärii haun ja luvun try/except-lohkoon, joka nielee poikkeuksen ja palauttaa nollan. Jos Delphi-poikkeuksen sallittaisiin levitä ulos takaisinkutsusta, se purkautuisi PDFiumin cdecl-pinokehysten läpi, joita ei koskaan rakennettu Pascalin poikkeuskoneiston purettaviksi. Tuloksena on parhaimmillaan määrittelemätöntä käyttäytymistä ja pahimmillaan kova kaatuminen syvällä PDF-jäsentimessä ilman käyttökelpoista pinoa. Nollan palauttaminen pitää epäonnistumisen sopimuksen sisällä. PDFium näkee epäonnistuneen lohkoluvun, keskeyttää operaation puhtaasti, ja FPDF_LoadCustomDocument ilmoittaa, ettei dokumenttia voitu ladata, minkä komponentti tuo pintaan EPdfError-virheenä Pascal-puolella, jonne se kuuluu.
Dokumentin avaaminen tällä tavalla
Komponenttimetodi, joka ajaa suoratoistopolkua, on LoadCustomDocument, joka on esitelty erillisenä metodina eikä uutena LoadDocument-ylikuormituksena, jotta TMemoryStream-virran välittäminen ei koskaan vahingossa päädy puskuroituun polkuun. Se rakentaa sovittimen, kutsuu FPDF_LoadCustomDocument-funktiota ja pitää sovittimen elossa ladatun dokumentin eliniän ajan.
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;
Sama kutsu toimii TMemoryStream-virralle, tietokannan tietojoukon blob-virralle tai mukautetulle TStream-jälkeläiselle. Tarpeenmukainen lataus ansaitsee paikkansa silloin, kun tiedosto on suuri ja vain osa siitä luetaan: arkistokatselussa, muutamia sivuja näyttelevässä pikkukuvageneraattorissa tai hakuhakemistossa, joka vetää yhden sivun kerrallaan. Kun tiedosto on pieni tai aiot joka tapauksessa lukea sen kokonaan, puskuroitu lataus on yksinkertaisempi eikä suoratoistokoneisto tuo sinulle mitään hyötyä. Ratkaiseva tekijä on todellisuudessa koskettamiesi tavujen suhde tiedoston sisältämiin tavuihin.
Once pages stream in on demand, the next concern is keeping rendered pages responsive as the user zooms and scrolls, which is covered in our note on render caching and zoom performance. When the streamed document is one a viewer should display but not let the user export or alter, the techniques in the secure PDF preview walkthrough pair naturally with this loading path. Both build on the streaming load described here, which ships as part of the PDFium Component for Delphi and C++Builder alongside the rendering, text extraction, and annotation APIs covered elsewhere on this blog.