En enkelt A4-side renderet i en behagelig læse-zoom svarer til et par megabyte 32-bit bitmap. Ganger man det med en kontrakt på 400 sider, ophører aritmetikken med at være abstrakt: Renderes hver side på forhånd, beder du Windows om langt over en gigabyte bitmaps, som brugeren vil se på én skærmfuld ad gangen. Programmet løber enten tør for adresseplads på en 32-bit version eller fryser i de første par sekunder, mens GPU'en og sideparseren kværner sig igennem sider, som ingen har rullet hen til endnu. En fremviser med kontinuerlig rulning skal føles som ét langt bånd af sider, men den kan ikke reelt have dem alle i hukommelsen på én gang
Dette spændingsfelt er hele problemet her. PDFium Component løser det inde i TPdfView, så det meste af arbejdet består i at vælge den rigtige visningstilstand og forstå, hvad komponenten gør på dine vegne. De dele, den ikke gør for dig — dimensionering af sider til et læseflow og at holde hurtig rulning responsiv — er der, hvor en lille smule kode gør gavn. Hvis du still er ved at samle den omgivende brugerflade (værktøjslinje, miniaturer, søgeboks), dækker gennemgangen af en funktionsrig fremviser det emne; her er emnet selve rulningen
Layoutet er en visningstilstand, ikke et panel af bitmaps
Instinktet fra VCL-formulararbejde er at række ud efter en scroll-boks og stable billedkontroller inde i den, én pr. side. Modstå det. Det design tvinger dig til at eje sidepositionering, rulningsaritmetik og hukommelsesspørgsmålet på én gang, og du vil genopfinde hver eneste af dem på en dårlig måde. TPdfView modellerer allerede dokumentet som et kontinuerligt forløb af sider og eksponerer layoutet gennem sin DisplayMode-egenskab
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');
Dette er hele opsætningen med kontinuerlig rulning. dmSingleContinuous udlægger siderne i en enkelt lodret kolonne, hvor hullerne mellem dem håndteres internt, og visningen ruller gennem denne kolonne som én overflade. Der er ingen kontrol pr. side, der skal forbindes, og ingen rulningsbehandler, der skal skrives for almindelig navigation. Bemærk kontrollen af Pdf.Active efter tildelingen: Åbning af et dokument udløser aldrig en undtagelse, så en beskadiget eller adgangskodebeskyttet fil efterlader Active på False uden nogen undtagelse at fange, og en fremviser, der springer denne kontrol over, renderer et tomt panel og bebrejder sig selv
Den samme egenskab bærer opslags-tilstandene. dmTwoPageContinuous placerer sider side om side, to i en række, til den boglignende læsning, som nogle dokumenter kræver; dmTwoPageContinuousWithCover gør det samme, men lader side et stå alene som et omslag, så de resterende opslag falder på den naturlige lige-ulige grænse. Alle tre ruller kontinuerligt. Skift mellem dem er en enkelt tildeling, hvilket gør en kombinationsboks til visningstilstand nem at tilføje senere
Kun de synlige sider bliver rasteriseret
Grunden til, at dette kan skaleres til en fil på 400 sider, er, at kolonnen er virtuel. TPdfView kender højden på hver side ud fra dokumentets sidetræ, så den kan beregne det samlede rulningsområde og positionen for hver side uden at rasterisere noget. Rasterisering, det dyre trin, der forvandler en sides indholdsstrøm til pixels, sker kun for de sider, der i øjeblikket skærer visningsområdet (viewport), plus a small margin so a page is ready by the time it scrolls into view. Når du ruller ned, renderes sider, der kommer ind i visningsområdet, og sider, der forlader det, får deres bitmaps frigivet. Hukommelsen forbliver proportional med det, der kan være på skærmen, ikke med dokumentets længde
Dette er værd at internalisere, fordi det ændrer den måde, du tænker på omkostninger. Åbning af et dokument på 400 sider is cheap: Det parser strukturen, ikke indholdet. Udgiften er pr. side, og den betales dovent (lazily), i det øjeblik en side rulles tæt på. En fremviser, der føles øjeblikkelig ved åbning og flydende under rulning, udfører ikke mindre arbejde samlet set, den spreder blot arbejdet over brugerens faktiske læsesti og kasserer det, der lades bagud. Den praktiske konsekvens er, at du næsten aldrig ønsker at tvinge rendering af sider forud for brugeren. Lad visningen bestemme, hvad der er synligt
Tilpas sider til bredden, og lad derefter zoom være i fred
En læsekolonne kræver sider, der er tilpasset panelets bredde, ikke låst til en absolut zoom. FitMode gør dette og bliver ved med det, når vinduets størrelse ændres
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
Med pfmFitWidth genberegner komponenten zoomen, hver gang visningen ændrer størrelse, så kolonnen altid udfylder den tilgængelige bredde, og sidehøjderne, og dermed rulningsområdet, følger deraf. Der er én fælde, der fanger folk: Direkte tildeling af Zoom nulstiller FitMode tilbage til pfmNone. Det er bevidst, fordi en manuel zoom og en automatisk tilpasning er modstridende hensigter, men det betyder, at en vildfaren PdfView.Zoom := 1.0 et sted i din kode lydløst slår tilpas-til-bredde fra, og den næste størrelsesændring stopper med at ombryde. Hvis du tilbyder både en zoom-kontrol og en tilpasningsknap, så behandl dem som et tilstandsskift: Indstilling af den ene rydder den anden, og du bestemmer, hvilken der vinder
For absolutte zoom-kontroller, der læses naturligt, eksponerer visningen tilpasnings-zooms som værdier, du kan anvende eller vise: PageWidthZoom[PageNumber] returnerer den zoom, der ville tilpasse den side til bredden, og den tilsvarende PageZoom tilpasser hele siden. At læse disse er måden, du udfylder en "Tilpas til bredde" / "Tilpas side"-menu på uden at hårdkode magiske procentsatser, der går galt på sider i liggende format eller i overstørrelse
Hold hurtig rulning responsiv med progressiv rendering
Standard-renderingsstien tegner en side færdig, før den returnerer. For en enkelt side er det fint. Under hurtig rulning (flick-scroll) gennem et tæt dokument er det ikke: Hver side, der suser forbi, sætter gang i en fuld rasterisering, og hvis brugeren ruller hurtigere, end siderne kan renderes, hobes disse renderinger op, og panelet hakker, fordi der udføres arbejde for sider, som allerede er væk fra skærmen, når det er færdigt. Løsningen er at gøre en rendering afbrydelig og opgive den i det øjeblik, brugeren bevæger sig videre
RenderPageProgressive renderer i bidder og kontrollerer et afbrydelsestoken ved hver bidgrænse, så en igangværende rendering af en side, der lige er rullet væk, kan droppes i stedet for at køre til ende
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;
Den form, der betyder noget, er returværdien. prsDone betyder, at bitmappen er fuldt tegnet og værd at overføre (blit) til skærmen; prsCancelled betyder, at en nyere rulningsposition erstattede denne side, så du smider det delvise resultat ud i stedet for at vise det; prsFailed is a genuine error on that page. Afbrydelse polles ved bidgrænser frem for præemptivt, så forvent tiendedele millisekunders latenstid mellem kald af Cancel og at renderingen faktisk stopper. Det er stadig langt billigere end at lade en forældet fuldsides-rendering blokere køen. Videregivelse af nil som token renderer lige igennem til afslutning, hvilket er det rigtige valg for en engangsrendering som en udskriftsforhåndsvisning, hvor der ikke er noget at afbryde imod
Når du i stedet kalder funktionsformen af RenderPage, den der returnerer en ny TBitmap, skal du huske, at kalderen ejer den og skal frigive (Free) den. I en rulningsløkke, der allokerer en bitmap pr. side, er det at glemme dette en lækage, der vokser med hver side, brugeren passerer, hvilket er præcis den ubegrænsede hukommelsesfejl, som det kontinuerlige design skulle undgå. Render ind i en genbrugt bitmap, hvor du kan
Hvad du står tilbage med
Fremviseren med kontinuerlig rulning er for det meste komponentens opgave at levere. Du vælger dmSingleContinuous til layoutet, indstiller pfmFitWidth, så kolonnen ombrydes med vinduet, og kontrollerer Pdf.Active, så en dårlig fil fejler højlydt. Den eneste del, der er værd at skrive selv, er afbrydelig rendering, fordi en fremviser bedømmes på, hvordan den opfører sig, når nogen trækker rullebjælken til bunden af et langt dokument, og panelet enten følger med eller ej. Alt ud over det — tekstvalg på tværs af sider, søgefremhævning, et bogmærketræ — er brugerfladearbejde, der sidder oven på denne rulningsoverflade frem for inde i den
API'erne TPdfView, DisplayMode og RenderPageProgressive, der er vist her, er en del af PDFium Component til Delphi og Lazarus