Technický článek

Prohlížeč PDF s plynulým posouváním v Delphi s PDFium Component

Jediná stránka formátu A4 vykreslená při přiblížení vhodném pro pohodlné čtení zabírá v paměti přibližně několik megabajtů jako 32bitová bitmapa. Vynásobte to smlouvou o 400 stránkách a matematika přestane být abstraktní: pokud vykreslíte každou stránku předem, budete po systému Windows požadovat výrazně přes gigabajt bitmap, které si uživatel bude prohlížet po jednotlivých obrazovkách. Aplikace buď vyčerpá adresní prostor ve 32bitovém sestavení, nebo stráví prvních několik sekund zamrzlá, zatímco grafická karta (GPU) a analyzátor stránek zpracovávají listy, na které nikdo ještě ani neposunul. Prohlížeč s plynulým posouváním (continuous scroll) musí působit jako jeden dlouhý pás stránek, ale nemůže je reálně držet v paměti všechny najednou

Toto pnutí definuje celý problém. Komponenta PDFium Component jej řeší uvnitř třídy TPdfView, takže většina práce spočívá ve volbě správného režimu zobrazení a pochopení toho, co komponenta dělá za vás. Části, které za vás neřeší — jako je přizpůsobení velikosti stránek plynulému čtení a zajištění rychlé odezvy při posouvání —, vyžadují napsání malého množství kódu. Pokud stále sestavujete okolní prvky rozhraní (panel nástrojů, miniatury, vyhledávací pole), popisuje tento postup návod na funkčně bohatý prohlížeč. Zde se budeme věnovat samotnému posouvání

Rozvržení je režim zobrazení, nikoli panel s bitmapami

Při práci s formuláři VCL bývá prvním instinktem sáhnout po komponentě ScrollBox a skládat do ní obrázky (TImage) pro každou stránku zvlášť. Tomuto nutkání odolejte. Tento návrh vás nutí řešit pozicování stránek, matematiku posouvání i správu paměti najednou a vše z toho byste naimplementovali s chybami. Třída TPdfView již sama o sobě modeluje dokument jako plynulý pás stránek a zpřístupňuje toto rozvržení prostřednictvím vlastnosti DisplayMode

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');

To je celé nastavení plynulého posouvání. Režim dmSingleContinuous uspořádá stránky do jednoho svislého sloupce, přičemž mezery mezi nimi řeší interně, a prohlížeč tímto sloupcem posouvá jako jedním povrchem. Pro běžnou navigaci není nutné propojovat ovládací prvky jednotlivých stránek ani psát obsluhu posouvání. Všimněte si kontroly vlastnosti Pdf.Active po přiřazení: otevření dokumentu nevyvolává výjimky, takže poškozený nebo heslem chráněný soubor ponechá Active na hodnotě False, aniž by došlo k chybě. Prohlížeč, který tuto kontrolu vynechá, pak zobrazí prázdný panel a chyba bude mylně hledána v něm

Stejná vlastnost podporuje také režimy dvou stránek vedle sebe. Režim dmTwoPageContinuous umisťuje stránky vedle sebe, dvě na řádek, což odpovídá stylu knihy, který některé dokumenty vyžadují. dmTwoPageContinuousWithCover postupuje stejně, ale ponechává první stránku samostatně jako obálku, takže zbývající dvojice stránek vycházejí na přirozené sudé a liché hranice. Všechny tři režimy se posouvají plynule. Přepínání mezi nimi je otázkou jediného přiřazení, což usnadňuje pozdější přidání rozbalovacího seznamu pro volbu režimu

Rastrují se pouze viditelné stránky

Důvodem, proč toto řešení funguje i pro soubor o 400 stránkách, je to, že sloupec stránek je virtuální. Komponenta TPdfView zná výšku každé stránky ze stromu stránek dokumentu, takže dokáže spočítat celkový rozsah posouvání a pozici každé stránky, aniž by cokoli rastrovala. Rastrování — náročný krok, který převádí tok obsahu stránky na pixely — probíhá pouze u stránek, které aktuálně protínají výřez zobrazení (viewport), plus s malou rezervou, aby byla stránka připravena v okamžiku, kdy na ni uživatel posune. Při posouvání dolů se stránky vstupující do výřezu vykreslují a stránkám, které jej opouštějí, se uvolňují jejich bitmapy z paměti. Spotřeba paměti tak zůstává úměrná tomu, co se vejde na obrazovku, nikoli celkové délce dokumentu

Tento princip je dobré si osvojit, protože mění pohled na náročnost operací. Otevření 400stránkového dokumentu je nenáročné: analyzuje se pouze struktura, nikoli samotný obsah. Náklady se platí líně (lazily) pro každou stránku zvlášť až v okamžiku, kdy se k ní posuvník přiblíží. Prohlížeč, který se při otevření zdá okamžitý a při posouvání plynulý, nedělá celkově méně práce; pouze ji rozkládá na skutečnou trasu čtení uživatele a zahazuje to, co již zůstalo pozadu. Praktickým důsledkem je, že téměř nikdy nechcete vynucovat vykreslování stránek dopředu před uživatelem. Nechte prohlížeč, aby sám rozhodl, co je viditelné

Přizpůsobit stránky na šířku a neměnit měřítko ručně

Pro plynulé čtení je vhodné přizpůsobit velikost stránek šířce panelu, nikoli je vázat na pevné měřítko. Vlastnost FitMode se o to postará a udržuje toto chování i při změně velikosti okna

PdfView.FitMode := pfmFitWidth;   // each page fills the column width; height follows

V režimu pfmFitWidth komponenta přepočítává měřítko (zoom) při každé změně velikosti zobrazení. Sloupec stránek tak vždy vyplňuje dostupnou šířku a výšky stránek (a tím i rozsah posouvání) se tomu přizpůsobují. Je zde však jedno úskalí: přímé přiřazení hodnoty do Zoom resetuje vlastnost FitMode zpět na pfmNone. To je záměrné, protože ruční nastavení měřítka a automatické přizpůsobení velikosti jdou proti sobě. Znamená to však, že náhodné volání PdfView.Zoom := 1.0 kdekoli ve vašem kódu tiše vypne přizpůsobení na šířku a při další změně velikosti okna se rozvržení nepřepočítá. Pokud nabízíte jak ovládání měřítka, tak tlačítko pro přizpůsobení velikosti, přistupujte k nim jako k přepínání režimů: nastavení jednoho vymaže druhé a vy určujete, které má přednost

Pro přirozené ovládání pevného měřítka zpřístupňuje prohlížeč hodnoty přiblížení jako vlastnosti, které můžete aplikovat nebo zobrazit: PageWidthZoom[PageNumber] vrací hodnotu přiblížení, která by přizpůsobila danou stránku na šířku, a PageZoom přizpůsobí celou stránku výšce. Načtením těchto hodnot můžete sestavit nabídku „Přizpůsobit na šířku“ / „Přizpůsobit na stránku“, aniž byste museli pevně zapisovat procenta, která by nefungovala u stránek na šířku (landscape) nebo u nestandardních rozměrů

Zajištění odezvy při rychlém posouvání pomocí progresivního vykreslování

Výchozí cesta vykreslování vykreslí stránku kompletně předtím, než vrátí řízení. U jediné stránky je to v pořádku. Při rychlém posouvání rozsáhlým dokumentem však nikoli: každá stránka, která se na okamžik mihne na obrazovce, spustí plné rastrování. Pokud uživatel posouvá rychleji, než se stránky stíhají vykreslovat, požadavky se hromadí a zobrazení začne trhat, protože se zpracovávají stránky, které jsou v okamžiku dokončení vykreslování již dávno mimo obrazovku. Řešením je učinit vykreslování zrušitelným a opustit jej ve chvíli, kdy uživatel posune dál

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;

Klíčovou roli hraje návratová hodnota. prsDone znamená, že bitmapa je kompletně vykreslena a připravena k zobrazení na obrazovce; prsCancelled značí, že nová pozice posuvníku tuto stránku nahradila, takže rozpracovaný výsledek zahodíte, místo abyste jej zobrazovali; prsFailed značí reálnou chybu na dané stránce. Zrušení se kontroluje na hranicích jednotlivých částí, nikoli okamžitě, takže mezi voláním Cancel a skutečným zastavením vykreslování počítejte s latencí v řádu desítek milisekund. To je však stále nesrovnatelně úspornější než nechat frontu zablokovanou vykreslováním stránek, které již nikoho nezajímají. Předání hodnoty nil namísto tokenu vykreslí stránku kompletně bez přerušení, což je správná volba pro jednorázové operace, jako je náhled před tiskem, kde není důvod k přerušování

Pokud namísto toho voláte funkci RenderPage, která vrací novou instanci TBitmap, pamatujte, že volající kód se stává jejím vlastníkem a musí ji uvolnit pomocí Free. V cyklu posouvání, který alokuje bitmapu pro každou stránku zvlášť, by opomenutí tohoto kroku vedlo k úniku paměti rostoucímu s každým posunem stránky — což je přesně to nekontrolované vyčerpání paměti, kterému měl návrh s plynulým posouváním zabránit. Kdykoli je to možné, vykreslujte do již existující bitmapy

Co vám zbývá vyřešit

Vytvoření prohlížeče s plynulým posouváním leží z velké části na samotné komponentě. Zvolíte dmSingleContinuous pro rozvržení, nastavíte pfmFitWidth pro přizpůsobení sloupce šířce okna a zkontrolujete Pdf.Active pro zachycení neplatných souborů. Jedinou částí, kterou stojí za to napsat vlastními silami, je přerušitelné vykreslování, protože kvalita prohlížeče se hodnotí podle toho, jak reaguje, když uživatel rychle přetáhne posuvník až na konec dlouhého dokumentu. Vše ostatní — výběr textu napříč stránkami, zvýrazňování výsledků hledání nebo strom záložek — je již otázkou uživatelského rozhraní, které staví nad tímto posuvným povrchem, nikoli uvnitř něj

Zde představená rozhraní API TPdfView, DisplayMode a RenderPageProgressive jsou součástí produktu PDFium Component pro Delphi a Lazarus