Techninis straipsnis

Fono PDF atvaizdavimo eilė Delphi su HotPDF

HotPDF klasė THPDFBackgroundRenderer yra TThread palikuonė, kuri darbuotojo gijoje į bitmaps atvaizduoja įkeltus PDF puslapius, todėl Delphi peržiūros programa gali toliau slinkti ir perpiešti langą, kol puslapis fone vis dar rastruojamas. THPDFBackgroundRenderer.RequestPage į tą darbuotojo giją įtraukia puslapio indeksą, CancelAll pašalina viską, kas dar laukia, o GetCachedBitmap grąžina paruoštą bitmapą, kurį iškvietėjas valdo ir privalo atlaisvinti. Vien gijoje su vartotojo sąsaja spausdinimo raiška slinkite per dviejų šimtų puslapių nuskaitytą sutartį, ir kiekvienas puslapio pakeitimas sustabdys langą, kol GDI baigs jį piešti — būtent tokį trūkčiojimą ir pašalina THPDFBackgroundRenderer

Kam apskritai atvaizduoti PDF puslapius fono gijoje?

Fono gija pateisina savo sudėtingumą, nes HotPDF puslapių atvaizduoklis yra tikras turinio srauto interpretatorius, o ne pigi bitmapo kopija, kuri grįžta dar prieš kam nors pastebint: jis pereina PDF operatorius, saugo grafikos būsenų dėklą ir per GDI rastro pavidalu atvaizduoja kelius, vaizdus bei glifus, naudodamas tą patį variklį, kuris aprašytas puslapių atvaizdavime į TBitmap. Vykdykite šį darbą sinchroniškai slinkimo arba piešimo apdorojimo funkcijoje, ir pranešimų ciklas nustos apdoroti įvykius, kol iškvietimas grįš — būtent tai iš tikrųjų yra užstrigęs langas. Įterpus Application.ProcessMessages į atvaizdavimo iškvietimą problema neišsprendžiama: tai leidžia ištuštinti pranešimų eilę, tačiau pats atvaizdavimas vis dar užima iškvietėjo giją, todėl langas tik greičiau perpiešia pasenusį turinį, o tikras darbas niekur nepersikėlė. Vienintelis būdas išlaikyti peržiūros programą reaguojančią lėto atvaizdavimo metu — vykdyti tą atvaizdavimą kitur, todėl THPDFBackgroundRenderer yra TThread poklasis, o ne atgalinis iškvietimas ar laikmatis

Užklausų eilės nustatymas slenkančiai peržiūros programai

THPDFBackgroundRenderer.Create priima įkeltą THotPDF egzempliorių ir DPI reikšmę, kuri visą to atvaizduoklio gyvavimo laiką išlieka pastovi, todėl kiekvienas per vieną egzempliorių į eilę įtrauktas puslapis atvaizduojamas ta pačia raiška; mastelio keitimą palaikančiai peržiūros programai pakeitus mastelį reikia naujo atvaizduoklio, o ne naujos DPI savybės. RequestPage prideda puslapio indeksą į vidinę eilę ir iškart grįžta: jis pats neatvaizduoja ir niekada neliečia vartotojo sąsajos gijos. Execute, paveldėtas TThread įėjimo taškas, kurį HotPDF paleidžia iškvietus Start, vienu metu paima po vieną indeksą iš eilės pradžios, atvaizduoja jį per dokumento puslapių spartinančiąją atmintinę ir išsaugo kopiją pagal puslapį, kad GetCachedBitmap galėtų ją grąžinti vėliau

type
  TViewerForm = class(TForm)
    RenderPollTimer: TTimer;
    procedure RenderPollTimerTimer(Sender: TObject);
  private
    FDoc: THotPDF;
    FRenderer: THPDFBackgroundRenderer;
    FPendingPage: Integer;
    procedure RequestPageWindow(CenterPage: Integer);
  end;

procedure TViewerForm.RequestPageWindow(CenterPage: Integer);
var
  I: Integer;
begin
  if FRenderer <> nil then
  begin
    FRenderer.CancelAll;
    FRenderer.Free;
  end;
  FRenderer := THPDFBackgroundRenderer.Create(FDoc, 150);
  for I := CenterPage - 1 to CenterPage + 1 do
    if (I >= 0) and (I < FDoc.LoadedPageCount) then
      FRenderer.RequestPage(I);
  FPendingPage := CenterPage;
  FRenderer.Start;
end;

procedure TViewerForm.RenderPollTimerTimer(Sender: TObject);
var
  Bmp: TBitmap;
begin
  if FRenderer = nil then Exit;
  Bmp := FRenderer.GetCachedBitmap(FPendingPage);
  if Bmp <> nil then
  begin
    PageImage.Picture.Bitmap.Assign(Bmp);
    Bmp.Free;
  end;
end;

GetCachedBitmap grąžina nil, kol to puslapio kopija neparuošta, todėl aukščiau parodyto pavyzdžio tikrinimo laikmačiu pakanka; atskiro paruošimo įvykio prijungti nereikia, HotPDF tai išsprendžia paprastu nil tikrinimu, o ne didesne pranešimų API. Kitame skyriuje aprašoma, ką iš tikrųjų daro CancelAll ir tas Free iškvietimas, nes abu tampa svarbūs, kai puslapiai pradedami atvaizduoti ne eilės tvarka arba slenkama greičiau, nei eilė spėja ištuštėti

Vieno iškvietimo spartusis būdas vienam puslapiui

THotPDF.RenderLoadedPageToBitmapAsync skirtas dažnam atvejui, kai norima paleisti tiksliai vieno puslapio atvaizdavimą tiesiogiai neliečiant THPDFBackgroundRenderer: jis viduje sukuria atvaizduoklį, vieną kartą iškviečia RequestPage, paleidžia giją ir grąžina TThread nuorodą iškvietėjui, kuris ją valdo ir privalo atlaisvinti. Rezultatą reikia gauti per THotPDF.GetLoadedCachedRenderedBitmap, o ne per paties atvaizduoklio GetCachedBitmap, nes GetLoadedCachedRenderedBitmap skaito bendrą dokumento spartinančiąją atmintinę, indeksuojamą puslapio indeksu ir DPI, tą pačią, kurią jau užpildo RenderLoadedPageToBitmapCached ir integruotas išankstinio nuskaitymo mechanizmas — puslapis, kurį kita peržiūros programos dalis jau atvaizdavo tuo DPI, gali grįžti iškart, dar prieš operacinei sistemai suplanuojant ką tik paleistą fono giją

// A simpler alternative to the queue above, for one page at a time.
procedure TViewerForm.RequestSinglePage(PageIndex: Integer);
begin
  if FAsyncWorker <> nil then
    FAsyncWorker.Free; // waits if a prior page is still rendering
  FAsyncWorker := Pdf.RenderLoadedPageToBitmapAsync(PageIndex, 150);
  FPendingPage := PageIndex;
end;

procedure TViewerForm.AsyncPollTimerTimer(Sender: TObject);
var
  Bmp: TBitmap;
begin
  Bmp := Pdf.GetLoadedCachedRenderedBitmap(FPendingPage, 150);
  if Bmp <> nil then
  begin
    PageImage.Picture.Bitmap.Assign(Bmp);
    Bmp.Free;
  end;
end;

Ar galima atšaukti jau į eilę įtrauktą puslapį?

CancelAll pašalina tik tas užduotis, kurios dar laukia eilėje; puslapis, kurį HotPDF jau paėmė iš eilės pradžios ir perdavė atvaizdavimo iškvietimui, baigs darbą, nes THPDFBackgroundRenderer neturi mechanizmo jau vykdomam darbui nutraukti. Praktikoje tai pagrįstas kompromisas — vieno puslapio atvaizdavimas retai trunka pakankamai ilgai, kad dėl nutraukimo būtų verta pridėti sudėtingumo — tačiau greitas slinkimas, per kiekvieną slinkimo įvykį iškviečiantis CancelAll, vis tiek sumoka už tą vieną puslapį, kuris kiekvieno atšaukimo metu buvo atvaizduojamas. Oficialioje nuorodoje tai pasakyta tiesiai: jau vykdomas atvaizdavimas gali baigtis prieš gijos nutraukimą

Execute turi ir antrą, lengvai praleidžiamą elgseną: radęs tuščią eilę ciklas iškart baigiasi, jis nepasilieka laukdamas naujo darbo. Todėl THPDFBackgroundRenderer egzempliorius yra vienkartinis paketinis darbuotojas, o ne nuolatinė fono tarnyba — įtraukite į eilę kelis puslapius, iškvieskite Start, ir kai paskutinis į eilę įtrauktas puslapis bus atvaizduotas, pagrindinė operacinės sistemos gija baigsis pati. Dar kartą iškvietus RequestPage tame pačiame egzemplioriuje po to, kai Execute jau ištuštino eilę, jis nebus paleistas iš naujo, todėl RequestPageWindow aukščiau kiekvieno iškvietimo metu pakeičia atvaizduoklio egzempliorių, o ne bando nuolat maitinti vieną ilgai gyvuojantį objektą

Ar Delphi saugu iš fono gijos liesti TBitmap?

HotPDF architektūroje liesti TBitmap iš fono gijos saugu, jei tik vienu metu konkretų bitmapo egzempliorių valdo viena gija, o THPDFBackgroundRenderer šią ribą užtikrina užuot palikęs ją iškvietėjui. Execute kiekvieną puslapį atvaizduoja dokumento nuosavoje atvaizdavimo užrakinimo srityje, toje pačioje kritinėje sekcijoje, kurią jau bendrai naudoja kiekvienas RenderLoadedPageToBitmapCached iškvietimas ir integruotas PrefetchLoadedPages išankstinio nuskaitymo mechanizmas, todėl tikrasis konkretaus puslapio GDI piešimas vienu metu vyksta tiksliai vienoje gijoje ir niekada nesutampa su kitu to dokumento atvaizdavimu. Gautas bitmapas yra darbuotojo gijos valdomas objektas, kurio THPDFBackgroundRenderer niekada tiesiogiai nepaskelbia iškvietėjui

Vietoj to GetCachedBitmap sukuria visiškai naują TBitmap ir, laikydamas atskirą atvaizduoklio užraktą, iškviečia jo Assign, todėl kopijavimas visada vyksta tuo metu, kai Execute negali pakeisti tos spartinančiosios atmintinės vietos — iškvietėjo gija gauna pikselių duomenis, o ne pradinę rankenėlę. Dėl šio atskyrimo taip pat verta vengti pasirinktinės atvaizdavimo gijos, kuri tiesiogiai iškviečia HotPDF atvaizdavimo funkcijas apeidama THPDFBackgroundRenderer arba PrefetchLoadedPages: dviem atvaizdavimams lenktyniaujant dėl to paties įkelto dokumento bendrų spartinančiųjų atminčių ir objektų grafo susidaro būtent ta situacija, kurios HotPDF vidinis užrakinimas siekia išvengti, o fono atvaizduoklio klasė suteikia šį užrakinimą nemokamai, todėl nereikia jo įgyvendinti iš naujo

Kuo tai skiriasi nuo HotPDF integruoto puslapių išankstinio nuskaitymo?

PrefetchLoadedPages ir THPDFBackgroundRenderer sprendžia susijusias, bet skirtingas problemas: PrefetchLoadedPages, gavęs puslapių intervalą, automatiškai atvaizduoja visą tą aplinką į bendrą dokumento spartinančiąją atmintinę savo darbuotojo gijoje, o iškvietėjui nereikia kurti ar valdyti jokio eilės objekto. THPDFBackgroundRenderer iškeičia šį automatizavimą į valdymą — iškvietėjas tiksliai nusprendžia, kurie puslapių indeksai svarbūs ir kokia tvarka, taip pat gali atšaukti dar eilėje laukiančius puslapius, neliesdamas intervalo, kurį kitur šildo integruotas išankstinio nuskaitymo mechanizmas. Abu naudoja tą patį atvaizdavimo užraktą, todėl peržiūros programa gali naudoti PrefetchLoadedPages įprastam kelių tolesnių puslapių atvejui ir rinktis THPDFBackgroundRenderer tik tada, kai prireikia ko nors už šio modelio ribų, pavyzdžiui, miniatiūrų juostai iškart pereinant į puslapį, kurį ką tik spustelėjo naudotojas

begin
  // PrefetchLoadedPages takes a 1-based "start-end" range string, while
  // RequestPage below stays 0-based like every other loaded-page index.
  Pdf.PrefetchLoadedPages(Format('%d-%d', [CenterPage + 1, CenterPage + 5]), 150);

  // Reach for THPDFBackgroundRenderer only for a page outside that
  // window, such as a thumbnail the user just clicked.
  FRenderer := THPDFBackgroundRenderer.Create(Pdf, 150);
  FRenderer.RequestPage(ClickedThumbnailPage);
  FRenderer.Start;
end;

Į gamybinį kodą verta perkelti dvi gyvavimo ciklo detales. Dokumento masto spartinančioji atmintinė, naudojama RenderLoadedPageToBitmapCached, ribojama RenderCacheCapacity ir pagal numatytuosius nustatymus talpina aštuonis puslapius, o užsipildžiusi pašalina mažiausiai neseniai naudotą įrašą, tačiau paties THPDFBackgroundRenderer egzemplioriaus rezultatų sąrašas neturi tokio apribojimo — jame iki paties egzemplioriaus atlaisvinimo lieka po vieną bitmapą kiekvienam skirtingam puslapio indeksui, kada nors įtrauktam per tą egzempliorių, todėl visą slinkimo seansą aukšta raiška gyvas atvaizduoklis lengvai sukaups po visos raiškos bitmapą kiekvienam praeitam puslapiui. HotPDF taip pat automatiškai neatšaukia iškvietėjo sukurto atvaizduoklio taip, kaip atšaukia savo išankstinio nuskaitymo mechanizmą prieš įkeliant dokumentą arba jį sunaikinant, nes THPDFBackgroundRenderer egzempliorius niekada neužregistruojamas THotPDF objekte, į kurį rodo — todėl iškvietimo kodas turi atšaukti ir atlaisvinti kiekvieną su dokumentu susietą atvaizduoklį prieš iš naujo įkeldamas arba atlaisvindamas tą dokumentą, laikydamasis tos pačios tvarkos, kurią HotPDF viduje taiko PrefetchLoadedPages

THPDFBackgroundRenderer yra viena iš įkelto dokumento fasado dalių HotPDF MVC peržiūros programos architektūroje, ir jis natūraliai dera su failo lygio darbo eigomis, aprašytomis Direct File API dideliems PDF, kai slenkamas dokumentas pats yra per didelis, kad jį būtų galima neapgalvotai įkelti nuo pat pradžių. Fono atvaizdavimas, užklausų eilės ir čia aprašyta atvaizdavimo spartinančioji atmintinė yra standartinio HotPDF komponento, skirto Delphi ir C++Builder, dalis