Technical Article

Векторная графика в PDF с помощью Delphi: контуры и градиенты

Большинство разработчиков на Delphi при работе с PDF воспринимают этот формат просто как контейнер для текста и растровых изображений. Такое представление верно лишь отчасти и оставляет неиспользованной наиболее мощную составляющую формата. Страница PDF представляет собой независимый от разрешения двухмерный холст, построенный на той же модели визуализации, что и PostScript. Он позволяет рисовать линии, кривые, закрашенные области, градиенты и повторяющиеся узоры в виде векторов, которые остаются четкими при любом масштабировании и печатаются в максимальном разрешении устройства. Если вы создаете логотип, диаграмму, водяной знак или рамку сертификата, векторный контур практически всегда является лучшим решением, занимая меньше места и обеспечивая более высокую четкость по сравнению с растровым изображением.

В этой статье рассматривается векторная модель в соответствии со стандартом ISO 32000-1 и показываются соответствующие вызовы в PDFlibPas. Наша цель состоит в том, чтобы сделать спецификацию более понятной на практических примерах, поскольку API тесно связан с ней, и освоение одного помогает понять другое.

Страница как генератор контуров

Раздел §8.5 стандарта ISO 32000-1 описывает работу с графикой в два независимых этапа. Сначала создается контур (path), представляющий собой чистую геометрию без визуального представления. Затем этот контур отрисовывается за одну операцию, которая обводит его границы, заполняет внутреннюю область или делает и то, и другое. Во время построения на странице ничего не отображается. Контур представляет собой абстрактную последовательность точек и сегментов, хранящуюся в графическом состоянии до тех пор, пока оператор отрисовки не применит ее, после чего она рендерится и удаляется.

Контур состоит из одного или нескольких подконтуров. Подконтур начинается в определенной точке и расширяется путем добавления сегментов: прямых линий, кубических кривых Безье, а на некоторых платформах и целых прямоугольников, добавляемых как собственные закрытые подконтуры. В PDFlibPas вы открываете контур с помощью StartPath, задавая начальную точку, а затем расширяете его методами AddLineToPath и AddCurveToPath. Каждый вызов перемещает текущую точку рисования, поэтому следующий сегмент начинается там, где закончился предыдущий. Метод ClosePath проводит прямую линию обратно к началу подконтура, что критически важно при обводке, так как создает плавное соединение линий в замыкающей вершине вместо двух отдельных стыков.

// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // straight segment back to (150, 100)
PDF.DrawPath(2);                   // 2 = fill and stroke; path is consumed

Для создания кривых используется метод AddCurveToPath, принимающий две контрольные точки Безье и конечную точку: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Кривая идет от текущей точки к точке (EndX, EndY), изгибаясь под влиянием двух контрольных точек. Круговые дуги создаются с помощью AddArcToPath(CenterX, CenterY, TotalAngle), где радиус определяется расстоянием между текущей точкой и центром, а движок преобразует дугу в цепочку сегментов Безье. Для прямоугольников предусмотрен быстрый метод AddBoxToPath(Left, Top, Width, Height), добавляющий замкнутый прямоугольный подконтур без предварительного вызова StartPath.

Два правила заливки и причины их различий

При заполнении контура, который пересекает сам себя или содержит внутренние циклы, рендереру требуется правило для определения того, какие области лежат внутри фигуры, а какие являются отверстиями. Раздел §8.5.3.3 стандарта ISO 32000-1 определяет два таких правила, и они могут по-разному окрашивать одну и ту же геометрию. Правило ненулевого индекса (nonzero winding rule) подсчитывает знаки пересечений луча, проведенного из тестовой точки в бесконечность, добавляя единицу при пересечении слева направо и вычитая единицу при обратном направлении; точка считается внутренней, если сумма не равна нулю. Правило четности (even-odd rule) игнорирует направление линий и просто считает число пересечений: точка признается внутренней, если это число нечетно.

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

PDFlibPas выбирает правило заливки по используемому методу отрисовки, а не по флагу. Метод DrawPath выполняет заливку по правилу ненулевого индекса, а метод DrawPathEvenOdd задействует правило четности. Оба метода принимают одинаковый целочисленный режим: 0 выполняет только обводку, 1 - только заливку, а 2 - и заливку, и обводку. Правило четности является более простым инструментом для создания сквозных отверстий именно потому, что вам не нужно следить за направлением подконтуров.

// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120);   // outer
PDF.AddBoxToPath(140, 130, 120,  60);   // inner
PDF.DrawPath(1);                         // 1 = fill, nonzero winding

// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120);   // outer
PDF.AddBoxToPath(140, 330, 120,  60);   // inner cut-out
PDF.DrawPathEvenOdd(1);                  // 1 = fill, even-odd

Осевые градиенты: изменение цвета вдоль линии

Сплошная заливка наносит один цвет на всю область. Градиент же плавно меняет цвет, и самым простым его типом является осевой (линейный) градиент. Раздел §8.7.4.5 стандарта ISO 32000-1 описывает его как осевое затенение типа 2 (Type 2 axial shading): вы указываете две точки, определяющие ось, начальный цвет в первой точке и конечный цвет во второй, а рендерер интерполирует цвет вдоль этой оси. Каждая точка в закрашенной области принимает цвет своей перпендикулярной проекции на ось, поэтому градиент распространяется полосами под прямым углом к линии между этими точками.

В PDFlibPas градиент является именованным ресурсом документа, который создается один раз, а затем выбирается для рисования. Для его регистрации служит функция NewRGBAxialShader. Сигнатура вызова: NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend), где указываются две конечные точки оси, тройки значений RGB для каждого конца в диапазоне от 0 до 1 и флаг Extend. Если флаг Extend установлен в 1, крайние цвета продолжают заполнять область за пределами конечных точек оси, что обычно и требуется, чтобы углы области вне оси не оставались незакрашенными; значение 0 оставляет их пустыми. После создания шейдера вы связываете его с помощью SetFillShader для заливки областей, SetLineShader для обводки или SetTextShader для текста. Эта привязка остается активной для последующих вызовов рисования, поэтому следующий контур закрашивается градиентом вместо сплошного цвета.

// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // start point and start RGB
  0, 260,   1.00, 1.00, 1.00,    // end point and end RGB
  1);                            // 1 = extend ends as solid color

// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill, now filled by the shader

Повторяющиеся узоры: заполнение ячейками

Если градиент плавно изменяет один цвет, то повторяющийся узор (tiling pattern) дублирует небольшой графический элемент по всей области. Раздел §8.7.3.1 стандарта ISO 32000-1 определяет его как ячейку узора, независимый элемент содержимого, который рендерер дублирует по фиксированной сетке для заполнения области. Таким способом создается штриховка для чертежей, повторяющийся фирменный знак под заголовком или текстурный фон, который сохраняет векторную четкость и практически ничего не весит независимо от размера области, поскольку ячейка сохраняется один раз и просто тиражируется.

PDFlibPas создает ячейку узора из захваченного содержимого страницы. Вы захватываете страницу или область с помощью CapturePage, преобразуете ее в именованный узор вызовом NewTilingPatternFromCapturedPage(PatternName, CaptureID), а затем выбираете этот узор для текущей заливки с помощью SetFillTilingPattern(PatternName). С этого момента любой заполняемый контур покрывается повторяющейся ячейкой вместо сплошного цвета, по аналогии с шейдером, но используя в качестве источника ячейку узора. Эта последовательность сложнее обычного вызова, поэтому, если шаг захвата вызывает вопросы, воспринимайте создание узора как двухэтапную операцию: сначала подготовьте ячейку, а затем свяжите ее по имени перед рисованием области.

Объединение графических примитивов

Графические элементы легко объединяются. Закрашенная фигура Безье представляет собой контур из кривых, отрисованный с помощью DrawPath. Тот же контур, отрисованный через DrawPathEvenOdd после добавления внутреннего контура, демонстрирует отверстие, которое при обычном правиле было бы закрашено. Прямоугольник с градиентной заливкой привязывается к шейдеру. Приведенный ниже пример последовательно рисует все три варианты, чтобы разница между правилами заливки была наглядно видна на одной странице, а затем добавляет под ними панель с градиентом.

// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480);   // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480);   // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1);                                     // 1 = fill

// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300);                              // new subpath: the hole
PDF.AddArcToPath(200, 300, 360);                     // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1);                              // hole is punched out

// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
  60, 100,  0.95, 0.55, 0.10,
  60, 200,  0.20, 0.10, 0.40,
  1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);

Стоит обратить внимание на две детали. Выбор правила заливки определяется вызываемым методом, поэтому выбор между DrawPath и DrawPathEvenOdd означает выбор между правилом ненулевого индекса и правилом четности. При работе с фигурами, содержащими отверстия, правило четности избавляет от необходимости отслеживать направление подконтуров. Графическое состояние фиксируется в момент отрисовки: задавайте цвета, толщину линий и шейдеры перед вызовом рисования, так как именно эти параметры считывает движок. Сначала конструируйте, затем настраивайте состояние и только потом рисуйте, и тогда векторная модель всегда будет работать предсказуемо.

Логичным продолжением темы станет чтение векторов и текста из существующих документов, о чем рассказывается в нашей статье об извлечении текста, изображений и шрифтов, а также рендеринг векторной графики в контекст устройства Windows для предпросмотра и печати, описанный в руководстве по печати и предпросмотру. Описанные методы работы с контурами, шейдерами и узорами входят в состав библиотеки Delphi PDF Library наряду с API для работы с текстом, изображениями, формами и цифровыми подписями.