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

Малювання на Canvas у HotPDF в Delphi: векторні контури та кольори

HotPDF малює векторну графіку, будуючи шлях на поточній сторінці, а потім подаючи запит на його відмальовування. Жодного проміжного етапу з раструванням немає. Лінія, яку ви малюєте за допомогою MoveTo та LineTo, зрештою стає операторами шляху PDF у потоці вмісту, тому вона залишається справжнім вектором: чіткою при масштабі 50%, чіткою при 1600%, і становить лише частку того розміру, якого вимагала б растрована версія. Для діаграм, ліній таблиць, осей графіків і декорацій форм - це саме те, що вам потрібно, а API, що стоїть за цим, достатньо малий, щоб вивчити його за один раз

Уся поверхня малювання знаходиться на THotPDF.CurrentPage. Між BeginDoc та EndDoc ви встановлюєте колір і товщину лінії на цьому об'єкті сторінки, задаєте геометрію і викликаєте оператор малювання для її фіксації. Чотири примітиви, які ви будете використовувати найчастіше, - це MoveTo і LineTo для довільних шляхів, Rectangle для прямокутників, Circle для кругів, а також два оператори відмальовування: Stroke та Fill

Під час малювання потрібно перетворити координати макета у PDF-користувацькі одиниці: початок координат розташований унизу ліворуч, а висота сторінки визначає вертикальне віддзеркалення екранних координат

Система координат починається в лівому нижньому куті

Це єдина річ, на якій спотикається кожен, хто приходить із VCL. TCanvas, яким ви малюєте елементи керування, розміщує початок координат у лівому верхньому куті, а Y збільшується донизу. У PDF все навпаки. HotPDF вимірює координати від лівого нижнього кута сторінки в пунктах (1/72 дюйма), причому Y збільшується в міру руху вгору. Точка з Y := 720 знаходиться біля верхнього краю сторінки формату US Letter, висота якої становить 792 пункти, а Y := 50 - біля нижнього. Якщо ваш перший малюнок виходить віддзеркаленим по вертикалі, то ось чому: код, портований з екранної графіки, припускає неправильний напрямок і виходить за нижній край

Та ж сама конвенція поширюється і на TextOut, тому текст і фігури поділяють одну ментальну модель, щойно ви її засвоїте. Плануйте макет, вирішуючи, де знаходиться низ кожного елемента, а не верх, і все інше стане на свої місця

Шляхи: MoveTo, LineTo, Stroke

Відмальований (stroked) шлях - це підняте, розміщене і проведене перо. MoveTo піднімає перо і встановлює початкову точку, нічого не позначаючи. Кожен LineTo подовжує поточний шлях до нової точки. На сторінці нічого не з'являється доти, доки ви не викличете Stroke, який малює накопичений шлях з використанням поточного кольору обведення і товщини лінії, а потім очищає шлях, щоб наступний MoveTo починав з чистого аркуша

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // Line width is in points and applies until you change it.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // A horizontal rule near the top of the page (Y measured from bottom).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // commit the path; nothing drew before this

    // A thicker connected polyline: three segments in one path.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Дві деталі значно економлять час на налагодження. Товщина лінії - це стан, а не аргумент: SetLineWidth встановлює її один раз, і кожен наступний Stroke використовує це значення, поки ви не зміните його знову. Саме тому ламана лінія (polyline) вище є товстішою за звичайну лінію. Крім того, шлях скидається після кожного Stroke, тому забутий Stroke означає, що геометрія, яку ви так ретельно вибудували, взагалі ніколи не відмалюється. Якщо у вихідному файлі бракує фігури, виклик малювання - це перше місце, де варто шукати проблему

Координати виражаються в пунктах, а пункти можуть бути дробовими. MoveTo та LineTo приймають значення Single, тому волосяна лінія в 0.5 пункту або позиція на 72.25 є коректною і має сенс, а не округлюється до найближчого цілого числа. Ця точність має значення у двох протилежних напрямках. Товщина лінії, менша за 0.5, може відмалюватися як найтонша можлива лінія, залежна від пристрою, яка зникає на екрані і з'являється знову під час друку, тому для видимої лінії потрібна товщина, яку ви встановлюєте цілеспрямовано, а не за замовчуванням. З іншого боку, прив'язка ліній таблиці та сітки до цілих значень координат запобігає тому, щоб щільна сітка виглядала злегка нерівною там, де суміжні лінії округлюються по-різному. Заздалегідь визначте крок сітки в пунктах, і решта макета успадкує його

Залиті фігури та колір

Замкнуті примітиви можна заливати замість того, щоб лише обводити контуром. Rectangle приймає позицію і розмір, Circle приймає центр і радіус, і будь-який з них фіксується за допомогою Fill, який зафарбовує нутрощі поточним кольором заливки, або за допомогою Stroke лише для контуру. Колір заливки і колір обведення є окремими елементами стану, які встановлюються за допомогою SetRGBFillColor і SetRGBStrokeColor, кожен з яких приймає одне значення TColor. Це означає, що ви можете безпосередньо використовувати константи кольорів Delphi та допоміжну функцію RGB

// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Зверніть увагу на набір аргументів у Rectangle. Це позиція-плюс-розмір: X, Y, Width, Height, а не два протилежні кути. Метод TCanvas.Rectangle, знайомий розробникам Delphi, приймає (Left, Top, Right, Bottom), тому м'язова пам'ять може підсунути HotPDF другий кут там, де він очікує ширину і висоту, і прямокутник вийде неправильного розміру. Пара (X, Y) - це лівий нижній кут, що відповідає початку координат сторінки. Для круга (X, Y) - це центр, а третій аргумент - радіус у пунктах

Один вибір кольору, в якому оригінальний приклад помилився

Старіша версія цього прикладу задавала кольори кожній фігурі за допомогою Random($FFFFFF). Це виглядає жваво, але це неправильний підхід для генерованих документів. PDF-файл, який ви створюєте з коду, зазвичай є тим, що ви також хочете протестувати, а випадкові кольори заливки роблять неможливим порівняння результатів від запуску до запуску: побайтове порівняння з еталонним файлом щоразу зазнаватиме невдачі без жодної реальної причини. Обирайте явні кольори. Коли ви хочете отримати різноманітність для серії фігур, керуйте цим за допомогою ваших даних або масиву з фіксованою палітрою, щоб той самий вхід завжди давав той самий файл. Детермінізм вартий більшого, ніж новизна, коли артефакт проходить через конвеєр випуску

Де векторне малювання окупається, а де - ні

Звертайтеся до цих викликів шляхів і фігур тоді, коли геометрія генерується: лінії сітки та стовпчики діаграм, розділові лінії таблиці рахунків-фактур, виноски на схемі, логотип, виражений кількома шляхами. Усе це масштабується без розмиття і майже нічого не додає до розміру файлу, оскільки прямокутник - це лише кілька чисел, а не тисячі пікселів. Зворотний бік також чесний. Якщо те, що ви насправді маєте, є фотографією або скриншотом, малюйте його як зображення за допомогою AddImage і ShowImage; векторизація растрового зображення за допомогою векторних викликів нічого вам не дасть. Складні криві тут також виходять за рамки. Примітиви, наведені вище, - це прямі відрізки, прямокутники і круги, які виконують переважну більшість реальної роботи зі створення звітів; будь-що, що потребує довільних кривих Безьє, є окремою частиною API

Ще одна звичка, яку варто зберегти, - це перевірка. Згенерована геометрія може правильно відображатися на вашій машині і давати збій на машині клієнта, зазвичай через підміну шрифтів у тексті, який ви додаєте, або через припущення щодо розміру сторінки, яке не справджується. Відкрийте готовий файл на кількох рівнях масштабування, щоб переконатися, що краї залишаються чіткими, і перевірте, чи кожна фігура потрапляє всередину задуманої вами обмежувальної рамки. За умови детермінованої колірної схеми, цю перевірку можна автоматизувати за допомогою еталонного PDF-файлу, замість того, щоб оцінювати все на око

Виклики MoveTo, LineTo, Stroke, Fill та функції для роботи з кольором, показані тут, є частиною HotPDF Component для Delphi та C++Builder