Technical Article

Векторна графіка в PDF з Delphi: контури та градієнти

Більшість коду Delphi, який працює з PDF, розглядає цей формат просто як контейнер для двох речей: фрагментів тексту та кількох розміщених растрових зображень. Ця точка зору є вірною лише частково, і вона залишає невикористаною найбільш функціональну частину формату. Сторінка PDF — це незалежне від роздільної здатності 2D-полотно, побудоване на тій самій моделі зображення, що й 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

Осьові градієнти змінюють колір уздовж лінії

Однотонний колір заливки має однакове значення по всій області. Градієнт змінює колір безперервно, і найпростішим його різновидом є осьовий, або лінійний, градієнт. Стандарт ISO 32000-1 §8.7.4.5 визначає його як осьове затінення типу 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

Вісь тут є вертикальною, від y=100 до y=260 при фіксованому x, тому смуги кольору йдуть горизонтально, а прямокутник змінює колір від синього в основі до білого вгорі. Оскільки шейдер ідентифікується за назвою, одне визначення може заповнювати будь-яку кількість фігур на сторінці, а перехід назад до плоского кольору — це просто ще один виклик SetFillColor перед наступним контуром.

Плиткові візерунки повторюють комірку

У той час як градієнт плавно змінює один колір, візерунок плитки (tiling pattern) повторює невеликий фрагмент зображення по всій області. Стандарт ISO 32000-1 §8.7.3.1 визначає візерунок плитки як комірку візерунка (pattern cell) — незалежний елемент вмісту, який візуалізатор дублює на фіксованій сітці для покриття зони малювання. Саме так ви створюєте штриховку для інженерного заповнення, повторюваний фірмовий мотив за заголовком або текстуроване тло, яке залишається векторно чітким і майже нічого не важить, незалежно від розміру області, оскільки комірка зберігається один раз і використовується як посилання всюди.

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 для попереднього перегляду на екрані та друку, про що йдеться в інструкції з друку та попереднього перегляду. Виклики контурів, шейдерів та візерунків, описані тут, постачаються як частина бібліотеки PDF для Delphi разом з API тексту, зображень, форм та підписів, які описані в інших статтях цього блогу.