Technical Article

Създаване на PDF четец в Delphi с PDFium VCL

Изграждането на PDF четец в Delphi се свежда до два компонента и връзката помежду им. TPdf притежава документа: той отваря файла, декриптира го и отговаря на въпроси за брой страници и метаданни. TPdfView е визуалната контрола, която изобразява страниците на екрана и управлява превъртането, мащабирането и конкретната страница, която потребителят преглежда в момента. PDFium VCL обвива същия двигател за рендиране, който се доставя в Chrome, така че глифовете, заглаждането на шрифтовете (anti-aliasing) и цветовете върху платното съвпадат с това, което вашите потребители вече виждат в браузъра си. Работата не е в самото изобразяване. Тя е в свързването на обекта на документа с изгледа, зареждането без сривове при повреден или защитен с парола файл, както и в предоставянето на няколкото контроли, които правят четеца завършен: прелистване на страници, промяна на мащаба, напасване на страницата към прозореца.

Това ръководство разглежда сглобяването в реда, в който реално го изграждате. Всичко тук изобразява по една страница наведнъж, което е предпочитаният вариант за повечето работни процеси с документи. Ако имате нужда от страници, подредени в една непрекъснато превъртаща се колона, това е различно решение за оформление, разгледано в отделна статия, и не е обект на тази тема.

Свързване на TPdf към TPdfView

Поставете TPdf и TPdfView върху формата, след което кажете на изгледа кой документ да покаже. Това единично присвояване е цялата връзка между невизуалния документ и контролата, която го изчертава.

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

Преди нещо от това да заработи, оригиналната PDFium библиотека трябва да бъде налична на компютъра. PDFium VCL се обръща към pdfium32.dll или pdfium64.dll в зависимост от вашата целева платформа, и документът просто ще откаже да се отвори, ако DLL файлът не бъде намерен. Доставете съответния DLL до вашето изпълнимо приложение или го поставете там, където системният зареждащ модул ще го открие. Версиите с поддръжка на V8 съществуват само за PDF файлове с JavaScript, който искате да изпълните (нещо, което обикновеният четец не прави), така че използвайте стандартния DLL, освен ако нямате конкретна причина за обратното.

Зареждане на документ без сляпо доверие към входните данни

Първоначалният инстинкт е да обвиете зареждането в try/except блок и да третирате всяко изключение като неуспех. Този инстинкт тук е грешен, и ако го последвате, ще създадете четец, който изглежда добре, докато някой не му подаде повреден файл. Задаването на Active := True не предизвиква изключение при грешка в зареждането. PDFium VCL улавя вътрешната грешка и оставя Active със стойност False, така че единственият сигурен начин да разберете дали документът е отворен е да прочетете обратно стойността на свойството, след като сте го задали.

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

Две неща заслужават внимание. Първото е, че PageNumber съществува и в двата обекта и те са независими един от друг. Pdf.PageNumber е концепцията на документа за текуща страница; PdfView.PageNumber е страницата, която контролата реално показва, и това е стойността, която променяте, за да движите потребителя из файла. Настройването на едното не променя автоматично другото, така че четецът винаги управлява свойството на изгледа. Второто нещо е индексирането от 1: страниците започват от 1 до Pdf.PageCount, а не от 0, което може да изненада всеки, свикнал с масиви, започващи от нула.

Работа с шифрован файл

Шифрованите документи се вписват в същия път на зареждане. Ако паролата за отваряне е зададена преди активирането, документът се декриптира при отварянето си; ако тя е грешна или липсва, Active остава False, точно както при повреден файл. Така че възстановяването се състои в изискване на парола от потребителя и повторен опит за активиране.

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // must be set before Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

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

Прелистване на страници в документа

При отворен документ навигацията е просто аритметика върху PdfView.PageNumber, ограничена от Pdf.PageCount. Единствената реална работа е ограничаването на стойностите (clamping), така че бутоните никога да не изтласкат страницата извън диапазона, а бутоните за първа и последна страница да остават неактивни в двата края на файла.

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// the four navigation buttons reduce to one call each
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

Текстовото поле „отидÐ?на страница Nâ€?е същото извикване на GoToPage, захранвано с преобразувано цяло число, а ограничението покрива случая, при който потребителят въведе 9999 в документ от десет страници. Поддържайте UpdatePageLabel като единственото място, което изписва „Страница 3 от 12â€? за да не се разминава показанието с това, което изгледът реално показва.

Мащабиране: точни проценти и режими на напасване

Мащабирането в TPdfView се предлага под две взаимодействащи форми, и разбирането на това взаимодействие е разликата между контрола за мащаб, която се държи предвидимо, и такава, която се бори с потребителя. Директният начин е свойството Zoom â€?процент, при който 100 означава реален размер. Другият начин е FitMode, който казва на изгледа сам да изчисли мащаба и да го преизчислява динамично при оразмеряване на прозореца.

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

Ето частта, която обърква хората. Директното присвояване на стойност на Zoom нулира FitMode до pfmNone. Това е правилно поведение, а не бъг: в момента, в който потребителят избере точно 150%, изгледът вече не може да спазва режима „напасванÐ?по ширинаâ€? тъй като двете заявки си противоречат. Последицата за вашия потребителски интерфейс е, че бутонът за приближаване и бутонът за напасване на страницата са взаимно изключващи се състояния, и лентата с инструменти трябва да показва активния режим. Когато потребителят кликне върху напасване на страницата, задайте FitMode; когато избере числен мащаб, задайте Zoom и го оставете да изчисти режима на напасване сам.

Ако предпочитате сами да изчислявате стойността за напасване (например за да захраните плъзгач за мащабиране с текущия процент), помощните свойства за всяка страница ви дават тези числа, без да променят режима. PageWidthZoom[N], PageZoom[N], и ActualSizeZoom[N] връщат процента, който би напаснал страница N по ширина, изцяло или би я изобразил в реален размер.

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

Какво реално е необходимо за завършен четец

Първоначалното заглавие може да преувеличава обема на работата. Описаният по-горе четец е само няколко десетки реда и вече изпълнява задачите, необходими за работа с документи: отваряне на файл, справяне с повреден такъв, показване на страница, прелистване между страниците и промяна на мащаба ръчно или чрез автоматично напасване. PDFium върши сложната работа незабележимо. Вградените шрифтове се визуализират правилно, анотациите и полетата на формуляри се изчертават там, където ги поставя документът, а страницата, която виждате, съвпада с тази, която би видял потребител на Chrome, тъй като се използва един и същ двигател за изчертаване.

На тази база допълненията са по-скоро козметични, отколкото структурни. Селекцията на текст и търсенето четат от същия текстов слой, който PDFium вече изгражда; метаданни като Pdf.Title и Pdf.Author се достъпват с едно четене на свойство; ротацията и нюансите на сивото са опции за рендиране, които предавате при изчертаване на страницата върху растерно изображение. Нищо от това не променя основния скелет, който имате тук: обекта на документа, изгледа и потока зареждане-към-навигация, който ги свързва. Изградете правилно този скелет, а останалото е въпрос на декорация.

Компонентите TPdf и TPdfView, използвани тук, са част от PDFium VCL за Delphi и C++Builder, чиято продуктова страница съдържа пълната документация на четеца.