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

Создание средства просмотра PDF в Delphi с помощью компонента PDFium

Средство просмотра PDF в Delphi сводится к двум компонентам и связи между ними. TPdf владеет документом: он открывает файл, расшифровывает его и отвечает на вопросы о количестве страниц и метаданных. TPdfView — это визуальный элемент управления, который отрисовывает страницы на экране и управляет прокруткой, масштабированием и страницей, которую в данный момент просматривает пользователь. Компонент PDFium оборачивает тот же механизм рендеринга, который поставляется в 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 вызывает pdfium32.dll или pdfium64.dll в зависимости от целевой платформы, и документ просто отказывается открываться, если DLL не найдена. Поставляйте соответствующую DLL вместе с вашим исполняемым файлом или поместите ее туда, где ее найдет системный загрузчик. Сборки с поддержкой V8 существуют только для файлов PDF, содержащих JavaScript, который вы хотите выполнить, чего не делает обычное средство просмотра, поэтому используйте стандартную DLL, если у вас нет конкретных причин для обратного

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

Инстинкт подсказывает обернуть загрузку в try/except и рассматривать выброшенное исключение как ошибку. В данном случае этот инстинкт ошибочен, и если вы ошибетесь, получится средство просмотра, которое будет выглядеть нормально, пока кто-нибудь не передаст ему поврежденный файл. Установка Active := True не вызывает исключения при сбое загрузки. Компонент PDFium перехватывает внутреннюю ошибку и оставляет значение 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: страницы нумеруются от 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 для Delphi и C++Builder, который содержит полный справочник по средству просмотра на странице своего продукта