Technical Article

Преглед на PDF с непрекъснато превъртане в Delphi с PDFium VCL

Единична страница А4, изобразена с удобен за четене мащаб, е от порядъка на няколко мегабайта 32-битова растерна графика. Умножете това по договор от 400 страници и аритметиката престава да бъде абстрактна: ако изобразите всяка страница предварително, ще изискате от Windows над един гигабайт растерни графики, които потребителят ще гледа екран по екран. Приложението или ще изчерпи адресното пространство при 32-битова компилация, или ще прекара първите си няколко секунди в замръзнало състояние, докато графичният процесор (GPU) и парсерът на страници обработват страници, до които никой все още не е превъртял. Четецът с непрекъснато превъртане трябва да се усеща като една дълга лента от страници, но не може действително да държи всички тях в паметта едновременно.

Това напрежение е целият проблем тук. PDFium VCL го решава вътре в TPdfView, так че по-голямата част от работата е в избора на правилния режим на показване и разбирането на това какво прави компонентът вместо вас. Частите, които той не прави автоматично â€?оразмеряването на страниците за четене и поддържането на бързото превъртане отзивчиво â€?са местата, където малко код си заслужава усилията. Ако все още сглобявате околния интерфейс (лента с инструменти, миниатюри, поле за търсене), ръководството за богат на функции четец покрива тази тема; тук предметът е самото превъртане.

Оформлението е режим на показване, а не панел от растерни изображения

Инстинктът при работа с VCL форми е да потърсите кутия за превъртане (scroll box) и да подредите контролни елементи за изображения в нея, по един за всяка страница. Противопоставете се на това. Този дизайн ви принуждава да управлявате позиционирането на страниците, математиката на превъртането и управлението на паметта едновременно, и ще преоткриете лошо всяко едно от тях. TPdfView вече моделира документа като непрекъсната поредица от страници и излага оформлението чрез своето свойство 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');

Това е цялата настройка за непрекъснато превъртане. dmSingleContinuous подрежда страниците в една вертикална колона, като разстоянията между тях се обработват вътрешно, а изгледът се превърта през тази колона като една повърхност. Няма управление на отделни страници за свързване и няма манипулатор за превъртане за писане при нормална навигация. Обърнете внимание на проверката на Pdf.Active след присвояването: отварянето на документ никога не хвърля изключение, така че повреден или защитен с парола файл оставя Active в състояние False без изключение за улавяне, а четец, който пропуска тази проверка, изобразява празен панел и обвинява себе си.

Същото свойство поддържа и режимите на разгръщане. dmTwoPageContinuous поставя страниците една до друга, по две на ред, за четене в стил книга, каквото някои документи изискват; dmTwoPageContinuousWithCover прави същото, но оставя първата страница самостоятелна като корица, така че останалите разгръщания да съвпадат с естествената четно-нечетна граница. И трите режима се превъртат непрекъснато. Превключването между тях е с едно присвояване, което прави лесно добавянето на избор на режим по-късно.

Само видимите страници се растеризират

Причината това да работи за файл от 400 страници е, че колоната е виртуална. TPdfView знае височината на всяка страница от дървото на страниците на документа, така че може да изчисли общия размер на превъртане и позицията на всяка страница, без да растеризира нищо. Растеризацията â€?скъпата стъпка, която превръща потока от съдържание на страницата в пиксели â€?се случва само за страниците, които в момента пресичат прозореца за преглед (viewport), плюс малък марж, за да бъде страницата готова, когато се появи на екрана. Докато превъртате надолу, страниците, влизащи в прозореца за преглед, се изобразяват, а страниците, които го напускат, освобождават своите растерни изображения. Паметта остава пропорционална на това, което се вижда на екрана, а не на дължината на документа.

Това си струва да се разбере добре, защото променя начина, по който разсъждавате за производителността. Отварянето на документ от 400 страници е евтино: то анализира структурата, а не съдържанието. Разходът е на ниво страница и се плаща отложено (lazily), в момента, в който се приближите до страница при превъртане. Четец, който се усеща мигновен при отваряне и плавен при превъртане, не върши по-малко работа като цяло; той просто разпределя работата по действителния път на четене на потребителя и изхвърля това, което остава назад. Практическото следствие е, че почти никога не трябва да налагате предварително изобразяване на страници пред потребителя. Оставете изгледа да реши какво е видимо.

Оразмерете страниците по ширината, след което не пипайте мащаба

Колоната за четене изисква страниците да бъдат оразмерени спрямо ширината на панела, а не да са фиксирани към абсолютен мащаб. FitMode прави точно това и продължава да го прави при преоразмеряване на прозореца.

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

При pfmFitWidth компонентът преизчислява мащаба всеки път, когато изгледът се преоразмери, така че колоната винаги запълва наличната ширина, а височините на страниците и съответно обхватът на превъртане следват от това. Има един капан, който улавя разработчиците: директното присвояване на Zoom нулира FitMode обратно до pfmNone. Това е умишлено, тъй като ръчният мащаб и автоматичното напасване са противоречиви намерения, но означава, че случайно PdfView.Zoom := 1.0 някъде в кода ви тихомълком изключва напасването по ширина и следващото преоразмеряване спира да преформатира изгледа. Ако предлагате както контрол на мащаба, така и бутон за напасване, третирайте ги като превключване на режими: задаването на едното изчиства другото и вие решавате кое да надделее.

За контрол на абсолютния мащаб, който се усеща естествено, изгледът излага мащабите за напасване като стойности, които можете да приложите или покажете: PageWidthZoom[PageNumber] връща мащаба, който би напаснал тази страница по ширината, а съответният PageZoom напасва цялата страница. Четенето на тези стойности е начинът да попълните меню "По ширина" / "Цяла страница", без да кодирате твърди магически проценти, които се объркват при пейзажни или извънгабаритни страници.

Поддържайте бързото превъртане отзивчиво с прогресивно изобразяване

Пътят на изобразяване по подразбиране рисува страницата докрай, преди да се върне. За една страница това е добре. По време на бързо превъртане през обемист документ обаче не е: всяка преминаваща страница стартира пълна растеризация и ако потребителят превърта по-бързо, отколкото страниците могат да се изобразят, тези процеси се натрупват и панелът започва да насича, тъй като се върши работа за страници, които вече са извън екрана, когато процесът приключи. Решението е изобразяването да бъде прекъсваемо и да се изоставя в момента, в който потребителят продължи нататък.

RenderPageProgressive изобразява на части и проверява токен за отмяна (cancellation token) на границата на всяка част, така че текущото изобразяване на страница, която току-що е била превъртана извън екрана, може да бъде прекратено, вместо да се изпълнява докрай.

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;

Формата, която е от значение, е върнатата стойност. prsDone означава, че растерното изображение е напълно нарисувано и е готово за показване на екрана; prsCancelled означава, че по-нова позиция на превъртане е заменила тази страница, така че изхвърляте частния резултат, вместо да го показвате; prsFailed е реална грешка на тази страница. Отмяната се проверява на границите на частите, а не превантивно, така че очаквайте десетки милисекунди закъснение между извикването на Cancel и действителното спиране на изобразяването. Това все пак е много по-евтино от оставянето на остаряло изобразяване на цяла страница да блокира опашката. Предаването на nil като токен води до пълно изобразяване без прекъсване, което е правилният избор за еднократни изобразявания като преглед преди печат, където няма срещу какво да се извърши отмяна.

Когато вместо това извикате функцията RenderPage, която връща нов обект TBitmap, помнете, че извикващият код притежава обекта и трябва да го освободи с Free. В цикъл за превъртане, който разпределя растерно изображение на всяка страница, забравянето на това е теч на памет, който расте с всяка страница, която потребителят преминава â€?което е точно сривът на неограничена памет, който непрекъснатият дизайн трябваше да избегне. Изобразявайте в повторно използвано растерно изображение, където е възможно.

Какво получавате в крайна сметка

Четецът с непрекъснато превъртане се осигурява основно от самия компонент. Избирате dmSingleContinuous за оформлението, задавате pfmFitWidth, така че колоната да се преформатира спрямо прозореца, и проверявате Pdf.Active, за да може повреден файл да сигнализира ясно за грешка. Единствената част, която си струва да напишете сами, е прекъсваемото изобразяване, тъй като един четец се оценява по това как се държи, когато някой дръпне плъзгача за превъртане до най-долната част на дълъг документ и панелът или успява да го настигне, или не. Всичко след това â€?избор на текст в страниците, подчертаване на търсенето, дърво с отметки â€?е интерфейсна работа, която стои върху тази повърхност за превъртане, а не вътре в нея.

Интерфейсите TPdfView, DisplayMode и RenderPageProgressive, показани тук, са част от PDFium VCL компонента за Delphi и Lazarus.