Technical Article

PDFium Delphi peržiūros programa: atvaizdavimo talpykla ir sklandus mastelio keitimas

Laikykite nuspaudę mastelio keitimo mygtuką paprastoje PDF peržiūros programoje ir stebėkite procesoriaus (CPU) apkrovos grafiką. Vienas automatiškai besikartojantis mygtuko paspaudimas sukelia dešimt ar daugiau mastelio keitimo žingsnių per sekundę. Jei kiekvienas žingsnis paleidžia pilnos kokybės matomo puslapio pakartotinį atvaizdavimą, užklausos kaupiasi greičiau nei yra įvykdomos. Atskirai puslapis atvaizduojamas gerai, pavyzdžiui, A4 formato skenuotas lapas trunka 180 ms, tačiau dabar jūs vykdote tuziną 180 ms trukmės atvaizdavimų darbams, kuriuos vartotojas jau praleido. Peržiūros programa užstringa, vienas procesoriaus branduolys apkraunamas 100%, o kol ekranas spėja atsinaujinti, vartotojas jau sustojo ties mastelio lygiu, buvusiu prieš keturis atvaizdavimo žingsnius. Semicinas yra ne greitesnis rasterizatorius, o talpykla, kuri akimirksniu grąžina paruoštus puslapius, ir atvaizdavimo ciklas, galintis nutraukti darbą, kai tik jis praranda aktualumą.

„PDFium Component“ suteikia visas reikalingas dalis abiem sprendimams, tačiau nesikiša į pačią programos politiką. Gaunate iškvietėjo valdomus taškinius paveikslėlius, progresyvųjį atvaizdavimą su nutraukimo žetonu, pritaikymo režimus, kurie perskaičiuoja mastelį keičiant lango dydį, ir plytelių (tiling) iškvietimą puslapiams, kurie yra per dideli, kad būtų atvaizduoti iškart. Ko komponentas sąmoningai nepateikia – tai pačios talpyklos, nes teisinga įrašų šalinimo politika prikrauso nuo jūsų ekrano srities (viewport), jūsų platformos atminties ribų ir to, kaip jūsų vartotojai slenka puslapius. Šį sprendimą turite priimti patys, o klaidų pasekmės yra būtent programos užstrigimas ir atminties nutekėjimas.

Kur dingsta milisekundės ir megabaitai

Prieš pradėdami kurti sistemą, įvertinkite sąnaudas skaičiais. A4 formato puslapis esant 96 DPI yra maždaug 794 x 1123 pikselių, o tai sudaro apie 3.5 MB kaip 32 bitų taškinis paveikslėlis. Padidinkite mastelį iki 200%, ir šis dydis padidės keturis kartus. Esant 400% masteliui didelės raiškos (high-DPI) ekrane, vieno puslapio taškiniam paveikslėliui skiriate ir užpildote 50–60 MB, o nenutrūkstamo slinkimo peržiūros programa vienu metu palaiko aktyvius kelis puslapius. Rasterizacijos sąnaudos priklauso nuo išvesties pikselių, todėl kiekvienas mastelio padvigubinimas maždaug keturis kartus padidina atvaizdavimo laiką ir atminties suvartojimą.

Iš šios aritmetikos išplaukia dvi pasekmės. Talpykla, kurios raktas neatsižvelgia į mastelio lygį, yra bevertė, nes būtent tam veiksmui, kurį ji turi pagreitinti – mastelio keitimui – kiekvieną kartą reikia sukurti naują taškinį paveikslėlį. Be to, neribojama talpykla greitai išeikvos 32 bitų proceso adresų erdvę būtent tais atvejais, kai vartotojai intensyviai didina vaizdą: tankiuose nuosavybės dokumentų skenuose, inžineriniuose brėžiniuose ar didelio formato žemėlapiuose. Talpyklos raktai turi būti sudaryti teisingai, o jos dydis – griežtai ribojamas; abu šie reikalavimai yra privalomi.

Kas priklauso talpyklos raktui

Talpykloje saugomą taškinį paveikslėlį saugu naudoti tik tada, kai sutampa visi parametrai, nulėmę jo taškus. Tai reiškia puslapio numerį, faktinį mastelį (arba išvesties pikselių matmenis), pasukimą, monitoriaus DPI ir atvaizdavimo parinktis, kurios galiojo jį sukuriant. Puslapis, atvaizduotas su reAnnotations, yra kitoks vaizdas nei tas pats puslapis be jų, o nespalvotas atvaizdavimas per reGrayscale – dar kitoks. Pašalinkite bent vieną iš šių parametrų iš rakto, ir klaidos bus dėsningos: anotacijų perdanga lieka kaboti peržiūros dalyviui ištrynus komentarą arba puslapis tampa neryškus, kai tik vartotojas perkelia langą iš nešiojamojo kompiuterio ekrano į išorinį 4K monitorių, o DPI pasikeičia po pasenusiu taškiniu paveikslėliu.

function TPageCache.Acquire(Pdf: TPdf; PageNo: Integer; ZoomPct: Single;
  Rotation: TRotation; Opts: TRenderOptions): TBitmap;
var
  Key: string;
begin
  Key := Format('%d|%.0f|%d|%d|%d',
    [PageNo, ZoomPct, Ord(Rotation), Screen.PixelsPerInch, OptionsMask(Opts)]);
  if FBitmaps.TryGetValue(Key, Result) then
    Exit;

  Pdf.PageNumber := PageNo;
  Result := Pdf.RenderPage(0, 0, OutputWidth(PageNo, ZoomPct),
    OutputHeight(PageNo, ZoomPct), Rotation, Opts);
  FBitmaps.Add(Key, Result);   // the cache now owns this bitmap
end;

Talpyklos pataikymo atveju grąžinimas trunka mikrosekundes, ir tai yra visa esmė. Sudėtingesnis klausimas – kas nutinka su taškiniais paveikslėliais, kurie pašalinami iš talpyklos, o tai galiausiai yra klausimas apie nuosavybę.

Kas atlaisvina taškinį paveikslėlį

Funkcinė RenderPage forma grąžina TBitmap, kurį valdo iškvietėjas. Vieno etapo eksporto atveju ši nuosavybė yra aiški ir lengvai valdoma. Tačiau talpykloje tai tampa viena dažniausių atminties nutekėjimo priežasčių Delphi PDF peržiūros programose, nes žodynas (TDictionary) dabar saugo vienintelę nuorodą į kiekvieną taškinį paveikslėlį, o paprastas TDictionary atlaisvina raktus ir reikšmes tik tada, jei jie yra valdomi (managed) tipai. TBitmap toks nėra. Pašalinkite įrašą neiškviesdami Free, ir taškai liks paskirti atmintyje, nors jokia nuoroda į juos neberodys.

Ši klaida dažnai praslysta pro akis dėl laiko faktoriaus. Dešimties minučių paviršutiniškas testas niekada neapima tiek daug skirtingų puslapių mastelio keitimų, kad tai pastebėtumėte; nutekėjimas išryškėja tik tada, kai kas nors porą valandų slenka ir didina ilgą dokumentą. Tuo momentu procesas laiko šimtus našlaičiais likusių puslapio taškinių paveikslėlių, o sistema pradeda naudoti apsikeitimo failą (page file). Todėl įrašų pašalinimas turi būti realizuotas pirmoje talpyklos versijoje, o ne vėliau. Ribokite talpyklą pagal numatomus baitus (apskaičiuojamus kaip plotis padaugintas iš aukščio ir iš keturių), pašalinkite mažiausiai naudotus puslapius, kurie yra už matomos srities bei išankstinio įkėlimo (prefetch) ribų, ir atlaisvinkite kiekvieną pašalinamą taškinį paveikslėlį. Laikinam piešimui skirtos perkrovos, kurios piešia į iškvietėjo pateiktą TBitmap arba tiesiai į HDC, leidžia visiškai išvengti šios nuosavybės valdymo užduoties. Spaudinio peržiūra yra akivaizdus pavyzdys – atvaizduojate kiekvieną lapą vieną kartą, ir talpinimas talpykloje neduoda jokios naudos.

Progresyvusis atvaizdavimas ir sąžiningas nutraukimas

Paprastos RenderPage perkrovos blokuoja programos vykdymą, kol puslapis yra visiškai baigiamas – tai yra būtent toks elgesys, kurio nenorite, kol vartotojas vis dar keičia mastelį. Šiam tikslui pasiekti naudokite RenderPageProgressive. Ji priima IPdfCancellationToken ir grąžina vieną iš reikšmių: prsDone, prsCancelled arba prsFailed. Elgsenos detalė, kurios programuotojai dažnai nepastebi – nutraukimas nėra akimirksnis. Žetono būsena tikrinama atvaizdavimo fragmentų (chunks) ribose, zodėl žetonas, kurį pažymite fragmento viduryje, suveikia tik tada, kai tas fragmentas baigiamas. Sudėtingame puslapyje laiko tarpas tarp prašymo ir sustojimo gali siekti kelias dešimtis milisekundžių. Projektuokite programą atsižvelgdami į šį skirtumą: nutraukite ankstesnį žetoną tą pačią akimirką, kai gaunama nauja mastelio reikšmė, bet nemanykite, kad senasis atvaizdavimas sustoja tą pačią sekundę, kai to paprašote.

procedure TViewerForm.RequestRender(TargetZoom: Single);
var
  Status: TPdfProgressiveStatus;
begin
  if FTokenSource <> nil then
    FTokenSource.Cancel;           // abandon the previous in-flight render
  FTokenSource := TPdfCancellationTokenSource.New;  // FPdfAsync unit

  Status := Pdf.RenderPageProgressive(FBackBuffer, 0, 0,
    FBackBuffer.Width, FBackBuffer.Height, FTokenSource.Token,
    ro0, [reAnnotations]);

  case Status of
    prsDone:      PresentBackBuffer;
    prsCancelled: ;                // superseded by a newer request: drop silently
    prsFailed:    ShowRenderFailure;
  end;
end;

Sąveikos metu prsCancelled yra normalus rezultatas, o ne išskirtinis atvejis. Dauguma atvaizdavimų, kuriuos paleidžia mastelio keitimas, bus pakeisti naujesniais prieš jiems pasibaigiant, todėl traktuokite nutraukimą kaip įprastą eigą ir tyliai atmeskite rezultatą. Atvaizdavimo eilė, kuri registruoja kiekvieną nutraukimą kaip įspėjimą, tiesiog palaidos vienintelę svarbią klaidą po tūkstančiais žurnalo eilučių triukšmo. Kad ekranas neatrodytų negyvas, kol veikia tikrasis atvaizdavimas, suderinkite progresyvųjį kelią su paprastu pakaitalu: padidinkite ankstesnį talpyklos taškinį paveikslėlį iki naujo mastelio ir parodykite jį iškart. Jis atrodys neryškus šimtą ar dvi milisekundes, tačiau bus suvokiamas kaip akimirksnis ir suteiks pilnos kokybės atvaizdavimui laiko užbaigti darbą arba būti nutrauktam kito veiksmo.

Pritaikymo režimas, kurį mastelio keitimas tyliai išjungia

Peržiūros programos savybė FitMode, nustatyta į pfmFitPage arba pfmFitWidth, perskaičiuoja mastelį per kiekvieną lango dydžio keitimą, kad puslapis tilptų jam kintant. Tačiau tiesioginis Zoom priskyrimas sugrąžina FitMode į pfmNone. Kaip numatytasis elgesys tai yra teisinga: vartotojas, kuris sąmoningai įvedė 150%, nenori, kad kitas lango dydžio keitimas šį nustatymą panaikintų. Tačiau tai nustebina kiekvieną, kuris susieja mastelio didinimo mygtuką kaip Zoom := Zoom * 1.25 ir vėliau negali suprasti, kodėl pritaikymas pagal plotį nustojo veikti po pirmo paspaudimo. Jei jūsų įrankių juosta siūlo tiek tiesioginį mastelio keitimą, tiek pritaikymo režimus, turite patys įsiminti paskutinį vartotojo pritaikymo pasirinkimą ir vėl jį priskirti, kai vartotojas paspaudžia pritaikymo mygtuką. Komponentas neatkurs režimo, kurį ką tik išvalė mastelio priskyrimas, ir jis neturėtų to daryti.

Atminties biudžetas, kurį galite apginti

Biudžetas, kurį galite aiškiai aprašyti, yra biudžetas, kurį galite apginti kodo peržiūros metu, todėl pradėkite nuo konkretaus scenarijaus. Tarkime, nenutrūkstamas slinkimas išlaiko matomą puslapį bei po vieną iš anksto įkeltą puslapį viršuje ir apačioje bei miniatiūrų juostą. Esant 100% masteliui 96 DPI ekrane, šie trys pilno dydžio taškiniai paveikslėliai sudaro maždaug po 3.5 MB kiekvienas – tai yra smulkmena. Esant 300% 4K ekrane tie patys trys paveikslėliai jau užima apie 30 MB kiekvienas, ir tai dar prieš talpyklai išsaugant bet kokį istorinį puslapį. Šis augimas priklauso nuo vartotojo veiksmų, o ne nuo paties dokumento.

32 bitų Delphi procesui protingas numatytasis nustatymas yra 256 MB taškinių paveikslėlių biudžetas naudojant LRU šalinimo principą. 64 bitų sistemoje galite didinti šį dydį priklausomai nuo fizinės RAM, tačiau bet kokiu atveju nustatykite griežtą viršutinę ribą, nes gedimas, nuo kurio saugotės, nėra jūsų proceso avarinis išsijungimas. Tai visos sistemos sulėtėjimas naudojant apsikeitimo failą, kol jūsų peržiūros programa techniškai veikia, o vartotojas nesupranta, kodėl viskas sulėtėjo. Griežtas ribojimas sugenda nuspėjamai; neribojama talpykla sugenda sužlugdydama visą operacinę sistemą. Miniatiūros reikalauja atskiro tvarkymo: atvaizduokite kiekvieną iš jų vieną kartą jos mažu tiksliniu dydžiu ir laikykite atskirame rinkinyje, kurio LRU logika neliečia. Regeneruoti 120 pikselių miniatiūrą masteliu sumažinant 60 MB pilno puslapio taškinį paveikslėlį yra pats neefektyviausias būdas sukurti pašto ženklo dydžio paveikslėlį.

Kai kurie atskiri puslapiai gali viršyti bet kokį biudžetą. Didelio formato inžinerinis brėžinys arba didelis žemėlapis, visiškai atvaizduotas esant 400% masteliui, reikalauja kelių šimtų megabaitų, ir jokia šalinimo politika nepadarys to priimtino. Atsakymas tokiu atveju – nustoti atvaizduoti pilnus puslapius. RenderTile rasterizuoja tik tą sritį, kurios pikselių poslinkis yra (Left, Top) puslapyje, kuris sąlygiškai padidintas iki PageWidth ir PageHeight. Taip atvaizduojate tik matomą stačiakampį ir vienos plytelės atsargą aplink jį sklandžiam stumdymui, o plytelių poslinkius įtraukiate į talpyklos raktą kartu su masteliu. Išlaikykite plytelių matmenis fiksuotus visame dokumente. Fiksuotos plytelės užtikrina, kad DPI pasikeitimas švariai anuliuoja visą tinklelį, o kintamos plytelės verčia kovoti su matomomis siūlėmis tarp sričių, atvaizduotų šiek tiek skirtingais masteliais.

Dvi gretutinės funkcijos tyliai prisideda prie viso to. Spalvų filtrų etapai, tokie kaip nespalvotas vaizdas arba inversija, veikia po atvaizdavimo ir kiekvieną kartą sukuria antrą pilno dydžio taškinį paveikslėlį, padvigubindami bet kurio juos naudojančio rodinio puslapio pėdsaką; šios sąnaudos nagrinėjamos straipsnyje apie silpnaregių spalvų filtravimą Delphi PDF peržiūros programose. Be to, peržiūros programa, kuri paryškina žodžius teksto pavertimo kalba metu, anuliuoja sugeneruotą vaizdą su kiekvienu ištartu žodžiu, todėl paryškinimo perpiešimo ir kalbos greičio sąveika yra svarbesnė nei atrodo iš pirmo žvilgsnio, kaip aprašyta straipsnyje apie žodžių paryškinimą teksto pavertimo kalba (TTS) metu.

Atvaizdavimo perkrovos, progresyvios būsenos kodai ir pats peržiūros komponentas yra aprašyti produkto puslapyje: „PDFium Component“.