Skenuotas archyvas viename PDF faile gali siekti kelis gigabaitus. Tokį failą atidaranti peržiūros programa paprastai nori parodyti vieną puslapį, galbūt turinį, arba puslapį, į kurį vartotojas perėjo iš žymelės. Viso failo skaitymas į atmintį, siekiant atvaizduoti du puslapius, yra neekonomiškas visais atžvilgiais: tai eikvoja adresų erdvę, vilkina vartotojo darbą dėl ilgo pradinio skaitymo, o 32 bitų „Delphi“ procese tai gali visiškai nepavykti dar nepasirodžius nė vienam puslapiui. „PDFium“ buvo sukurtas atsižvelgiant į tai. Jis gali įkelti dokumentą per atgalinį iškvietimą, kuris prašo konkrečių reikalingų baitų diapazonų tada, kai jų reikia, ir niekada nereikalauja viso failo iš karto. Viena riba yra nustatyta iš anksto: šis srautinio perdavimo kanalas apibūdina failą 32 bitų ilgiu, todėl jis aptarnauja vieną failą iki 4 GiB, o tai praktiškai apima beveik kiekvieną nuskaitytą archyvą. Failas, peržengiantis šią ribą, nėra šio straipsnio tema; nuskaitymo metu jį reikėtų padalyti į tomus arba atidaryti naudojant tiesioginės prieigos strategiją, o apsauga, užtikrinanti šią viršutinę ribą, nuoširdžiai gaus savo atskirą skyrių žemiau
Komponentas atskleidžia šį kelią per srauto adapterį. Jūs perduodate jam bet kokį TStream, ir „PDFium“ pagal pareikalavimą traukia blokus iš to srauto. Failas gali būti diske, duomenų bazės „blob“ lauke arba už bet kokio kito TStream palikuonio, ir nė viena jo dalis nėra iš anksto nukopijuojama į atmintį
Kaip „PDFium“ prašo baitų
„PDFium“ C API įkelia dokumentą iš skambinančiojo pateikto objekto, aprašyto FPDF_FILEACCESS struktūra. Čia svarbios trys struktūros dalys: ilgio laukas, skaitymo atgalinis iškvietimas (angl. read callback) ir nepermatomas vartotojo parametras (angl. opaque user parameter). Įvesties taškas, kuris jį naudoja, yra FPDF_LoadCustomDocument. Kai „PDFium“ gauna šią struktūrą, jis išanalizuoja pabaigą (trailer), randa kryžminių nuorodų lentelę (cross-reference table) ir nuo tada skaito tik tai, ko reikalauja tam tikra operacija. Dokumento atidarymas paliečia failo uodegą ir keletą katalogo objektų. 400 puslapio atvaizdavimas nuskaito to puslapio turinio srautus ir išteklius, ir nieko daugiau
Tai yra skirtumas tarp buferinio įkėlimo ir srautinio įkėlimo. Buferinis įkėlimas nuskaito failą nuo pradžios iki pabaigos dar prieš tai, kai „PDFium“ pamato nulinį baitą. Srautinis įkėlimas apverčia šį santykį: „PDFium“ valdo skaitymą, o baitai, kurie niekada nepaliečiami, niekada nėra skaitomi. Kelių gigabaitų faile, peržiūrimame po vieną puslapį, tai yra atotrūkis tarp neįmanomo naudoti įkėlimo ir momentinio
Srauto adapteris
Adapteris, kuris sujungia „Delphi“ TStream su FPDF_FILEACCESS, yra TPdfStreamAdapter. Jo konstruktorius paima srautą ir nuosavybės vėliavėlę (ownership flag), vieną kartą užfiksuoja srauto ilgį, užpildo FPDF_FILEACCESS įrašą ir susieja skaitymo atgalinį iškvietimą. Kai „PDFium“ vėliau iškviečia atgal su poslinkiu ir dydžiu, adapteris perkelia srautą į tą poslinkį ir nukopijuoja tiksliai tą diapazoną į buferį, kurį pateikė „PDFium“
// 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;
Nuosavybės vėliavėlė nusprendžia, kas atlaisvina srautą. Perduokite False, ir skambinantysis pasilieka srautą bei privalo išlaikyti jį gyvą visą dokumento gyvavimo laiką. Perduokite True, ir adapteris perima valdymą, atlaisvindamas srautą, kai dokumentas uždaromas. Bet kokiu atveju srautas turi pergyventi kiekvieną „PDFium“ atliktą skaitymą, nes „PDFium“ laiko FPDF_FILEACCESS rodyklę ir gali atlikti atgalinį iškvietimą bet kuriuo metu, kol dokumentas yra atidarytas, o ne tik pradinio įkėlimo metu
Kodėl atgalinis iškvietimas yra statinė funkcija
Skaitymo atgalinis iškvietimas, kurį „PDFium“ saugo m_GetBlock, yra paprasta C funkcijos rodyklė su cdecl iškvietimo konvencija. „Delphi“ metodo negalima naudoti tiesiogiai, nes metodas turi paslėptą Self argumentą, apie kurį C skambinantysis nieko nežino ir niekada nepateiks. Todėl adapteris deklaruoja atgalinį iškvietimą kaip class function, pažymėtą cdecl; static, kuri kompiliuojama į savarankišką funkciją su C rėmelio išdėstymu, kurio tikisi „PDFium“, ir be numanomo Self
Tai išsprendžia iškvietimo konvenciją, bet kelia antrą klausimą: be Self, kaip atgalinis iškvietimas pasiekia konkretų srautą, iš kurio jis turi skaityti? Atsakymas yra nepermatomas vartotojo parametras. Kai adapteris sukuria įrašą, jis išsaugo savo paties instancijos rodyklę m_Param lauke. „PDFium“ grąžina tą pačią rodyklę kaip pirmąjį argumentą kiekviename atgaliniame iškvietime. Statinė funkcija paverčia jį atgal į TPdfStreamAdapter ir nukreipia skaitymą į tos instancijos srautą. Tai yra standartinis batutas (trampoline), skirtas perduoti objekto kontekstą per C ribą, kuri neturi jokios objektų sampratos
// 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 riba ir kodėl jai reikia apsaugos
Štai iš kur atsiranda įžangoje nurodyta riba. Ilgio laukas m_FileLen struktūroje FPDF_FILEACCESS yra 32 bitų neženklinė (unsigned) reikšmė. Didžiausias jos atvaizduojamas ilgis yra vienu baitu mažesnis nei 4 GiB. TStream praneša apie savo dydį kaip Int64, todėl srautas gali aprašyti kur kas daugiau baitų, nei laukas gali sutalpinti. Tą akimirką, kai srauto dydis viršija šią ribą, nėra jokio sąžiningo būdo pasakyti „PDFium“, kokio ilgio yra failas
Klaidingas atsakas būtų priskirti dydį ir leisti jam persisukti (wrap). Sutrumpinus 5 GiB ilgį iki 32 bitų lauko, gaunamas mažas, įtikinamai atrodantis skaičius, ir tada „PDFium“ analizuos failą tikėdamas, kad jis baigiasi maždaug po gigabaito. Pabaiga ir kryžminių nuorodų lentelė yra tikrojoje failo pabaigoje, gerokai už sutrumpinto ilgio, todėl analizė nepavyksta tokiu būdu, kuris neturi nieko bendro su tikrąja priežastimi. Jūs derintumėte kryžminių nuorodų klaidą faile, kuris yra visiškai tinkamas, be jokios užuominos, kad sveikasis skaičius persisuko dviem lygiais aukščiau
Vietoj to adapteris atmeta įvestį. Konstruktorius palygina srauto dydį su High(FPDF_DWORD) ir sukelia EPdfError tą pačią akimirką, kai srautas tampa per didelis, kad jį būtų galima aprašyti. Aiškus, tiesioginis klaidos pranešimas įvardija tikrąją problemą konstravimo metu. Tylus sutrumpinimas paslepia ją už klaidinančio simptomo, kurį persekiotumėte daug vėliau. 4 GiB apribojimas yra tikras šio įkėlimo kelio apribojimas, ir sąžiningiausia yra tai garsiai iškelti, o ne dangstyti aritmetika, kuri atsitiktinai kompiliuojasi. Kai archyvas išties peržengia ribą, viršuje pažadėtos priemonės gyvuoja už šios API ribų: padalykite nuskaitymą į atskirus tomų failus, kurių kiekvienas neviršija ribos, arba palikite dokumentą diske ir aptarnaukite jį naudodami tiesioginės prieigos dizainą, pagrįstą 64 bitų poslinkiais, o ne per FPDF_FILEACCESS
Gedimai neturi peržengti ribos
Skaitymas gali nepavykti. Srautas gali būti tinkle esantis objektas, kuriam baigiasi laikas, „blob“ rankena, kuri buvo uždaryta po jumis, arba failas, kuris buvo sutrumpintas po dokumento atidarymo. „PDFium“ sutartis dėl skaitymo atgalinio iškvietimo yra grąžinama reikšmė: ne nulis sėkmės atveju, nulis – gedimo. Tai yra C rėmelis, ir jis neturi jokio mechanizmo, kaip sugauti ar perduoti Paskalio (Pascal) išimtį
Štai kodėl batutas (trampoline) apgaubia paiešką (seek) ir skaitymą į try/except, kuris praryja išimtį ir grąžina nulį. Jei „Delphi“ išimčiai būtų leista plisti už atgalinio iškvietimo ribų, ji atsuktių „PDFium“ cdecl steko rėmelius, kurie niekada nebuvo sukurti tam, kad juos atsuktu Paskalio išimčių mechanizmas. Rezultatas geriausiu atveju yra neapibrėžtas elgesys (undefined behavior), o blogiausiu – kieta avarija giliai PDF analizatoriuje be naudingo steko. Nulio grąžinimas išlaiko gedimą sutarties ribose. „PDFium“ mato nepavykusį bloko skaitymą, švariai nutraukia operaciją, ir FPDF_LoadCustomDocument praneša, kad dokumento nepavyko įkelti, o tai komponentas iškelia kaip EPdfError Paskalio pusėje, kur jam ir vieta
Dokumento atidarymas šiuo būdu
Komponento metodas, kuris valdo srautinio perdavimo kelią, yra LoadCustomDocument, paskelbtas kaip atskiras metodas, o ne dar viena LoadDocument perkrova, kad perduodant TMemoryStream niekada netyčia nepatektumėte į buferinį kelią. Jis sukuria adapterį, iškviečia FPDF_LoadCustomDocument ir išlaiko adapterį gyvą visą įkelto dokumento gyvavimo laiką
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;
Tas pats iškvietimas veikia ir TMemoryStream, „blob“ srautui iš duomenų bazės rinkinio arba pasirinktiniam TStream palikuoniui. Įkėlimas pagal pareikalavimą atsiperka, kai failas yra didelis ir bus skaitoma tik jo dalis: archyvo peržiūros programa, miniatiūrų generatorius, kuris paima kelis puslapius, paieškos indeksas, kuris traukia po vieną puslapį. Kai failas yra mažas arba vis tiek ketinate jį visą perskaityti, buferinis įkėlimas yra paprastesnis, ir srautinio perdavimo mechanizmas neduoda jokios naudos. Lemiantis veiksnys yra baitų, kuriuos faktiškai paliesite, ir failo turimų baitų santykis
Kai puslapiai pradedami srautu perduoti pagal pareikalavimą, kitas rūpestis yra išlaikyti atvaizduotų puslapių reagavimą vartotojui masteliuojant ir slenkant, kas aptariama mūsų pastaboje apie atvaizdavimo talpyklą ir mastelio keitimo našumą. Kai srautiniu būdu perduodamas dokumentas yra toks, kurį peržiūros programa turėtų rodyti, bet neleisti vartotojui eksportuoti ar keisti, metodai, esantys saugaus PDF peržiūros vadove, natūraliai dera su šiuo įkėlimo keliu. Abu remiasi čia aprašytu srautiniu įkėlimu, kuris pristatomas kaip „PDFium“ komponento, skirto „Delphi“ ir „C++Builder“, dalis kartu su atvaizdavimo, teksto išgavimo ir anotacijų API, aptariamais kitur šiame tinklaraštyje