Vienas A4 formato puslapis, atvaizduotas patogiu skaitymo masteliu, užima kelis megabaitus 32 bitų taškinio vaizdo (bitmap) atminties. Padauginkite tai iš 400 puslapių sutarties ir skaičiavimai nebėra abstraktūs: jei atvaizduosite kiekvieną puslapį iš anksto, pareikalausite iš „Windows“ daugiau nei gigabaito taškinių vaizdų, kuriuos vartotojas žiūrės po vieną ekrano užpildymą vienu metu. Programa arba pritrūks adresų erdvės 32 bitų versijoje, arba praleis pirmąsias kelias sekundes užstrigusi, kol GPU ir puslapių analizatorius apdoros puslapius, iki kurių niekas dar net neslinko. Nepertraukiamo slinkimo skaitytuvas turi atrodyti kaip viena ilga puslapių juosta, tačiau jis negali vienu metu atmintyje talpinti visų puslapių.
Šis prieštaravimas yra visa esminė problema. „PDFium VCL“ ją išsprendžia komponento TPdfView viduje, zodžiu, didžioji dalis darbo yra tiesiog tinkamo rodymo režimo parinkimas ir supratimas, ką komponentas atlieka už jus. Dalys, kurių jis neatlieka – puslapių dydžio nustatymas skaitymo srautui ir greito slinkimo spartos palaikymas – yra tos vietos, kurioms reikės parašyti šiek tiek kodo. Jei vis dar dėliojate aplinkinį rėmą (įrankių juostą, miniatiūras, paieškos laukelį), funkcijomis gausaus peržiūros programos kūrimo vadovas apima šią temą; čia pagrindinis dėmesys skiriamas pačiam slinkimui.
Išdėstymas yra rodymo režimas, o ne taškinio vaizdo skydelis
Kuriantiems VCL formas intuityviai norisi naudoti slinkties dėžutę (scroll box) ir sudėti į ją vaizdo valdiklius (image controls) po vieną kiekvienam puslapiui. Venkite to. Toks dizainas priverčia jus pačius valdyti puslapių pozicionavimą, slinkties matematiką ir atminties klausimą vienu metu, ir jūs visa tai realizuosite prastai. TPdfView jau modeliuoja dokumentą kaip nepertraukiamą puslapių seką ir pateikia šį išdėstymą per savo DisplayMode savybę.
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // one page wide, scrolls vertically
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
Tai yra visa nepertraukiamo slinkimo sąranka. dmSingleContinuous išdėsto puslapius viename vertikaliame stulpelyje su tarpais, kurie valdomi viduje, o vaizdas slenka per šį stulpelį kaip per vieną paviršių. Nereikia susieti jokių atskirų puslapių valdiklių ir rašyti slinkties apdorojimo programos įprastai navigacijai. Atkreipkite dėmesį į Pdf.Active patikrą po priskyrimo: dokumento atidarymas niekada neišmeta išimties, zodžiu, sugadintas ar slaptažodžiu apsaugotas failas palieka Active reikšmę ties False be jokių pagaunamų išimčių, o peržiūros programa, praleidžianti šią patikrą, tiesiog rodys tuščią skydelį ir klaidins vartotoją.
Ta pati savybė valdo ir puslapių poravimo (spread) režimus. dmTwoPageContinuous išdėsto puslapius šalia vienas kito, po du eilutėje, knygos stiliaus skaitymui; dmTwoPageContinuousWithCover daro tą patį, bet leidžia pirmam puslapiai stovėti atskirai kaip viršeliui, kad likusios poros išsidėstytų natūraliose lyginio ir nelyginio puslapio ribose. Visi trys režimai slenka nepertraukiamai. Perjungimas tarp jų yra vienas priskyrimas, todėl vėliau lengva pridėti rodymo režimo pasirinkimo lauką.
Rastruojami tik matomi puslapiai
Priežastis, kodėl tai tinka 400 puslapių failui, yra ta, kad stulpelis yra virtualus. TPdfView žino kiekvieno puslapio aukštį iš dokumento puslapių medžio, todėl gali apskaičiuoti bendrą slinkties mastą ir kiekvieno puslapio poziciją nieko nerastruodamas. Rastravimas – brangus veiksmas, paverčiantis puslapio turinio srautą pikseliais – vyksta tik tiems puslapiams, kurie šiuo metu patenka į matomumo sritį (viewport), pridedant nedidelę atsargą, kad puslapis būtų paruoštas dar prieš jam pasirodant ekrane. Slenkant žemyn, į viewport patenkantys puslapiai yra atvaizduojami, o išeinantys puslapiai atlaisvina savo atmintį. Atminties sąnaudos išlieka proporcingos tam, kas telpa ekrane, o ne dokumento ilgiui.
Tai verta įsidėmėti, nes tai keičia jūsų požiūrį į našumo sąnaudas. Atidaryti 400 puslapių dokumentą yra pigus veiksmas: jis analizuoja struktūrą, o ne turinį. Sąnaudos patiriamos atskiriems puslapiams ir yra mokamos vėluojančiu būdu (lazily), būtent tuo momentu, kai puslapis atsiranda netoliese. Peržiūros programa, kuri atsidaro akimirksniu ir slenka sklandžiai, apskritai neatlieka mažiau darbo – ji tiesiog paskirsto darbą vartotojo skaitymo kelyje ir atmeta tai, kas lieka praeityje. Praktinė pasekmė: beveik niekada nereikia priverstinai generuoti puslapių anksčiau laiko. Leiskite vaizdui pačiam nuspręsti, kas yra matoma.
Pritaikykite puslapius pagal plotį, o mastelį palikite ramybėje
Skaitymo stulpeliui reikia puslapių, pritaikytų pagal skydelio plotį, o ne pririštų prie absoliutaus mastelio. Savybė FitMode tai atlieka ir toliau palaiko keičiantis lango dydžiui.
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Esant pasirinktam pfmFitWidth režimui, komponentas perskaičiuoja mastelį kaskart pasikeitus vaizdo dydžiui, todėl stulpelis visada užpildo prieinamą plotį, o puslapių aukščiai ir slinkties mastas seka paskui. Yra vienas spąstas: tiesioginis Zoom priskyrimas anuliuoja FitMode atgal į pfmNone. Tai daroma sąmoningai, nes rankinis mastelio keitimas ir automatinis pritaikymas prie pločio yra prieštaringi ketinimai, tačiau tai reiškia, kad kur nors kode paliktas priskyrimas PdfView.Zoom := 1.0 tyliai išjungs pritaikymą prie pločio ir kitas vaizdo dydžio keitimas neatliks perskaičiavimo. Jei siūlote ir mastelio valdymą, ir pritaikymo mygtuką, vertinkite juos kaip režimų perjungimą: vieno nustatymas išvalo kitą, ir jūs sprendžiate, kuris turi pirmenybę.
Absoliutaus mastelio valdikliams vaizdas pateikia pritaikymo mastelius kaip reikšmes, kurias galite pritaikyti arba rodyti: PageWidthZoom[PageNumber] grąžina mastelį, kuris pritaikytų tą puslapį pagal plotį, o atitinkamas PageZoom pritaiko visą puslapį. Šių reikšmių nuskaitymas leidžia sukurti meniu „Pritaikyti pagal plotį“ / „Pritaikyti puslapį“, nenaudojant griežtai užkoduotų magiškų procentų, kurie neveiktų gulsčiuose ar nestandartinio dydžio puslapiuose.
Išlaikykite greitą slinkimą spariu naudodami laipsnišką atvaizdavimą
Numatytasis generavimo kelias nupiešia puslapį iki galo prieš grąžindamas rezultatą. Vieno puslapio atveju tai tinka. Tačiau greitai slenkant per sudėtingą dokumentą tai netinka: kiekvienas prašmėžuojantis puslapis pradeda pilną rastravimą, o jei vartotojas slenka greičiau nei puslapiai spėja generuotis, šie darbai kaupiasi ir skydelis pradeda strigti, nes darbas atliekamas tiems puslapiams, kurie jau seniai paliko ekraną. Sprendimas yra padaryti atvaizdavimą atšaukiamu ir nutraukti jį iškart, kai vartotojas slenka toliau.
RenderPageProgressive atlieka generavimą dalimis ir tikrina atšaukimo žetoną (cancellation token) ties kiekvienos dalies riba, todėl pradėtas puslapio, kuris ką tik nuslinko, generavimas gali būti nutrauktas, užuot vykdžius jį iki galo.
type
TFormMain = class(TForm)
// ...
private
FRenderCancel: IPdfCancellationTokenSource;
procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
end;
procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
Status: TPdfProgressiveStatus;
begin
// Cancel whatever was rendering; the old token is now signaled.
if Assigned(FRenderCancel) then
FRenderCancel.Cancel;
FRenderCancel := TPdfCancellationTokenSource.New;
Pdf.PageNumber := PageNo;
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
FRenderCancel.Token);
case Status of
prsDone: ; // bitmap is complete, paint it
prsCancelled: Exit; // superseded, discard this result
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
Svarbiausia čia yra grąžinama reikšmė. prsDone reiškia, kad taškinis vaizdas yra pilnai nupieštas ir paruoštas rodymui; prsCancelled reiškia, kad naujesnė slinkties pozicija pakeitė šį puslapį, todėl dalinį rezultatą tiesiog atmetate; prsFailed reiškia tikrą puslapio klaidą. Atšaukimas tikrinamas tarpinių dalių ribose, o ne iš anksto, todėl tikėkitės kelių dešimčių milisekundžių vėlavimo tarp Cancel iškvietimo ir realaus generavimo sustabdymo. Tai vis tiek yra nepalyginamai pigiau nei leisti pasenusiam viso puslapio generavimui blokuoti eilę. Perduodant nil kaip žetoną, generavimas vykdomas tiesiogiai iki galo – tai teisingas pasirinkimas vienkartiniam generavimui, pavyzdžiui, spaudinio peržiūrai, kur nėra poreikio ką nors atšaukti.
Kai vietoje to kviečiate funkciją RenderPage, kuri grąžina naują TBitmap, prisiminkite, kad iškvietėjas tampa jo savininku ir privalo jį atlaisvinti (Free). Slinkties cikle, kuris išskiria taškinį vaizdą kiekvienam puslapiui, šio žingsnio pamiršimas sukels atminties nutekėjimą, augantį sulig kiekvienu pravažiuotu puslapiu – o tai yra būtent tokia atminties problema, kurios nepertraukiamo slinkimo dizainas turėjo padėti išvengti. Kur įmanoma, atvaizduokite į pakartotinai naudojamą taškinį vaizdą.
Kas lieka jums
Nepertraukiamo slinkimo skaitytuvo realizacija iš esmės tenka pačiam komponentui. Pasirenkate dmSingleContinuous išdėstymui, nustatote pfmFitWidth, kad stulpelis prisitaikytų prie lango dydžio, ir patikrinate Pdf.Active, kad sugadintas failas sukeltų klaidą. Vienintelė dalis, kurią verta parašyti patiems – atšaukiamas generavimas, nes skaitytuvas vertinamas pagal tai, kai vartotojas nutempia slinkties juostą į ilgo dokumento apačią, o skydelis arba spėja reaguoti, arba ne. Viskas, kas seka po to – teksto žymėjimas per kelis puslapius, paieškos rezultatų paryškinimas, žymių medis – yra sąsajos darbai, kurie atliekami ant šio slinkties paviršiaus, o ne jo viduje.
Šiame straipsnyje parodytos TPdfView, DisplayMode ir RenderPageProgressive API yra dalis PDFium VCL Component rinkinio, skirto „Delphi“ ir „Lazarus“.