Tehnični članek

Odpovedljivo postopno upodabljanje PDF v Delphiju (PDFium)

Sinhrono upodabljanje blokirа nit za svojo celotno trajanje. Za preprosto stran to ni opazno. Za kompleksno stran z gradientnimi senčili, maskami prosojna ali visoko ločljivostjo je trajanje upodabljanja merljivo in je od perspektive UI nerazlikljivo od zamrznjene aplikacije. Dve rešitvi obstajata: premaknite upodabljanje v nit ozadja ali posnemite vmesne točke za ustavitev in nadzirajte trajanje iz zanke, ki teče tam, kjer teče upodabljanje. PDFium ponuja mehanizem za slednje prek strukture IFSDK_PAUSE, in vezava PDFium VCL ga izpostavlja prek IPdfCancellationToken

Postopni API upodabljanja, ki ga PDFium že dostavi

PDFium razkrije tri funkcije za postopno upodabljanje: FPDF_RenderPageBitmap_Start, FPDF_RenderPageBitmap_Continue in FPDF_RenderPageBitmap_Close. Start inicializira kontekst upodabljanja in začne upodabljati, dokler ne prispe do vmesne točke ali zaključi. Vrne eno od treh vrednosti: FPDF_RENDER_DONE (zaključeno), FPDF_RENDER_TOBECONTINUED (delno, znova pokliči) ali FPDF_RENDER_FAILED. Continue nadaljuje od zadnje vmesne točke in vrne iste tri vrednosti. Close sprosti kontekst upodabljanja ne glede na to, ali je upodabljanje zaključeno ali ne

Vmesne točke so kontrolirane s kazalcem na strukturo IFSDK_PAUSE, ki ga posredujete Start in Continue. Struktura vsebuje en sam povratni klic -- NeedToPauseNow -- ki ga PDFium kliče periodično med upodabljanjem. Ko ta povratni klic vrne neničlo vrednost, PDFium shrani stanje upodabljanja in vrne FPDF_RENDER_TOBECONTINUED. Ko vrne nič, upodabljanje nadaljuje brez prekinitve

// The pause structure, as the PDFium C header defines it
type
  IFSDK_PAUSE = record
    version: Integer;
    NeedToPauseNow: function(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
    user: Pointer;   // caller-defined context; passed back on each callback
  end;

Prenamembanje pavze kot preklica

Privzeto NeedToPauseNow vrne neničlo vrednost po fiksnem intervalu časa, tako da upodabljanje naredi napredek v majhnih korakih in klic Continue teče postopoma v zanki. Da bi to spremenili v odpovedljivo upodabljanje, zamenjate logiko vmesne točke: namesto merjenja časa preverite odpovedni žeton. Ko je žeton nastavljen, NeedToPauseNow vrne neničlo vrednost pri vsakem klicu. PDFium zaznava premik prekinitve vmesne točke, shrani stanje in vrne FPDF_RENDER_TOBECONTINUED. Zanka ga dobita in se zaključi

function NeedToPauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
  Token: IPdfCancellationToken;
begin
  // pThis^.user holds the cancellation token interface reference
  Token := IPdfCancellationToken(pThis^.user);
  if Token.IsCancelled then
    Result := 1   // non-zero: pause immediately
  else
    Result := 0;  // zero: keep rendering
end;

Ohranjanje žetona živega čez zanko

Kazalec user v strukturi IFSDK_PAUSE je surov kazalec. Delphi ne doda štetja referenc na vmesnik, shranjen prek surovega kazalca, kar pomeni, da mora biti vmesnik žeton ohranjen živ prek drugega mehanizma, dokler je struktura IFSDK_PAUSE v uporabi. Pravilna oblika je hraniti vmesnik v lokalni spremenljivki, ki živi na klicnem skladu za čas trajanja zanke upodabljanja. Ko se zanka zaključi, se vmesnik sprosti po naravni poti

procedure RenderWithCancellation(
  Page: FPDF_PAGE; Bitmap: FPDF_BITMAP;
  Token: IPdfCancellationToken);
var
  Pause: IFSDK_PAUSE;
  Status: Integer;
begin
  Pause.version := 1;
  Pause.NeedToPauseNow := @NeedToPauseCallback;
  Pause.user := Pointer(Token);  // raw pointer; Token kept alive by local var above

  Status := FPDF_RenderPageBitmap_Start(Bitmap, Page, 0, 0,
    BitmapWidth, BitmapHeight, 0, FPDF_ANNOT, @Pause);

  while Status = FPDF_RENDER_TOBECONTINUED do
  begin
    if Token.IsCancelled then Break;
    Status := FPDF_RenderPageBitmap_Continue(Bitmap, Page, @Pause);
  end;

  FPDF_RenderPageBitmap_Close(Page);
end;

Zapiranje konteksta upodabljanja ne glede na to, kako se zanka konča

Klic FPDF_RenderPageBitmap_Close mora priti ne glede na to, ali je upodabljanje zaključeno ali preklicano. PDFium ohrani notranje stanje za trajanje konteksta postopnega upodabljanja in ne sprosti tega stanja, dokler ni Close poklican. Izpustitev klica Close pušča puščanje v PDFiumovem rezervatniku pomnilnika, ki se kopiči z vsakim preklicanjem. Vzorec try/finally je priporočena oblika

Status := FPDF_RenderPageBitmap_Start(...);
try
  while Status = FPDF_RENDER_TOBECONTINUED do
  begin
    if Token.IsCancelled then Break;
    Status := FPDF_RenderPageBitmap_Continue(Bitmap, Page, @Pause);
  end;
finally
  FPDF_RenderPageBitmap_Close(Page);  // always runs
end;

Tri izide in kaj bitna slika drži po preklicu

Zanka upodabljanja se konča z enim od treh izidov. Prvič, Status = FPDF_RENDER_DONE: upodabljanje je zaključeno in bitna slika je polno upodobljena. Drugič, Token.IsCancelled prekine zanko pred FPDF_RENDER_DONE: bitna slika drži delno upodabljanje -- kar koli je PDFium narisal do zadnje vmesne točke je v bitmapi, preostanek pa je pobarvan z barvo ozadja ali prejšnjo vsebino, odvisno od tega, kako je bila bitna slika inicializirana. Tretjič, Status = FPDF_RENDER_FAILED: upodabljanje je naletelo na notranjo napako in bitna slika morda drži nič ali delno vsebino

Preklicana bitna slika ni nujno neuporabna. Ker PDFium upodablja v naravnem vrstnem redu operaterjev toka vsebine in so operaterji pogosto razporejeni od ozadja do ospredja, je delno upodabljanje pogosto prepoznavno vsebino strani. Aplikacija se lahko odloči prikazati delno bitno sliko med čakanjem na zamenjavo ali jo takoj zavrže

Ničelni žeton in pot povratnega klica brez veje

Ko je odpovedni žeton neobvezna lastnost -- to je, ko klicatelj ne poda žetona -- je mehanizem povratnega klica manjši stres, če je mogoče brez veje. Eden od načinov je privzet ničelni žeton, ki vedno vrne False za IsCancelled. Ko je ta ničelni žeton priložen povratnemu klicu NeedToPauseNow, zanka upodabljanja deluje normalno, nikoli ne zaustavi zgodaj in kontekst IFSDK_PAUSE deluje povsem normalno. Klicatelj, ki ne poda žetona, dobi sinhrono vedenje brez posebnih poti v kodi upodabljanja

// A nil token always returns False for IsCancelled
type
  TNullCancellationToken = class(TInterfacedObject, IPdfCancellationToken)
  public
    function IsCancelled: Boolean;
  end;

function TNullCancellationToken.IsCancelled: Boolean;
begin
  Result := False;
end;

Ta vzorec ohranja logiko upodabljanja enotno ne glede na to, ali je odpovednost vklopljena ali ne. Ni blokov if Assigned(Token), razpršenih po zanki upodabljanja; ničelni objekt jih naredi odvečne. Postopno upodabljanje PDFium in odpovedni žeton sta del komponente PDFium VCL za Delphi in C++Builder