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

Рендеринг PDF-страниц в Bitmap в Delphi с помощью HotPDF

HotPDF отрисовывает загруженную страницу PDF в Delphi-класс TBitmap с помощью одного вызова: RenderLoadedPageToBitmap(PageIndex, DPI). Функция интерпретирует поток содержимого страницы и возвращает принадлежащий вызывающей стороне 24-битный RGB-растр в выбранном вами разрешении. Это именно то, что требуется для панели эскизов, предварительного просмотра или конвейера экспорта PDF в изображения. В этой статье рассматривается API, а также часть, отличающая полноценный рендерер от простой игрушки — отрисовка текста непосредственно на основе программ встроенных шрифтов, а не похожих системных аналогов

Почему рендеринг PDF-страницы сложнее, чем вывод изображения?

Страница PDF — это не рисунок. Это программа: поток операторов, которые строят контуры, выбирают шрифты, задают цвета и размещают глифы в соответствии с графической моделью, определенной стандартом ISO 32000-1 §8. В самом файле нет информации о том, как выглядит конкретный пиксель. Чтобы создать растровое изображение, необходимо выполнить эту программу: поддерживать текущую матрицу преобразования, стек состояний графики для операторов q/Q, контур отсечения, а также цветовые пространства заливки и обводки — и растрировать результат. Вот почему простая задача «показать страницу 3 как картинку» на самом деле требует интерпретатора потока контента, а не просто конвертации формата файлов

Рендерер HotPDF, представленный в версии v2.253.0, состоит из шести независимых модулей, отражающих эту модель: ядра аффинных матриц для алгебры преобразований PDF [a b c d e f], стека графических состояний, распознавателя цветовых пространств (DeviceRGB, DeviceGray, DeviceCMYK, Indexed), построителя контуров, связывающего операторы контуров PDF с GDI, слоя метрик шрифтов, считывающего массивы /Widths для точного позиционирования, и интерпретатора, распределяющего операторы и управляющего остальными пятью модулями. Объекты изображений XObjects проходят через тот же стек декодирования, который библиотека использует для извлечения. Поэтому любой фильтр изображений, который HotPDF декодирует при извлечении (включая изображения JPEG 2000 со сжатием JPXDecode), также поддерживается при отрисовке

Рендеринг загруженной страницы в TBitmap

Метод RenderLoadedPageToBitmap принимает отсчитываемый от нуля индекс страницы и значение DPI, где 72 DPI сопоставляют одну единицу пользовательского пространства PDF с одним пикселем. Функция возвращает nil в случае сбоя (индекс за пределами диапазона, отсутствие ресурсов) вместо генерации исключения, поэтому средство просмотра может просто пропустить некорректную страницу и продолжить работу. Вызывающая сторона владеет возвращенным растровым изображением и должна освободить его

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('report.pdf') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);  // страница 1 при 144 DPI
      if Bmp <> nil then
      try
        Image1.Picture.Assign(Bmp);
      finally
        Bmp.Free;  // вызывающая сторона владеет bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Аргумент DPI выполняет масштабирование для любого стандартного сценария. Панель эскизов отрисовывается при 36 или 48 DPI, создавая небольшие растры с быстрой обработкой; экранный просмотр при 96 или 144 DPI соответствует типичной плотности дисплеев; экспорт при 300 DPI дает изображения печатного качества. Поворот страниц из записи /Rotate и инверсия координат /MediaBox (в PDF начало координат находится слева внизу, а в GDI — слева вверху) обрабатываются внутри матрицы перевода координат страницы на устройство, поэтому страница формата US Letter при 72 DPI возвращается в виде изображения размером ровно 612×792 пикселей в правильной ориентации

Почему на эскизах страниц PDF отображаются неверные глифы?

Отображение неверных или приблизительных глифов при рендеринге PDF почти всегда указывает на то, что рендерер подменяет шрифт системным аналогом вместо использования шрифта, внедренного в файл. Первая версия рендерера HotPDF делала именно это: удаляла префикс подмножества из /BaseFont (превращая ABCDEF+Arial в Arial), запрашивала у GDI системный шрифт с этим именем и выводила текст с его помощью. Для документа, использующего Arial или Times New Roman со стандартной кодировкой, результат выглядит близко к оригиналу. Однако это лишь приближение, которое дает сбои в ряде характерных ситуаций

Худшим случаем являются внедренные подмножества шрифтов. Такое подмножество может содержать всего сорок глифов, реально используемых в документе, причем коды символов назначаются в произвольном порядке, внутреннем для этого файла: например, код 1 может быть буквой «T», код 2 — «h» и так далее. Системный шрифт ничего не знает об этом внутреннем распределении, поэтому текст либо исчезает, либо выводится в виде неверных символов. Пользовательские кодировки, символьные шрифты, шрифты штрихкодов и любые гарнитуры, не установленные на рабочей машине, дают аналогичные сбои. Рендерер, ограничивающийся подменой системными шрифтами, создает эскизы, отдаленно напоминающие страницу — ровно до тех пор, пока на ней не встретятся шрифты, которые изначально требовали внедрения

Рендеринг внедренных глифов: отрисовка на основе программы шрифта

Разработчики HotPDF устранили этот пробел за пять релизов (с версии v2.268.0 по v2.272.0), реализовав разбор внедренных программ шрифтов и отрисовку их контуров в виде заполненных векторных контуров GDI. Текст на отрисованной странице формируется на основе тех же данных о контурах, которые использует любой стандартный просмотрщик. Это гарантирует точное отображение подмножеств шрифтов, пользовательских кодировок и неустановленных гарнитур. Поддержка расширялась по типам шрифтов:

Для шрифтов Type0/CIDFontType2 с внедренной программой TrueType (FontFile2) рендерер разбирает таблицы glyf and loca напрямую: квадратичные контуры преобразуются в кубические кривые Безье, понятные GDI, восстанавливаются неявные опорные точки между соседними контрольными точками вне кривой, а составные глифы обрабатываются рекурсивно. Поддерживаются как разметки Identity, так и явные потоки CIDToGIDMap, а шаги CID учитывают записи ширины /W и /DW, обеспечивая правильное позиционирование двухбайтового текста Identity-H

Программы CFF (FontFile3, будь то CIDFontType0C, Type1C или обертка OpenType) получают полноценный интерпретатор символьных строк Type 2: линии, кривые, семейство flex, маски хинтинга, а также локальные и глобальные вызовы подпрограмм с правильным смещением. CID-ключевые программы CFF сопоставляют коды символов через кодировку шрифта, что важно для подмножеств шрифтов, порядок глифов в которых отличается от порядка CID, при этом учитывается поглифовый выбор словарей шрифтов через FDArray/FDSelect. Простые (не CID) шрифты TrueType сопоставляют однобайтовые коды через собственную таблицу cmap внедренного шрифта с надежной цепочкой субтаблиц: сначала форматы Unicode 4 и 12, затем таблицы символов с зеркалом приватной области F000, затем устаревшие форматы Macintosh. В то же время простые шрифты Type1 используют встроенную кодировку программы CFF

Два улучшения дополняют картину. Во-первых, словари /Encoding простых шрифтов разрешаются согласно приоритету, предписанному ISO 32000-1 §9.6.6: массивы /Differences переопределяют базовую кодировку, которая, в свою очередь, переопределяет собственную карту программы шрифта. На этот путь опираются цепочки инструментов TeX и PostScript, где имена глифов разрешаются через Adobe Glyph List, кодировку CFF или TrueType cmap. Во-вторых, шрифты Type3, глифы которых представляют собой небольшие потоки контента, обрабатываются через рендерер с компоновкой матрицы шрифта, размера шрифта и текстовой матрицы. Ширина в пространстве глифов /Widths интерпретируется через /FontMatrix, как требует стандарт ISO 32000-1 §9.6.5, а процедуры глифов, объявляющие ограничивающую рамку d1, обрезаются по ней (благодаря чему поврежденный глиф штрихкода не может выйти за пределы своей ячейки). Если код не удается сопоставить (поврежденная программа, неназначенный символ), рендерер использует системный шрифт для отрисовки этого глифа вместо пропуска всего текстового блока

Как ускорить повторный рендеринг?

Ответом, предлагаемым HotPDF, является кэш наиболее часто используемых страниц (MRU): метод RenderLoadedPageToBitmapCached сохраняет до RenderCacheCapacity отрисованных страниц (по умолчанию 8), индексируемых по номеру страницы и DPI, а попадание в кэш возвращает новую копию растра без повторного разбора потока контента. Это работает в тысячи раз быстрее переинтерпретации страницы. Такой шаблон идеально подходит для средств просмотра: переход пользователя между страницами или изменение масштаба с повторным запросом той же страницы в том же разрешении DPI всегда будут использовать кэш

// Панель эскизов: первый проход отрисовывает, прокрутка назад использует кэш
for I := 0 to ThumbCount - 1 do
begin
  Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 48);
  if Bmp <> nil then
  try
    ThumbList.AddThumbnail(I, Bmp);
  finally
    Bmp.Free;
  end;
end;

// После редактирования загруженной страницы на месте:
Pdf.InvalidateRenderedPageCache;  // следующий рендеринг отразит изменения

Оценивайте затраты памяти перед увеличением емкости кэша. Страница формата US Letter при 300 DPI содержит 2550×3300 пикселей, что занимает около 25 МБ в виде 24-битного растра. Таким образом, восемь кэшированных страниц в разрешении экспорта потребуют около 200 МБ. При разрешении эскизов те же восемь записей займут менее мегабайта. Настраивайте RenderCacheCapacity под разрешение, в котором вы реально работаете, и вызывайте InvalidateRenderedPageCache после любого локального изменения — кэш привязан только к странице и DPI, и он не может отследить изменения базового контента. Загрузка нового документа очищает его автоматически

Под кэшем страниц работает второй уровень кэширования: декодированные изображения XObjects хранятся в буфере, ограниченном объемом ImageCacheMaxBytes (по умолчанию 32 МБ), с вытеснением наименее используемых объектов (LRU). Логотип или изображение бланка, повторяющиеся на каждой странице, декодируются один раз за всё время загрузки документа, а не при каждом операторе Do. Это примерно вдвое сокращает время рендеринга страниц с общими изображениями и ускоряет экспорт многостраничных TIFF. Метод InvalidateRenderedPageCache также очищает этот кэш

Что по-прежнему отрисовывается приблизительно

Рендерер ориентирован на стандартное подмножество элементов PDF, и важно знать его ограничения. Цветовые пространства CalRGB, Lab и на основе ICC обрабатываются приближенно без полноценного управления цветом — аппаратные цветовые пространства, индексированные палитры и выборки цветов функций типа 0 поддерживаются, но файл допечатной подготовки, опирающийся на профили рендеринга ICC, не будет колориметрически точным. Шаблоны затенения (sh) и режимы наложения, отличные от простой прозрачности, также не поддерживаются, а рекурсия Form XObject ограничена по глубине для предотвращения зацикливания. Для счетов, отчетов, договоров и форм (страниц с текстом, контурами и изображениями) вывод абсолютно точен; для дизайн-макетов с градиентами и группами прозрачности растровое изображение следует рассматривать как превью, а не как финальную пробу

Практический вывод: если ваш конвейер генерирует документы с помощью HotPDF или обрабатывает стандартные деловые PDF-файлы, метод RenderLoadedPageToBitmap гарантирует их точное воспроизведение с сохранением контуров встроенных глифов, правильных шагов CID и геометрии страниц. Приближения затрагивают лишь те редкие области графической модели, которые практически не встречаются в бизнес-документах

Метод RenderLoadedPageToBitmap, его кэшированный вариант и описанный здесь конвейер рендеринга встроенных глифов поставляются в составе компонента HotPDF Component для Delphi и C++Builder — нативной библиотеки VCL без внешних DLL-зависимостей, объединяющей функции создания, редактирования, извлечения текста и рендеринга страниц PDF в одном пакете