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

Рендеринг PDF-градиентов в Delphi: осевые и радиальные

Документы PDF, в заголовках которых используются градиентные фоны в стиле Word или Excel, при экспорте в изображения раньше часто возвращали пустой белый прямоугольник вместо плавного смешивания цветов. Рендерер страниц HotPDF теперь поддерживает оператор PDF sh (затенение, shading) для осевых (Тип 2) и радиальных (Тип 3) градиентов: он вычисляет значения функции затенения вдоль оси градиента и отрисовывает смешение, тогда как ранние версии библиотеки просто игнорировали этот оператор, оставляя область чисто белой. Поддерживаются оба типа затенений, покрывающих практически любые градиенты офисных документов; сеточные градиенты не поддерживаются, и в этой статье мы честно опишем границы возможностей

Почему градиент PDF отрисовывается в виде белого прямоугольника?

Градиент в PDF — это векторная конструкция, а не растровое изображение, которое рендерер может легко перенести на холст. Словарь затенения (ISO 32000-1 §8.7.4.5) описывает геометрию — ось или пару окружностей в массиве Coords — а также цветовую функцию, и оператор sh отрисовывает это затенение по всей текущей области отсечения, используя ту же графическую модель, которую HotPDF предоставляет при рисовании контуров и заливок на холсте страницы. Рендерер страниц HotPDF обходит поток содержимого оператор за оператором, и любой оператор, для которого нет кода обработки, пропускается, чтобы остальная часть страницы по-прежнему отрисовывалась. До версии 2.335.0 оператор sh входил в этот пропущенный набор: градиент заголовка или всей страницы никак не отображался на выходе, поэтому фоновый цвет отображался как белый. Файл при этом не был поврежден. У рендерера просто не было обработчика для этого конкретного оператора, а белый бокс — это именно то, как выглядит отсутствующий обработчик

Страничный рендерер HotPDF в Delphi идёт по контентному потоку оператор за оператором: необработанный оператор sh когда-то оставлял градиентный заголовок бумажно-белым, а художник затенений теперь рисует переход
До версии 2.335.0 HotPDF пропускал оператор sh и оставлял область белой как бумага; теперешний живописец затенений цепляется к тому же вызову RenderLoadedPageToBitmap и вычисляет смешение вдоль Coords

Цепочка вычисления функций, стоящая за затенением

Затенение не может быть отрисовано до вычисления его функции, поэтому этот механизм должен был появиться первым. Осевые затенения Типа 2 и радиальные затенения Типа 3 содержат запись /Function, которая сопоставляет параметрическое положение вдоль градиента с цветом (ISO 32000-1 §7.10), и вычислитель функций HotPDF определяет FunctionType и обрабатывает его. Экспоненциальная интерполяция Типа 2 (§7.10.3) вычисляет значение C0 + t^N * (C1 - C0) с ограничением диапазонов Domain и Range. Дискретизированные функции Типа 0 (§7.10.2) считывают сетку 8-, 16- или 32-битных значений и выбирают ближайшее через таблицу Encode — этот случай подробно рассмотрен в нашем обзоре дискретизированных цветовых таблиц Типа 0. Составные (stitching) функции Типа 3 (§7.10.4) выбирают подфункцию по массиву Bounds и выполняют рекурсивный вызов. Калькуляторные функции PostScript Типа 4 (§7.10.5) не вычисляются — они корректно возвращают ошибку вычисления вместо выдачи неверного цвета, что соответствует консервативной политике HotPDF на всем пути рендеринга. Этот вычислитель появился в версии 2.334.0, на один выпуск раньше самого оператора, поскольку отрисовщик затенений напрямую зависит от качества лежащей под ним функции

Как HotPDF преобразует непрерывный градиент в пиксели?

HotPDF аппроксимирует непрерывное смешение набором полос сплошного цвета. Отрисовщик затенений обходит ось градиента, разбивая ее на число полос до 64 (меньше, если ось коротка в пикселях устройства), вычисляет функцию в параметрическом положении каждой полосы и заполняет прямоугольник, перпендикулярный оси, сплошной кистью. Здесь соблюдается компромисс между количеством полос и плавностью: большее число полос точнее передает градиент, но требует большего числа операций заливки, и 64 полосы являются разумным пределом. Поскольку это количество также зависит от длины оси в пикселях устройства, разрешение рендеринга имеет значение. Отрисовка той же страницы с более высоким значением DPI удлиняет ось, поэтому короткий градиент получает больше полос, и его переходы смягчаются до достижения лимита

// Дискретизатор затенений использует до 64 цветовых полос вдоль градиента
// оси, но не больше, чем длина оси в пикселях устройства. Рендеринг
// той же страницы при более высоком DPI удлиняет ось, поэтому короткий градиент
// получает больше полос, и полосность смягчается до упора в предел 64 полос.
Thumb := Pdf.RenderLoadedPageToBitmap(PageIndex, 96);   // более грубые полосы
Proof := Pdf.RenderLoadedPageToBitmap(PageIndex, 300);  // до 64 полос

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

Общая точка входа осталась неизменной: RenderLoadedPageToBitmap принимает индекс страницы с 0 и разрешение DPI, возвращает управляемый вызывающей стороной объект TBitmap, а в случае ошибки возвращает nil вместо вызова исключения, поэтому поддержка затенений прозрачна для уже существующего кода. Страница, которая раньше рендерила градиентный заголовок пустым, теперь возвращает то же изображение, но с отрисованным смешением цветов, и для этого не требуется никаких изменений API. Важно помнить об освобождении ресурсов: вызывающая сторона сама удаляет возвращенный bitmap. Весь конвейер рендеринга — интерпретация потока команд, встроенные глифы и кэширование страниц — описан в руководстве по рендерингу страниц PDF в TBitmap в Delphi

var
  Pdf: THotPDF;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('quarterly-report.pdf') > 0 then
    begin
      // Страница 1 (с нуля) при 150 DPI. Шапка с градиентной заливкой, которая
      // раньше возвращалась пустой, теперь несёт своё цветовое смешение.
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 150);
      if Assigned(Bmp) then
      try
        Bmp.SaveToFile('page1.bmp');
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Простая проверка на отсутствие регрессии строится на основе того же вызова. Отрендерите страницу, проверьте пиксель в той области, где должен находиться градиент, и убедитесь, что он больше не является чисто белым фоном, к которому рендерер возвращался раньше — это простая проверка в одну строку, выявляющая тихий возврат к старому поведению бездействия

Bmp := Pdf.RenderLoadedPageToBitmap(0, 150);
if Assigned(Bmp) then
try
  // True, когда оператор sh нарисовал; False, если область вернулась к белому.
  HeaderPainted := Bmp.Canvas.Pixels[Bmp.Width div 2, 8] <> clWhite;
finally
  Bmp.Free;
end;

Что HotPDF не отрисовывает: сеточные градиенты и границы обрезки

Четыре типа сеточных (mesh) градиентов не поддерживаются, и этот факт стоит учитывать при подготовке документов. Свободные сетки треугольников Гуро (Тип 4), регулярные сетки Гуро (Тип 5), сетки Coons patch (Тип 6) и сетки tensor-product patch (Тип 7), определенные в спецификации ISO 32000-1 §8.7.4.5.5 - §8.7.4.5.8, не рендерятся. Страница, использующая их, все равно отрисуется, но данная область останется в исходном состоянии. Также стоит упомянуть еще два момента. Прямоугольники полос градиента покрывают полосу затенения полностью, а не обрезаются точно по текущему пути отсечения, поэтому градиент, маскируемый в округлую или неправильную форму, отрисовывается в виде прямоугольной полосы до тех пор, пока в будущих версиях не появится точное отсечение по контурам. HotPDF также предполагает, что результатом работы функции является цвет RGB или градации серого, поэтому затенение, созданное в CMYK или другом цветовом пространстве, аппроксимируется без точного управления цветом. Для документов, с которыми работает большинство разработчиков на Delphi и C++Builder (отчетов, счетов, выписок и презентаций), осевых и радиальных градиентов вполне достаточно для покрытия практически любых практических задач, а сложные сеточные дизайнерские макеты стоит рассматривать в режиме предварительного просмотра, а не точной цветопробы

Художник затенений HotPDF дискретизирует непрерывный осевой градиент в до 64 сплошных цветовых полос, вычисляемых вдоль Coords, причём отрисовка Delphi с более высоким DPI удлиняет ось устройства до потолка в 64 полосы
Непрерывное смешение превращается в серию сплошных прямоугольников поперёк оси градиента, а отрисовка той же страницы при более высоком DPI обрастает полосами, пока потолок не смягчит рябь

Рендерер страниц HotPDF, описанная здесь поддержка оператора sh и лежащий в его основе вычислитель функций Типов 0/2/3 поставляются в составе компонента HotPDF для Delphi и C++Builder — нативной VCL-библиотеки без внешних зависимостей от DLL, объединяющей создание, редактирование, извлечение текста и рендеринг страниц PDF в одном пакете