Технічна стаття

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

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

У цій статті ми розглянемо векторну модель у тому вигляді, в якому її визначає стандарт ISO 32000-1, і покажемо відповідні виклики PDF Library for Delphi. Мета полягає в тому, щоб зробити специфікацію конкретною, оскільки API тісно пов'язаний з нею, і розуміння одного допомагає розібратися в іншому

Сторінка — це механізм обробки контурів

Параграф §8.5 стандарту ISO 32000-1 описує графіку у дві фази, які ніколи не перетинаються. Спочатку ви будуєте контур (path), який є чистою геометрією без видимого результату. Потім ви зафарбовуєте цей контур однією операцією, яка обводить його межі, заповнює його внутрішню частину або робить і те, і інше. Під час побудови на сторінці нічого не з'являється. Контур — це абстрактна послідовність точок і сегментів, що зберігається в графічному стані, доки оператор малювання не використає її, після чого вона відтворюється на екрані та видаляється

PDF Library for Delphi: діаграма життєвого циклу PDF-шляху: побудова як невидима чиста геометрія, за нею оператор малювання, що обводить, заповнює чи обидва, потім споживає і відкидає шлях
Побудова чотирикутника нічого не малює — виклик DrawPath зчитує графічний стан один раз, рендерить обведення, заливку або те й те і викидає контур

Контур складається з одного або кількох підконтурів. Підконтур починається в певній точці та росте шляхом додавання сегментів: прямих ліній, кубічних кривих Безьє, а на деяких платформах — цілих прямокутників, що додаються як власні закриті підконтури. У PDF Library for Delphi ви відкриваєте контур за допомогою StartPath, який встановлює початкову точку, а потім розширюєте його за допомогою AddLineToPath та AddCurveToPath. Кожен виклик переміщує неявну поточну точку, тому наступний сегмент продовжується там, де закінчився попередній. ClosePath малює фінальний прямий сегмент назад до початку підконтуру, що важливо для обведення ліній, оскільки це створює реальне з'єднання ліній у кінцевій вершині замість двох роз'єднаних кінців

// Замкнений чотирикутник, спочатку обведений, потім залитий
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // відкрити контур у першій вершині
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // прямий сегмент назад до (150, 100)
PDF.DrawPath(2);                   // 2 = заливання й обведення; контур використано

Криві використовують метод 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) ігнорує напрямок і просто рахує кількість перетинів, вважаючи точку внутрішньою, якщо ця кількість є непарною

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

PDF Library for Delphi вибирає правило за тим, який виклик для малювання ви робите, а не за допомогою прапорця. DrawPath заповнює за правилом ненульового числа обходів; DrawPathEvenOdd заповнює за правилом парного-непарного. Обидва методи приймають один і той самий цілочисельний режим: 0 лише обводить контур, 1 лише заповнює, а 2 заповнює та обводить. Правило парного-непарного є простішим інструментом для створення вирізаних отворів саме тому, що воно не вимагає від вас контролю напрямку підконтурів

PDF Library for Delphi: вкладені прямокутники, заповнені за правилами even-odd і ненульового намотування: even-odd вирубує внутрішній прямокутник як дірку, тоді як ненульове намотування заповнює суцільно, хіба що внутрішня петля намотується проти зовнішньої
Та сама геометрія шайби за двома легальними відповідями — DrawPathEvenOdd ігнорує напрям обведення і завжди видовбує дірку, тоді як DrawPath додає знакові обведення, які підсилюються, коли підконтури йдуть в один бік
// Ті самі два прямокутники, два правила заливання, два різні результати
// Ненульове обертання: обидва прямокутники обходяться в одному напрямку, тому внутрішній
// НЕ вирізає отвір, і весь зовнішній прямокутник заливається суцільно
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

// Парне-непарне правило: внутрішній прямокутник перетинається парну кількість разів,
// тому пробиває чіткий прямокутний отвір у зовнішньому
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): ви задаєте дві точки, які визначають вісь, початковий колір у першій точці та кінцевий колір у другій, а візуалізатор інтерполює колір уздовж цієї осі. Кожна точка в заповненій області набуває кольору своєї перпендикулярної проєкції на вісь, тому градієнт поширюється смугами під прямим кутом до лінії між двома точками

У PDF Library for Delphi градієнт є іменованим ресурсом документа, який ви створюєте один раз, а потім вибираєте як активну фарбу. NewRGBAxialShader реєструє його. Сигнатура виглядає так: NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): дві кінцеві точки осі, трійки RGB на кожному кінці як значення в діапазоні від 0 до 1, та прапорець Extend. Якщо Extend встановлено в 1, кінцеві кольори продовжуються як суцільна заливка за межами кінцевих точок осі, що зазвичай і потрібно, щоб кути області поза віссю не залишалися незафарбованими; 0 залишає їх неторканими. Коли шейдер створено, ви прив'язуєте його за допомогою SetFillShader для заповнених областей, SetLineShader для обведених контурів або SetTextShader для тексту. Прив'язка залишається активною для наступних викликів малювання, тому контур, який ви малюєте далі, набуває градієнта замість плоского кольору

PDF Library for Delphi: анатомія осьового RGB-градієнта: кольорові смуги перпендикулярні осі затінення, проєкція на вісь і Extend, що продовжує кінцеві кольори суцільною заливкою за межами кінців
Осьовий шейдер інтерполює вздовж осі між двома кінцевими кольорами, і кожна точка семплює свій перпендикулярний проєкт на ту лінію — Extend = 1 тримає кінцеві кольори такими, що тікають за межі точок, тож кути залишаються зафарбованими
// Визначити вертикальний градієнт один раз: синій унизу, білий угорі
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // початкова точка й початковий RGB
  0, 260,   1.00, 1.00, 1.00,    // кінцева точка й кінцевий RGB
  1);                            // 1 = продовжувати кінці суцільним кольором

// Вибрати градієнт як заливку, потім намалювати ним прямокутник
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = заливання, тепер заповнено шейдером

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

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

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

PDF Library for Delphi створює комірку візерунка із захопленого вмісту сторінки. Ви захоплюєте сторінку або область за допомогою CapturePage, перетворюєте захоплення на іменований візерунок за допомогою NewTilingPatternFromCapturedPage(PatternName, CaptureID), а потім вибираєте цей візерунок як поточну заливку за допомогою SetFillTilingPattern(PatternName). З цього моменту будь-який контур, який ви заповнюєте, малюється повторюваною коміркою, а не плоским кольором, що працює точно так само, як і заливка шейдером, але джерелом фарби є викладена плиткою комірка. Ця послідовність складніша за один виклик, тому якщо крок захоплення вам не знайомий, розгляньте створення візерунка як двоетапну операцію: спочатку створіть захоплену комірку, а потім прив'яжіть її як заливку за ім'ям перед малюванням області, яку ви хочете замостити плиткою

Об'єднання примітивів разом

Ці елементи поєднуються безпосередньо. Зафарбована фігура Безьє — це контур з кривих, намальований за допомогою DrawPath. Той самий контур, намальований за допомогою DrawPathEvenOdd після додавання внутрішнього циклу, показує отвір, який заповнення обходом закрило б. Прямокутник, заповнений градієнтом — це прямокутник, пов'язаний із шейдером. Наведений нижче приклад малює всі три елементи послідовно, так що різниця між двома правилами заповнення видна на одній сторінці, а під ними розміщується градієнтна панель

// 1. Заповнена фігура Bezier (ненульове обертання)
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. Той самий контур із внутрішньою петлею, залитий за парно-непарним правилом для показу отвору
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);                              // отвір вирізано

// 3. Прямокутник, залитий аксіальним градієнтом
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 тексту, зображень, форм та підписів, які описані в інших статтях цього блогу