Одна страница формата A4, отрисованная с комфортным для чтения масштабом, занимает несколько мегабайт в виде 32-битного растрового изображения (bitmap). Умножьте это на 400-страничный договор, и вычисления перестанут быть абстрактными: если рендерить все страницы сразу, вашему приложению потребуется более гигабайта памяти для растров, хотя пользователь видит на экране лишь малую часть. Приложение либо исчерпает адресное пространство в 32-битной сборке, либо зависнет на несколько секунд при запуске, пока GPU и парсер обрабатывают страницы, до которых пользователь еще даже не докрутил. Просмотрщик с непрерывной прокруткой должен выглядеть как одна длинная лента страниц, но он не может держать их все в памяти одновременно
Эта проблема является ключевой. PDFium Component решает ее внутри элемента управления TPdfView, поэтому основная задача разработчика сводится к выбору правильного режима отображения и пониманию того, что компонент делает автоматически. Та часть работы, которую он не делает за вас (расчет размеров страниц и поддержание плавности прокрутки), требует написания небольшого объема кода. Если вы все еще проектируете внешние элементы управления (панель инструментов, эскизы страниц, форму поиска), ознакомьтесь со статьей о создании полнофункционального PDF-просмотрщика; здесь же мы сосредоточимся непосредственно на прокрутке
Разметка — это режим отображения, а не панель растровых изображений
При разработке под VCL первой реакцией может быть желание взять элемент scroll box и разместить в нем компоненты Image (по одному на страницу). Не делайте этого. Такой дизайн вынуждает вас самостоятельно отвечать за позиционирование страниц, расчет прокрутки и управление памятью, и все эти задачи будут решены неоптимально. Компонент 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 выполняет рендеринг частями и проверяет токен отмены на границе каждой части. Благодаря этому текущая отрисовка страницы, которая только что покинула экран, может быть остановлена вместо выполнения до самого конца
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. В цикле прокрутки, выделяющем bitmap под каждую страницу, забытый вызов освобождения памяти приведет к утечке, растущей по мере прокрутки страниц, что перечеркнет всю идею оптимизации памяти. Старайтесь по возможности выполнять рендеринг в один и тот же повторно используемый объект растра
Резюме
Создание просмотра с непрерывной прокруткой большей частью берет на себя сам компонент. Вы выбираете режим dmSingleContinuous для разметки, устанавливаете pfmFitWidth для автоматической адаптации к ширине окна и проверяете Pdf.Active для корректной обработки ошибок. Единственная часть, которую имеет смысл реализовать самостоятельно — это отменяемый рендеринг, поскольку работа просмотрщика оценивается пользователем именно по отзывчивости интерфейса, когда он быстро перетаскивает ползунок прокрутки вниз. Все остальные функции (выделение текста на разных страницах, подсветка поиска, дерево закладок) представляют собой работу над интерфейсом, строящуюся поверх этого слоя прокрутки
Описанные здесь интерфейсы TPdfView, DisplayMode и RenderPageProgressive поставляются в составе компонента PDFium Component для Delphi и Lazarus