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

Изграждане на PDF четец в Delphi с PDFium Component

PDF четецът в Delphi се свежда до два компонента и връзката между тях. TPdf притежава документа: той отваря файла, декриптира го и отговаря на въпроси относно броя на страниците и метаданните. TPdfView е визуалната контрола, която рисува страниците на екрана и се справя с превъртането, мащабирането и страницата, която потребителят в момента разглежда. PDFium Component обвива същия енджин за рендиране, който се доставя вътре в 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 Component извиква pdfium32.dll или pdfium64.dll в зависимост от вашата целева платформа, и документът просто отказва да се отвори, ако DLL-ът не може да бъде намерен. Доставете съответстващия DLL до вашия изпълним файл или го поставете там, където системният товарач (system loader) ще го намери. Компилациите с активиран V8 съществуват само за PDF файлове, които носят JavaScript, който искате да изпълните, което обикновена програма за преглед не прави, така че посегнете към стандартния DLL, освен ако нямате конкретна причина да не го направите

Зареждане на документ без доверяване на входа

Инстинктът е да се обвие зареждането в try/except и хвърленото изключение да се третира като провал. Този инстинкт е грешен тук, и грешката в него създава програма за преглед, която изглежда добре, докато някой не й подаде счупен файл. Задаването на Active := True не повдига изключение при неуспешно зареждане. PDFium Component улавя вътрешната грешка и оставя 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, захранено от анализирано цяло число, а ограничението (clamp) покрива случая, когато потребителят напише 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 са на едно четене на свойство разстояние; ротацията и степените на сивото са опции за рендиране, които подавате, когато рисувате страница в растерна графика (bitmap). Никое от тези не променя гръбнака, който имате тук, а именно обекта на документа, изгледа и потока „зареждане-след-това-навигация“, който ги свързва. Направете този гръбнак правилно и останалото е декорация

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