Skannattu arkisto voi kasvaa useiden gigatavujen kokoiseksi 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 hahmontamiseksi on tuhlausta jokaiselta kannalta: se polttaa osoiteavaruutta, se jumittaa käyttäjän pitkän alkulukuoperaation taakse, ja 32-bittisessä Delphi-prosessissa se voi epäonnistua suoraan ennen kuin yhtäkään sivua ilmestyy. PDFium rakennettiin tämä mielessä pitäen. Se voi ladata asiakirjan takaisinkutsun kautta, joka pyytää juuri niitä tavualueita, joita se tarvitsee, silloin kun se tarvitsee niitä, eikä se koskaan vaadi koko tiedostoa kerralla. Yksi raja kannattaa mainita heti alkuun: tämä suoratoistokanava kuvailee tiedostoa 32-bittisellä pituudella, joten se palvelee yksittäistä tiedostoa enintään 4 GiB:n kokoon asti, mikä kattaa käytännössä lähes kaikki skannatut arkistot. Tuon rajan ylittävä tiedosto ei ole tämän artikkelin aluetta; se kannattaa jakaa taltioihin skannausaikana tai avata sen sijaan suorakäyttöstrategian kautta, ja rajaa valvova suoja saa rehellisesti sanoen oman osionsa alempana
Komponentti paljastaa tuon polun suoratoistosovittimen kautta. Luovutat sille minkä tahansa TStream-objektin, ja PDFium vetää lohkoja tuosta 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-API lataa asiakirjan kutsujan toimittamasta objektista, jota kuvaa FPDF_FILEACCESS-rakenne. Rakenteessa on kolme osaa, joilla on tässä väliä: pituuskenttä, lukutakaisinkutsu ja läpinäkymätön käyttäjäparametri. Aloituspiste, joka käyttää sitä, on FPDF_LoadCustomDocument. Kun PDFium pitää tuota rakennetta hallussaan, se jäsentää trailerin, paikallistaa ristiviittaustaulukon ja lukee siitä lähtien vain sen, mitä kulloinenkin toiminto vaatii. Asiakirjan avaaminen koskettaa tiedoston häntää ja kourallista luettelo-objekteja. Sivun 400 hahmontaminen lukee vain kyseisen sivun sisältövirrat ja resurssit, ei mitään muuta
Tässä on ero puskuroidun latauksen ja suoratoistetun latauksen välillä. Puskuroitu lataus lukee tiedoston alusta loppuun ennen kuin PDFium näkee nollatavua. Suoratoistettu lataus kääntää suhteen päinvastaiseksi: PDFium ohjaa lukemista, ja tavuja, joita ei koskaan kosketeta, ei myöskään koskaan lueta. Monigigatavuiselle tiedostolle, jota katsellaan sivu kerrallaan, tuo on ero käyttökelvottoman ja välittömän latauksen välillä
Suoratoistosovitin
Sovitin, joka siltaa Delphin TStream-luokan FPDF_FILEACCESS-rakenteeseen, on TPdfStreamAdapter. Sen rakentaja ottaa virran ja omistajuuslipun, tallentaa virran pituuden kerran, täyttää FPDF_FILEACCESS-tietueen ja kytkee lukutakaisinkutsun. Kun PDFium myöhemmin kutsuu takaisin siirtymän ja koon kanssa, sovitin hakee virran kyseiseen siirtymään ja kopioi täsmälleen sen alueen puskuriin, jonka PDFium toimitti
// Sanatarkasti komponentista: streamin ja FPDF_FILEACCESS:n välinen silta
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 on 32-bittinen etumerkitön long. Kieltäydy
// streamista, joka typistäisi hiljaa yli 4 GiB:n rajan.
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, niin kutsuja pitää virran hallussaan ja sen täytyy pitää se hengissä koko asiakirjan eliniän. Välitä True, niin sovitin ottaa vastuun ja vapauttaa virran, kun asiakirja sulkeutuu. Kummassakin tapauksessa virran täytyy elää pidempään kuin jokainen luku, jonka PDFium suorittaa, koska PDFium pitää hallussaan FPDF_FILEACCESS-osoitinta ja kutsuu takaisin milloin tahansa asiakirjan ollessa auki, ei vain alkulatauksen aikana
Miksi takaisinkutsu on staattinen funktio
Lukutakaisinkutsu, jonka PDFium tallentaa ominaisuuteen m_GetBlock, on pelkkä C-funktio-osoitin, jolla on cdecl-kutsukäytäntö. Delphi-metodia ei voida käyttää suoraan, koska metodi kantaa mukanaan piilotettua Self-argumenttia, josta C-kutsuja ei tiedä mitään eikä koskaan toimita sitä. Sovitin ilmoittaa siksi takaisinkutsun muodossa class function, joka on merkitty määreillä cdecl; static ja joka kääntyy vapaasti seisovaksi funktioksi, jolla on C-kehysasettelu, jota PDFium odottaa, ja jolla ei ole implisiittistä Self-argumenttia
Tämä ratkaisee kutsukäytännön, mutta nostaa esiin toisen kysymyksen: ilman Self-argumenttia, miten takaisinkutsu tavoittaa sen tietyn virran, josta sen oletetaan lukevan? Vastaus on läpinäkymätön käyttäjäparametri. Kun sovitin rakentaa tietueen, se tallentaa oman ilmentymänsä osoittimen ominaisuuteen m_Param. PDFium antaa saman osoittimen takaisin jokaisen takaisinkutsun ensimmäisenä argumenttina. Staattinen funktio muuntaa sen tyyppimuunnoksella takaisin TPdfStreamAdapter-luokaksi ja ohjaa lukupyynnön kyseisen ilmentymän virtaa vastaan. Tämä on standarditrampoliini objektikontekstin välittämiseen C-rajan yli, jolla ei ole käsitystä objekteista
// Sanatarkasti komponentista: cdecl-trampoliini takaisin ilmentymään
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); // palauta ilmentymä ominaisuudesta 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; // raportoi epäonnistuminen paluuarvolla, älä koskaan poikkeuksella
end;
end;
4 GiB:n raja ja miksi se tarvitsee suojan
Tästä tulee alussa mainittu raja. Pituuskenttä m_FileLen rakenteessa FPDF_FILEACCESS on 32-bittinen etumerkitön arvo. Sen suurin esitettävissä oleva pituus on yhtä tavua vaille 4 GiB. TStream raportoi kokonsa Int64-arvona, joten virta voi kuvata paljon enemmän tavuja kuin mitä kenttä voi sisältää. Heti kun virran koko ylittää tuon rajan, ei ole enää mitään rehellistä tapaa kertoa PDFiumille, kuinka pitkä tiedosto on
Väärä reaktio on määrittää koko ja antaa sen pyörähtää ympäri. 5 GiB:n pituuden typistäminen 32-bittiseksi kentäksi tuottaa pienen, uskottavalta näyttävän luvun, ja PDFium jäsentää sitten tiedoston uskoen sen päättyvän karkeasti gigatavun kohdalla. Traileri ja ristiviittaustaulukko sijaitsevat tiedoston todellisessa lopussa, reilusti typistetyn pituuden ohitse, joten jäsennys epäonnistuu tavalla, jolla ei ole mitään tekemistä todellisen syyn kanssa. Virheenjäljittäisit ristiviittausvirhettä tiedostosta, joka on täysin pätevä, ilman minkäänlaista vihjettä siitä, että kokonaisluku pyörähti ympäri kaksi kerrosta ylempänä
Sovitin sen sijaan kieltäytyy syötteestä. Rakentaja vertaa virran kokoa arvoon High(FPDF_DWORD) ja nostaa EPdfError-poikkeuksen välittömästi, kun virta on liian suuri kuvattavaksi. Nimenomainen, välitön virhe nimeää todellisen ongelman jo rakennushetkellä. Hiljainen typistäminen piilottaa sen harhaanjohtavan oireen taakse, jota jouduttaisiin jahtaamaan paljon myöhemmin. 4 GiB:n raja on tämän latauspolun aito rajoite, ja rehellinen ratkaisu on tuoda se esiin äänekkäästi sen sijaan, että se peiteltäisiin aritmetiikalla, joka sattuu vain kääntymään. Kun arkisto aidosti ylittää rajan, alussa luvatut korjauskeinot elävät tämän rajapinnan ulkopuolella: jaa skannaus taltiokohtaisiin tiedostoihin, joista jokainen pysyy rajan alapuolella, tai jätä asiakirja levylle ja palvele sitä 64-bittisiin siirtymiin rakennetun suorakäyttösuunnittelun kautta FPDF_FILEACCESS-rakenteen sijaan
Virheet eivät saa ylittää rajaa
Luku voi epäonnistua. Virta saattaa olla verkkotaustainen objekti, joka aikakatkaistaan, blob-kahva, joka suljettiin altasi, tai tiedosto, joka typistettiin asiakirjan avaamisen jälkeen. PDFiumin sopimus lukutakaisinkutsulle on paluuarvo: nollasta poikkeava onnistumiselle, nolla epäonnistumiselle. Se on C-kehys, eikä sillä ole koneistoa Pascal-poikkeuksen nappaamiseen tai välittämiseen
Tästä syystä trampoliini kietoo haun ja lukemisen try/except-lohkoon, joka nielee poikkeuksen ja palauttaa nollan. Jos Delphi-poikkeuksen annettaisiin edetä ulos takaisinkutsusta, se kelautuisi auki PDFiumin cdecl-pinokehysten läpi, joita ei koskaan rakennettu Pascal-poikkeuskoneiston kelattaviksi. Tuloksena on parhaimmillaan määrittelemätön toiminta ja pahimmillaan kova kaatuminen, syvällä PDF-jäsentimen sisällä ilman käyttökelpoista pinoa. Nollan palauttaminen pitää epäonnistumisen sopimuksen sisällä. PDFium näkee epäonnistuneen lohkoluvun, keskeyttää toiminnon siististi, ja FPDF_LoadCustomDocument raportoi, ettei asiakirjaa voitu ladata, minkä komponentti tuo esiin EPdfError-poikkeuksena Pascal-puolella, minne se kuuluukin
Asiakirjan avaaminen tällä tavalla
Komponentin metodi, joka ohjaa suoratoistopolkua, on LoadCustomDocument, joka on ilmoitettu omana metodinaan eikä toisena LoadDocument-ylikuormituksena, jotta TMemoryStream-luokan välittäminen ei koskaan vahingossa päädy puskuroidulle polulle. Se rakentaa sovittimen, kutsuu FPDF_LoadCustomDocument-metodia ja pitää sovittimen elossa ladatun asiakirjan koko eliniän
var
Pdf: TPdf;
FileStream: TFileStream;
begin
Pdf := TPdf.Create(nil);
FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
try
// Luovuta stream-omistajuus Pdf:lle: se vapauttaa FileStreamin, kun asiakirja sulkeutuu.
Pdf.LoadCustomDocument(FileStream, True);
// PDFium on toistaiseksi lukenut vain trailerin ja katalogin.
// Sivun hahmontaminen vetää vain kyseisen sivun tavut takaisinkutsun kautta.
// ... hahmonna tai tarkastele sivuja tässä ...
finally
Pdf.Free; // sulkee asiakirjan, mikä vapauttaa sovittimen ja virran
end;
end;
Sama kutsu toimii TMemoryStream-luokalle, tietokannan tietojoukon blob-virralle tai mukautetulle TStream-jälkeläiselle. On-demand-lataus ansaitsee paikkansa, kun tiedosto on suuri ja vain osa siitä luetaan: arkiston katseluohjelma, pikkukuvageneraattori, joka ottaa näytteitä muutamalta sivulta, hakuindeksi, 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 lisäarvoa. Ratkaiseva tekijä on niiden tavujen suhde, joita todella kosketat, tiedoston sisältämiin tavuihin
Kun sivut suoratoistetaan tarpeen mukaan, seuraava huolenaihe on pitää hahmonnetut sivut responsiivisina, kun käyttäjä zoomaa ja vierittää, mitä käsitellään hahmonnuksen välimuistia ja zoomaussuorituskykyä käsittelevässä huomautuksessamme. Kun suoratoistettu asiakirja on sellainen, jonka katseluohjelman tulisi näyttää mutta jota käyttäjä ei saa viedä tai muuttaa, turvallisen PDF-esikatselun läpikäynnissä esitellyt tekniikat sopivat luontevasti yhteen tämän latauspolun kanssa. Molemmat rakentuvat tässä kuvatulle suoratoistetulle lataukselle, joka toimitetaan osana PDFium Component -komponenttia Delphille ja C++Builderille, hahmontamisen, tekstinpoiminnan ja merkintöjen API:en rinnalla, joita käsitellään muualla tässä blogissa