Техническа статия

PDF четец с непрекъснато превъртане в Delphi с PDFium Component

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

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

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

Инстинктът при разработка с 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 знае височината на всяка страница от дървото на документа, така че може да изчисли общия размер на превъртане и позицията на всяка страница, без да растеризира нищо. Растеризацията – скъпата стъпка, която превръща потока от съдържание на страницата в пиксели – се случва само за страниците, които в момента пресичат видимата област, плюс малък буфер, така че страницата да е готова при появата си. При превъртане надолу новите страници се рендират, а излизащите от екрана освобождават паметта си. Заеманата памет остава пропорционална на това, което се вижда на екрана, а не на общата дължина на документа

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

Мащабирайте страниците по ширина и не променяйте мащаба ръчно

Колоната за четене изисква страниците да съответстват на ширината на панела, а не да са фиксирани с конкретен мащаб. Свойството 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 Component за Delphi и Lazarus