Teknisk artikel

Kontinuerligt rullande PDF-visare i Delphi med PDFium Component

En enskild A4-sida som renderas vid en bekväm läszoom ligger i storleksordningen några megabyte av 32-bitars bitmapp. Multiplicera det med ett avtal på 400 sidor så slutar aritmetiken att vara abstrakt: rendera varje sida i förväg och du ber Windows om långt över en gigabyte bitmappar som användaren kommer att titta på en skärmbild åt gången. Programmet får entingen slut på adressutrymme i ett 32-bitarsbygge eller tillbringar sina första sekunder med att vara fryst medan grafikprocessorn och sidtolkaren tuggar sig igenom sidor som ingen har rullat till ännu. En kontinuerligt rullande läsare måste kännas som ett enda långt band av sidor, men den kan faktiskt inte hålla alla i minnet samtidigt

Denna spänning är hela problemet här. PDFium Component löser det inuti TPdfView, så det mesta av arbetet handlar om att välja rätt visningsläge och förstå vad komponenten gör å dina vägnar. De bitar den inte gör åt dig, att dimensionera sidor för ett läsflöde och hålla snabb rullning responsiv, är där lite kod gör skäl för sig. Om du fortfarande sätter ihop det omgivande skalet (verktygsfält, miniatyrbilder, sökruta) täcker genomgången för den funktionsrika visaren den biten; här är ämnet själva rullningen

Layouten är ett visningsläge, inte en panel av bitmappar

Instinkten från VCL-formulärsarbete är att sträcka sig efter en rullningsruta (scroll box) och stapla bildkontroller inuti den, en per sida. Stå emot det. Den designen tvingar dig att äga sidpositionering, rullningsmatematik och minnesfrågan på en och samma gång, och du kommer att återuppfinna var och en av dem på ett dåligt sätt. TPdfView modellerar redan dokumentet som en kontinuerlig följd av sidor och exponerar layouten genom sin egenskap 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');

Det är hela konfigurationen för kontinuerlig rullning. dmSingleContinuous lägger ut sidorna i en enda vertikal kolumn med mellanrummen mellan dem hanterade internt, och vyn rullar genom den kolumnen som en yta. Det finns ingen kontroll per sida att koppla upp och ingen rullningshanterare att skriva för vanlig navigering. Observera kontrollen av Pdf.Active efter tilldelningen: att öppna ett dokument kastar aldrig undantag, så en skadad eller lösenordsskyddad fil lämnar Active stående på False utan något undantag att fånga, och en visare som hoppar över denna kontroll renderar en tom panel och skyller på sig själv

Samma egenskap bär uppslags-lägena. dmTwoPageContinuous placerar sidorna sida vid sida, två på en rad, för den bokliknande läsning vissa dokument vill ha; dmTwoPageContinuousWithCover gör detsamma men låter sida ett stå ensam som ett omslag så att de återstående uppslagen faller på den naturliga jämn-udda-gränsen. Alla tre rullar kontinuerligt. Att växla mellan dem är en enkel tilldelning, vilket gör en kombinationsruta för visningsläge trivial att lägga till senare

Endast de synliga sidorna rastreras

Anledningen till att detta skalar till en fil på 400 sidor är att kolumnen är virtuell. TPdfView känner till höjden på varje sida från dokumentets sidträd, så det kan beräkna den totala rullningsutsträckningen och positionen för varje sida utan att rastrera någonting. Rastrering, det dyra steget som omvandlar en sidas innehållsström till pixlar, sker endast för de sidor som för närvarande korsar visningsporten (viewporten), plus en liten marginal så att en sida är klar i det ögonblick den rullar in i vyn. När du rullar nedåt renderas sidor som kommer in i visningsporten och sidor som lämnar den får sina bitmappar frigjorda. Minnet förblir proportionellt mot det som får plats på skärmen, inte mot dokumentlängden

Detta är värt att ta till sig eftersom det förändrar hur du resonerar kring kostnader. Att öppna ett dokument på 400 sidor är billigt: det tolkar strukturen, inte innehållet. Kostnaden är per sida och den betalas lättjefullt (lazy), i det ögonblick en sida rullas nära. En visare som känns omedelbar när den öppnas och mjuk vid rullning gör inte mindre arbete totalt sett, den sprider ut arbetet över användarens faktiska läsväg och slänger det som hamnar på efterkälken. Den praktiska konsekvensen är att du nästan aldrig vill tvinga fram rendering av sidor i förväg för användaren. Låt vyn bestämma vad som är synligt

Dimensionera sidorna till bredden, låt sedan zoomen vara ifred

En läskolumn vill ha sidor dimensionerade till panelens bredd, inte fastnålade vid en absolut zoom. FitMode gör detta och fortsätter att göra det när fönstret ändrar storlek

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

Med pfmFitWidth beräknar komponenten om zoomen närhelst vyn ändrar storlek, så att kolumnen alltid fyller den tillgängliga bredden och sidhöjderna, och därmed rullningsutsträckningen, följer av det. Det finns en fälla som fångar människor: att tilldela Zoom direkt återställer FitMode till pfmNone. Det är avsiktligt, eftersom en manuell zoom och en automatisk anpassning är motstridiga intentioner, men det betyder att ett förvirrat PdfView.Zoom := 1.0 någonstans i din kod i tysthet stänger av anpassning-till-bredd (fit-to-width) och nästa storleksändring slutar omflöda (reflow). Om du erbjuder både en zoomkontroll och en anpassningsknapp, behandla dem som en lägesomkopplare: att ställa in den ena rensar den andra, och du bestämmer vilken som vinner

För absoluta zoomkontroller som läses naturligt exponerar vyn anpassnings-zoomarna som värden du kan tillämpa eller visa: PageWidthZoom[PageNumber] returnerar den zoom som skulle anpassa den sidan till bredden, och den matchande PageZoom anpassar hela sidan. Att läsa dessa är hur du fyller en "Anpassa till bredd" / "Anpassa sida"-meny utan att hårdkoda magiska procentsatser som blir fel på liggande eller överdimensionerade sidor

Håll snabb rullning responsiv med progressiv rendering

Den standardmässiga renderingsvägen ritar en sida till slutförande innan den returnerar. För en enstaka sida är det bra. Under en svep-rullning genom ett tätt dokument är det inte det: varje sida som blinkar förbi startar en fullständig rastrering, och om användaren rullar snabbare än sidorna kan renderas staplas dessa renderingar på varandra och panelen hackar eftersom arbete görs för sidor som redan är utanför skärmen när det är klart. Fixen är att göra en rendering avbrytbar och överge den i det ögonblick användaren går vidare

RenderPageProgressive renderar i bitar (chunks) och kontrollerar ett avbokningstecken (cancellation token) vid varje bitgräns, så att en pågående rendering av en sida som precis rullades bort kan släppas i stället för att köras till slutet

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;

Formen som spelar roll är returvärdet. prsDone betyder att bitmappen är helt målad och värd att blitta till skärmen; prsCancelled betyder att en nyare rullningsposition ersatte denna sida, så du kastar bort det partiella resultatet snarare än att visa det; prsFailed är ett genuint fel på den sidan. Avbrytande kontrolleras vid bitgränser snarare än förebyggande, så förvänta dig tiotals millisekunder i latens mellan anropet av Cancel och att renderingen faktiskt stannar. Det är fortfarande mycket billigare än att låta en inaktuell helsidesrendering blockera kön. Att skicka nil som token renderar rakt igenom till slutförande, vilket är rätt val för en engångsrendering som en förhandsgranskning före utskrift där det inte finns något att avbryta mot

När du istället anropar funktionsformen av RenderPage, den som returnerar en färsk TBitmap, kom ihåg att anroparen äger den och måste anropa Free på den. I en rullningsloop som allokerar en bitmapp per sida är att glömma detta en läcka som växer med varje sida användaren passerar, vilket är exakt det fel med obegränsat minne som den kontinuerliga designen var tänkt att undvika. Rendera till en återanvänd bitmapp där du kan

Vad du lämnas med

Den kontinuerligt rullande läsaren är mestadels komponentens sak att leverera. Du väljer dmSingleContinuous för layouten, ställer in pfmFitWidth så att kolumnen omflödas med fönstret, och kontrollerar Pdf.Active så att en dålig fil misslyckas högljutt. Den enda bit som är värd att skriva själv är avbrytbar rendering, eftersom en läsare bedöms efter hur den beter sig när någon drar i rullningslisten till botten av ett långt dokument och panelen antingen hänger med eller inte gör det. Allt utöver det, textmarkering över sidor, sökmarkering, ett bokmärkesträd, är gränssnittsarbete som sitter ovanpå denna rullningsyta snarare än inuti den

De TPdfView, DisplayMode och RenderPageProgressive API:er som visas här är en del av PDFium Component för Delphi och Lazarus