Tekninen artikkeli

Valtavien PDF-tiedostojen suoratoisto tarpeen mukaan PDFiumilla Delphissä

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ä

Arkkitehtuurikaavio vertailee puskuroitua latausta, joka kopioi monigigatavuisen PDF:n muistiin ennen jäsennystä, ja striimausta, jossa PDFium pyytää tavuvälejä Delphi-TStreamista FPDF_FILEACCESSin kautta
Avaus maksaa vain trailerin ja katalogin; sivun 400 renderöinti vetää sivun 400 tavut eikä mitään muuta callbackin kautta

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

Kaavio cdecl-trampoliinista, joka kantaa PDFium-lohkopyynnöt C-rajasta Delphi TPdfStreamAdapter -instanssiin ja kutistaa poikkeukset nollan paluuarvoksi
Staattinen cdecl-callback ei kätkeä implisiittistä Self-osoitinta, joten m_Param luovuttaa adapterin instanssin takaisin jokaiseen kutsuun, ja mikä tahansa Pascal-poikkeus kutistuu nollapaluksi
// 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

Päätöskaavio, joka vartioi FPDF_FILEACCESSin 4 GiB -rajaa, jossa ylikokoinen Delphi-TStream nostaa EPdfErrorin välittömästi kääriytymättä hiljaa julistettuun pituuskenttään
Välitön EPdfError voittaa aritmetiikan, joka vain kääntyy: kiertävä m_FileLen lähettää debuggauksen kuvitteellista ristiviiteketjua pitkin

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