Технічна стаття

Побудова переглядача PDF у Delphi з PDFium Component

Переглядач PDF у Delphi зводиться до двох компонентів і проводки між ними. TPdf володіє документом: він відкриває файл, розшифровує його й відповідає на питання про кількість сторінок і метадані. TPdfView є візуальним контролом, який малює сторінки на екрані й опікується прокруткою, масштабом і тією сторінкою, на яку користувач наразі дивиться. PDFium Component обгортає той самий рушій рендерингу, що постачається всередині Chrome, тож гліфи, згладжування та колір, які ви отримуєте на канві, збігаються з тим, що ваші користувачі вже бачать у своєму браузері. Робота не в рендерингу. Вона в тому, щоб з'єднати об'єкт документа з переглядом, завантажити файл, не впавши на пошкодженому чи захищеному паролем, і дати користувачеві ту жменьку контролів, яка робить переглядач завершеним: перегорнути сторінку, змінити масштаб, вписати сторінку у вікно

Далі йде це складання в тому порядку, у якому ви його справді будуєте. Усе тут рендерить одну сторінку за раз, а це те, чого хоче більшість документних робочих процесів. Якщо вам потрібні сторінки, складені в одну безперервно прокручувану колонку, це інше рішення щодо компонування й не той шлях, що тут

Проводка TPdf до TPdfView

Киньте TPdf та TPdfView на форму, а потім скажіть перегляду, який документ показувати. Це єдине присвоєння і є всім зв'язком між невізуальним документом та контролом, який його малює

Архітектура переглядача PDF у Delphi, де TPdf володіє документом, TPdfView його малює, а одне присвоєння властивості з'єднує їх поверх DLL PDFium
TPdf володіє документом, тоді як TPdfView його малює, а одне присвоєння з'єднує ці двоє поверх спільного рушія PDFium
procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf і PdfView кинуто на форму на етапі проєктування.
  PdfView.Pdf := Pdf;                 // перегляд малює те, що містить цей документ
  PdfView.FitMode := pfmFitWidth;     // починаємо з розумного масштабу
end;

Перш ніж усе це запуститься, нативна бібліотека PDFium має бути на машині. PDFium Component звертається до pdfium32.dll чи pdfium64.dll залежно від вашої цільової платформи, і документ просто відмовляється відкриватися, якщо DLL не знайдено. Постачайте відповідну DLL поряд із виконуваним файлом або покладіть її туди, де її знайде системний завантажувач. Збірки з увімкненим V8 існують лише для тих PDF, що несуть JavaScript, який ви хочете виконати, а звичайному переглядачеві це не потрібно, тож беріть стандартну DLL, доки у вас не буде конкретної причини вчинити інакше

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

Інстинкт підказує обгорнути завантаження в try/except і трактувати кинутий виняток як невдачу. Цей інстинкт тут хибний, і помилка на цьому дає переглядач, який виглядає добре, доки хтось не подасть йому зламаний файл. Присвоєння Active := True не піднімає винятку при невдалому завантаженні. PDFium Component ловить внутрішню помилку й лишає Active у стані False, тож єдиний чесний спосіб дізнатися, чи документ відкрився, - прочитати властивість назад після того, як ви її задали

Схема рішень при завантаженні у переглядачі PDFium на Delphi, де присвоєння Active ніколи не піднімає винятку, мовчазне false означає хибний пароль або пошкоджений файл, а далі йде одна повторна спроба з паролем
Активація ніколи не піднімає винятку при невдачі, тож переглядач читає Active назад і відповідає на мовчазне false однією повторною спробою з паролем
procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // ніколи не піднімає; невдача лишає Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // перегляд веде власну поточну сторінку
  UpdatePageLabel;
end;

Дві речі заслуговують на увагу. Перша полягає в тому, що PageNumber є в обох об'єктів і ці двоє незалежні. Pdf.PageNumber є уявленням документа про поточну сторінку; PdfView.PageNumber є тією сторінкою, яку контрол фактично показує, і саме її ви задаєте, щоб рухати користувача файлом. Задання однієї не рухає іншої, тож переглядач завжди керує властивістю перегляду. Друга - це індексація з одиниці: сторінки йдуть від 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;       // має бути задано до 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. Єдина справжня робота - це затиск, щоб кнопки ніколи не виштовхували сторінку за межі діапазону, а кнопки першої й останньої лишалися вимкненими на кінцях файлу

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;

// чотири кнопки навігації зводяться до одного виклику кожна
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, який каже перегляду обчислити масштаб за вас і перераховувати його, коли вікно змінює розмір

Взаємодія Zoom та FitMode у переглядачі PDFium на Delphi, де присвоєння точного Zoom скидає FitMode у pfmNone, а вибір режиму вписування повертає обчислення масштабу перегляду
Присвоєння точного масштабу скидає режим вписування, а вибір режиму вписування повертає обчислення масштабу перегляду
// фіксовані збільшення
PdfView.Zoom := 100;     // справжній розмір
PdfView.Zoom := 50;      // половина
PdfView.Zoom := 200;     // подвійний

// хай перегляд припасує сторінку до вікна й тримає її припасованою при зміні розміру
PdfView.FitMode := pfmFitWidth;   // ширина сторінки заповнює контрол
PdfView.FitMode := pfmFitPage;    // видно всю сторінку
PdfView.FitMode := pfmActualSize; // 1:1 з пунктами документа

Ось та частина, яка спотикає людей. Пряме присвоєння Zoom скидає FitMode у pfmNone. Це правильна поведінка, а не вада: тієї миті, коли користувач обирає точні 150%, перегляд більше не може одночасно шанувати "вписати за шириною", бо ці два запити конфліктують. Наслідком для вашого інтерфейсу є те, що кнопка збільшення та кнопка вписування сторінки є взаємовиключними станами, і панель інструментів має робити активний режим видимим. Коли користувач клацає вписати сторінку, задавайте FitMode; коли він клацає числовий масштаб, задавайте Zoom і дайте йому самому скинути режим вписування

Якщо ви радше обчислите значення вписування самі, скажімо щоб засіяти повзунок масштабу поточним відсотком вписування, посторінкові помічники дадуть вам числа, не змінюючи режиму. PageWidthZoom[N], PageZoom[N] та ActualSizeZoom[N] повертають той відсоток, який вписав би сторінку N за шириною, вписав би її цілком або відрендерив би її у справжньому розмірі

// засіваємо показник масштабу значенням вписування за шириною для поточної сторінки
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 Component для Delphi та C++Builder, чия сторінка продукту містить повний довідник переглядача