Odborný článok

Zrušiteľné progresívne vykresľovanie PDF v Delphi (PDFium)

Väčšina stránok PDF sa rasterizuje za niekoľko milisekúnd a nikdy na to nepomyslíte. Potom používateľ otvorí inžiniersky výkres formátu A1, stránku plnú desiatok tisíc vektorových ťahov alebo plagát preplnený skupinami priehľadnosti a jemnými maskami, a jediné volanie, ktoré to vykresľuje, trvá dve alebo tri sekundy. Ak sa toto volanie spustí vo vlákne používateľského rozhrania, okno sa prestane prekresľovať, záhlavie zošedne a operačný systém ponúkne ukončenie aplikácie. Táto práca je oprávnená. Stránka skutočne potrebuje taký dlhý čas. Chybou je, že vykresľovanie je jedno nedeliteľné blokujúce volanie bez možnosti nadýchnuť sa a bez možnosti zastaviť ho

Tento článok je presne o jednom z týchto dvoch problémov: o zrušení dlhého vykresľovania jednej stránky bez zamrznutia používateľského rozhrania. Používateľ klikol na ďalšiu stránku, priblížil ju alebo zatvoril dokument, a prebiehajúce vykresľovanie je teraz zbytočnou prácou, ktorá by sa mala skončiť pri najbližšej príležitosti namiesto toho, aby prebehla do konca. Vyhladenie posúvania a priblíženia ukladaním už zrasterizovaných častí do vyrovnávacej pamäte je samostatným problémom s vlastným dizajnom, ktorému sa venuje sprievodný článok v odkaze na konci. Tu je jedinou otázkou to, ako zabezpečiť, aby jedno progresívne vykresľovanie rýchlo a čisto odpovedalo na požiadavku o zrušenie

API pre progresívne vykresľovanie, ktoré PDFium už obsahuje

Knižnica PDFium predvídala tú polovicu problému, ktorá sa týka zamrznutia. Popri jednorazovom volaní FPDF_RenderPageBitmap ponúka aj progresívny variant, ktorý stránku rozdeľuje na menšie pracovné časti. Funkciu FPDF_RenderPageBitmap_Start zavoláte len raz pre nastavenie vykresľovania do cieľovej bitovej mapy a následne opakovane voláte FPDF_RenderPage_Continue. Každé volanie Continue zrasterizuje ohraničený kúsok a vráti stav. FPDF_RENDER_TOBECONTINUED znamená, že ostáva vykonať ďalšiu prácu, FPDF_RENDER_DONE znamená, že stránka je dokončená a FPDF_RENDER_FAILED indikuje, že proces sa zastavil pre chybu. Po skončení slučky zavoláte FPDF_RenderPage_Close na uvoľnenie stavu progresívneho vykresľovania konkrétnej stránky. Keďže sa riadenie medzi jednotlivými časťami vracia do vášho kódu, môžete spracovávať správy z frontu, aktualizovať indikátor priebehu alebo overiť, či je práca stále potrebná

Mechanizmus, ktorý PDFium poskytuje pre rozhodnutie, kedy prácu prerušiť, je štruktúra spätného volania s názvom IFSDK_PAUSE. Odovzdáte ju volaniu Start a každému volaniu Continue. Po každom kúsku PDFium zavolá svoj ukazovateľ na funkciu NeedToPauseNow, a ak vráti nenulovú hodnotu, aktuálne volanie Continue sa predčasne zastaví a vráti riadenie so stavom FPDF_RENDER_TOBECONTINUED. Táto štruktúra taktiež obsahuje pole version, ktoré musí byť nastavené na 1, a voľný ukazovateľ user, ktorého sa PDFium nikdy nedotkne a odovzdáva ho bezo zmeny. Tento nedotknutý ukazovateľ je celým základom nasledujúceho dizajnu

Využitie pozastavenia ako zrušenia

Pôvodným zámerom NeedToPauseNow je zdieľanie času (time-slicing). Ak vyčerpáte svoj rozpočet na snímku, vráťte nenulovú hodnotu; ak chcete vo vykresľovaní pokračovať, vráťte nulu. PDFium sa pozastaví, aby ste mohli vykonať niečo iné predtým, než budete v tom istom vykresľovaní pokračovať. Komponent PDFium opätovne využíva ten istý signál na odlišný účel. Namiesto odpovede na otázku "mám to pozastaviť, aby ste mohli pokračovať", spätné volanie odpovedá na "bola táto práca zrušená". Obe situácie sa do seba čisto mapujú vďaka tomu, čo urobí slučka, keď tento príznak uvidí. Skutočné pozastavenie očakáva neskoršie volanie Continue; zrušenie ho neočakáva. Akonáhle volajúca slučka zistí, že token je zrušený, zatvorí kontext vykresľovania a už nikdy Continue nezavolá, takže rovnaká nenulová návratová hodnota, ktorú PDFium číta ako "zastav tento kúsok", sa v skutočnosti zmení na "zastav definitívne"

Zrušenie je vyjadrené prostredníctvom rozhrania IPdfCancellationToken, ktorého vlastnosť IsCancelled sa preklopí z hodnoty false (nepravda) na true (pravda), keď iná časť programu požiada o zastavenie vykresľovania. Mostom medzi týmto rozhraním Pascalu a spätným volaním v jazyku C knižnice PDFium je jediný ukazovateľ. Odkaz na rozhranie tohto tokenu sa zapíše do premennej IFSDK_PAUSE.user a statické spätné volanie s konvenciou cdecl si ho z neho opäť načíta a dotazuje ho. Toto je klasický problém umožnenia knižnici v jazyku C volať kód v Pascale: spätné volanie musí byť obyčajnou funkciou s volacou konvenciou jazyka C, nie metódou, pretože PDFium uchováva a volá holý ukazovateľ na funkciu, ktorý nevie nič o objektoch Pascalu alebo o odkaze Self

type
  TPdfProgressivePause = record
    Pause: IFSDK_PAUSE;            // PDFium reads this; .user holds the token
    Token: IPdfCancellationToken; // strong ref keeps the token alive
  end;

function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
  Token: IPdfCancellationToken;
begin
  Result := 0;
  if (pThis = nil) or (pThis^.user = nil) then
    Exit;
  Token := IPdfCancellationToken(pThis^.user);
  if Token.IsCancelled then
    Result := 1; // non-zero: PDFium stops this chunk
end;

Spätné volanie obnoví token pretypovaním pThis^.user späť na typ rozhrania a prečíta si IsCancelled. Nič v ňom nealokuje pamäť, nezamyká ani neblokuje, čo je dôležité, pretože PDFium ho volá vo vykresľovacom vlákne po každom kúsku a akákoľvek vykonaná práca tu pridáva náklady na samotné vykresľovanie. Ochrana pred nulovou (nil) štruktúrou alebo poľom user nastaveným na nil znamená, že rovnakú funkciu je bezpečné nainštalovať dokonca aj pri vykresľovaní, ktorému nikdy nebol odovzdaný skutočný token

Udržanie tokenu nažive počas trvania slučky

Pretypovanie ukazovateľa rozhrania cez čistý Pointer (ukazovateľ) a späť je miestom, kde sa rodia chyby s dĺžkou životnosti. Typ IInterface v Delphi má počítané referencie a počet sa mení len vtedy, keď kompilátor vidí priraďovanie do premennej typu rozhranie (interface). Uloženie tokenu výhradne ako holého ukazovateľa v premennej IFSDK_PAUSE.user by ho úplne skrylo pred počítadlom referencií. Keby jediný ďalší odkaz na daný token stratil platnosť a prestal byť k dispozícii počas behu slučky s volaním Continue, objekt by sa potichu uvoľnil, a pri spracovaní ďalšieho bloku by spätné volanie zistilo neplatný, visiaci (dangling) ukazovateľ

Práve to je dôvod, prečo je deskriptor záznamom obsahujúcim dve, a nie jednu premennú. Pole Pause je štruktúra, ktorú si PDFium dokáže prečítať. Pole Token je plnohodnotný odkaz na typ rozhrania, ktorý kompilátor zohľadňuje a udržiava objekt tokenu v pamäti počas celej existencie tohto záznamu. Tento záznam je zároveň lokálnou premennou v zásobníku vo vnútri rutiny pre vykresľovanie, čím zostáva platný po celý čas vykonávania slučky a zruší sa len v momente, kedy rutina skončí. Holý ukazovateľ v poli user aj počítaný odkaz (referencia) v premennej Token odkazujú na totožný objekt – prvý umožňuje prístup knižnici PDFium, zatiaľ čo druhý chráni daný objekt pred zhromažďovaním a odstránením

var
  Pause: TPdfProgressivePause;
  EffectiveToken: IPdfCancellationToken;
begin
  // ... choose EffectiveToken ...

  // Strong ref first, then publish the same object to PDFium via .user.
  Pause.Token := EffectiveToken;
  Pause.Pause.version := 1;
  Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
  Pause.Pause.user := Pointer(EffectiveToken);

Zatvorenie kontextu pre vykresľovanie bez ohľadu na spôsob ukončenia slučky

Každé volanie FPDF_RenderPageBitmap_Start vytvorí progresívny stav, ktorý si PDFium s danou stránkou asociuje, a tento stav uvoľní výlučne volanie FPDF_RenderPage_Close. Slučku možno opustiť tromi spôsobmi. Po dokončení stránky je finálnym stavom hodnota FPDF_RENDER_DONE. Ak sa token aktivuje, slučka sa ukončí predčasne s informáciou o zrušení. Pri zlyhaní sa hlási stav FPDF_RENDER_FAILED. Vo všetkých týchto troch prípadoch je nutné volať príkaz Close, pričom práve pre trasu zrušenia vznikajú chyby najľahšie, keďže štandardná logika typu "zruš a vyskoč" často zanedbáva upratovanie a smeruje priamo preč. Ak funkcia Close neprebehne do konca, dôjde k unikaniu stavu príslušnej stránky z pamäte a aplikácia, ktorá by používateľovi umožnila opakované zrušenie, by toto unikanie nakopila s každou ďalšou prerušenou stránkou

Bezpečný prístup (robustný návrh) spočíva v umiestnení slučky a spracovania jej výsledku do bloku try, kým ukončovacie volanie FPDF_RenderPage_Close bude vo viazanej sekcii finally. V rovnakom bloku prebehne aj odstránenie výslednej bitovej mapy. Proces je tak možné zastaviť predčasným vyskočením z cyklu príkazom Exit, no blok finally sa vykoná vždy. Zaručíte tak, že odstránenie progresívneho stavu bude prebiehať presne na jedinom a nevyhnutnom mieste

Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
  Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
  while Status = FPDF_RENDER_TOBECONTINUED do
  begin
    if EffectiveToken.IsCancelled then
    begin
      Result := prsCancelled;
      Exit;
    end;
    Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
  end;

  if EffectiveToken.IsCancelled then
    Result := prsCancelled
  else if Status = FPDF_RENDER_DONE then
    Result := prsDone
  else
    Result := prsFailed;
finally
  // Frees the progressive state Start allocated; mandatory on every path.
  FPDF_RenderPage_Close(FPage);
  FPDFBitmap_Destroy(PdfBmp);
end;

Slučka token kontroluje nielen pred každým volaním funkcie Continue, ale zároveň pre svoju funkčnosť využíva vnútorné spätné volania. Zatiaľ čo spätné volanie (callback) redukuje priebeh aktuálne spracovávaného kúsku, počiatočná previerka v rámci slučky zabraňuje ďalšiemu spusteniu, čo obmedzuje čas zdržania (od prijatia príkazu na zrušenie) na približne jeden spracovávaný kúsok

Tri výsledky a čo obsahuje bitová mapa po zrušení

Verejným vstupným bodom pre spúšťanie je TPdf.RenderPageProgressive, pričom dané rozhranie vráti návratovú hodnotu typu TPdfProgressiveStatus, ktorá sa preklopí do jedného zo stavov prsDone, prsCancelled alebo prsFailed. Hoci tieto stavy kopírujú PDFium parametre FPDF_RENDER_* do jazyka Pascal, situáciu spojenú so zrušením začleňujú priamo ako plnohodnotný výsledok, a nie ako pochybnú chybu

Najproblematickejším bodom pre používateľov býva obvykle samotný obsah výslednej bitovej mapy po stave prsCancelled, ktorý, prekvapivo, nie je prázdny. PDFium vykonáva postupné vykresľovanie po kúskoch priamo do identickej bitovej mapy, takže pokiaľ zrušenie zastaví proces (slučku), zachová v mape čokoľvek, čo dokázalo doteraz spracovať, čiže iba čiastočný obraz. O tom, či ide pre aplikáciu (a koncového volajúceho) o relevantný vizuálny výstup, rozhoduje on sám. Zobrazovací program (viewer), ktorý by následne na základe navigačných prvkov celú mapu rovnako odstránil, ju môže ignorovať úplne. Iný grafický prehliadač zasa môže nedokončený obraz zanechať pre aspoň orientačný pohľad nízkej kvality (low-cost preview). Jedinou podstatnou vecou je, že prsCancelled nemožno interpretovať automaticky ako neadekvátne zlyhanie – reprezentuje presný okamih, v ktorom bol prerušený renderovací algoritmus (čiastkový pohľad na výsledok)

var
  Bmp: TBitmap;
  Token: IPdfCancellationToken;
  Status: TPdfProgressiveStatus;
begin
  Bmp := TBitmap.Create;
  try
    // Token starts un-cancelled; flip Token.IsCancelled from elsewhere
    // (a UI action, a navigation event) to abort the render in flight.
    Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
    case Status of
      prsDone:      Image1.Picture.Assign(Bmp);  // fully rendered
      prsCancelled: ;                            // partial bitmap, usually discarded
      prsFailed:    ShowMessage('Render failed');
    end;
  finally
    Bmp.Free;
  end;
end;

Zástupný "nil" token a plynulá voľba záchytnej trasy spätnej odozvy (callback)

Schopnosť zrušenia (cancellation) je nepovinná a je na užívateľovi (volajúcom), či ju zavedie, alebo požiada knižnicu iba o prínos spočívajúci v prijímaní udalostí vo vnútri slučiek pri zachovaní funkčnosti parametrom nil. Prirodzeným a najjednoduchším spôsobom, akým by programátor zvládol obslúžiť tento fakt, by zrejme bolo rozmiestnenie vnorených poistiek typu "pokiaľ prišiel odkaz na token" vo vetvách, to ale vedie k cyklickému zdržovaniu kódu pri každom kúsku s využitím zbytočného rozvetvovania (branch) medzi prítomnosťou a neprítomnosťou zástupného parametra

Využitie triedy priamo zohľadňuje túto skutočnosť, preto ak na vstupe necháte prázdnu funkciu, dôjde okamžitej k náhrade (substitute). Na miesto nil tak obvykle priradíme náhradný objekt na úrovni interfejsu (interface) s názvom PdfNoCancellationToken, u ktorého je parameter vlastnosti IsCancelled trvalo nastavený s návratovou hodnotou false. Prebiehajúca inštrukčná sekvencia a odozvy dokážu po zavedení bezpečne pracovať s týmto tokenom pre všetky eventuality bez vetvení alebo nutnej modifikácie dráhy (path). Záchytný token určený pre prípad bez príkazu odpovie zakaždým false a dovnútra procesov sa pre odozvu spätného volania zapisuje len nula, zatiaľ čo cyklus sa dokáže plynule uzavrieť podobne, ako keby nikdy nedošlo k spúšťaniu modulu bez povolenia k zrušeniu. Tento postup spracúva dobrovoľné moduly v podobe záchytného signálu (tichého tokenu), a nie spôsobom úplného vypustenia, čím udržiava aktívnu programovú časť prispôsobenú (uniformnú)

// nil -> never-cancel singleton, so the callback path is identical
// whether or not the caller opted into cancellation.
if AToken <> nil then
  EffectiveToken := AToken
else
  EffectiveToken := PdfNoCancellationToken;

Výsledný obraz, ktorý sme uviedli, je malý, zato natoľko hodnotný (pre znovupoužiteľnosť v programoch), že si zaslúži opätovné pripomenutie. Ak knižnica jazyka C zahŕňa záchytné volania s mechanizmom callbacku, do ktorých chcete odovzdávať stav systému, jedinou prijateľnou obojsmernou cestou voľby zvykne byť ukrytý ukazovateľ (opaque user pointer). Využite k tomu interface v Pascale (počítané premenné) schované za zmieneným užívateľským prepojením a vedľa vložte rovnako reálnu referenčnú stopu zachovávajúcu ochranu objektu od uvoľnenia z pamäte počas presunu do prostredia v jazyku C. Interface znova vráťte pre typografickú čitateľnosť, ktorú pre knižnicu následne zaisťuje kľúčové zadefinovanie cdecl k príslušnému volaniu funkcie na pevno zadeklarovanej premennej. Okrem toho chráňte slučku (drive loop) a jej celú štruktúru uväznením pod sekvenciu try spoločne za prispenia bloku finally, z ktorého natívne podklady uvoľníte. Naznačená predloha obstojí bez ohľadu na zameranie pre prakticky akékoľvek progresívne (a iné) záchytné modely na základoch odozvy priamo zameranej z prostredia PDFium – a v každej modifikácii s potrebou presného riadenia pamäte, ktorú spracúva domovské prostredie (Pascal) za súčasného dočasného odovzdania voľných ukazovateľov do externých komponentov s prostredím C

Fáza zrušenia procesov tvorí spravidla polovičnú podstatu fungujúceho moderného čítacieho prostredia (viewer). O druhú polovicu s vynechaním prečítaných a naformátovaných údajov (bez znovuvytvorenia v cykloch), ako aj o funkčnosť oddialenia/priblíženia (zoom) s prispôsobovaním cache bitových podkladov sa detailnejšie informácie dozviete v našom sprievodnom príspevku určenom priamo na vykresľovanie, odozvu vyrovnávacej pamäte a ladenie funkcie priblíženia v aplikáciách. Komplexný nákres o skompletizovaní vlastností čitateľskej zložky, funkcie vyhľadávania a integrovaných značiek pre PDFium knižnicu vo vizuálnom prostredí Delphi pokrýva rozsiahla analýza v dokumente pre vybudovanie funkčného PDF viewera s PDFium komponentom. Predstavený systém spracúvajúci riadené postupné vizuálne zobrazovanie sprevádza inštalačný modul prispôsobený pre PDFium Component ako uvoľnené moduly (load, form API) spájané s obsluhou ďalších operácií zdieľaných naprieč komunitným vzdelávacím príspevkom na samotnom blogu