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

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

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

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

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

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

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

Порівняння лівого верхнього початку координат екранної канви та лівого нижнього початку координат PDF: та сама точка біля верху сторінки US Letter читається як Y = 72 в коді TCanvas і як Y = 720 в пунктах HotPDF, тож перенесений код без перевертання Y малює дзеркально
HotPDF відмірює від лівого нижнього кута в пунктах, тож місце біля верху сторінки 612 на 792 несе Y = 720 — те саме фізичне розташування, яке код TCanvas адресує невеликим Y униз

Контури: MoveTo, LineTo, Stroke

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

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

    // Товщина лінії задається в пунктах і діє, доки ви її не зміните.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Горизонтальна лінія біля верху сторінки (Y відмірюється від низу).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // фіксує контур; до цього нічого не малювалося

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

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

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

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

Модель контурів HotPDF: MoveTo, LineTo, Rectangle та Circle будують у пам'яті невидимий поточний контур, і лише фіксація через Stroke, Fill чи FillAndStroke фарбує його, використовуючи сталий графічний стан із кольору обведення, кольору заливки та товщини лінії, після чого буфер очищається
Геометрія мовчки накопичується в поточному контурі, доки оператор фарбування не зафіксує її збереженими кольором обведення, кольором заливки та товщиною лінії — забутий виклик фарбування лишає фігуру ненамальованою
// Rectangle(X, Y, Width, Height): X та Y є лівим нижнім кутом.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X та Y є центром.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

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

Складаємо примітиви разом: виносна рамка

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

Анатомія виносної рамки, зібраної з примітивів HotPDF: прямокутник, зафіксований через FillAndStroke, обведена лінія-вказівник із MoveTo та LineTo, залита крапка-якір і смуги підписів TextOut, що поділяють ту саму сітку від лівого нижнього кута, що й фігури
Чотири фіксації будують виноску — FillAndStroke фарбує панель і облямівку разом, вказівник і крапка перевикористовують стан обведення та заливки, а кожен зсув підпису є звичайною арифметикою відносно кута рамки (90, 600)
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. Рамка: бліда заливка плюс видима облямівка, один контур, одна фіксація.
    //    Rectangle приймає лівий нижній кут плюс розмір, Y відмірюється від низу
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // бліда бурштинова панель
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // темніша облямівка
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. Вказівник: один обведений сегмент від краю рамки вниз
    //    до того, що анотується
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // лівий край рамки
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. Залита крапка закріплює вказівник на його цілі
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. Підпис, розміщений відносно лівого нижнього кута рамки.
    //    Текст і фігури поділяють одну систему координат, тож зсуви
    //    є звичайною арифметикою відносно (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

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

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

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

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

Криві, штрихи та прозорість стисло

Довільні криві розширюють ту саму механіку контурів, яку ви вже маєте. CurveToC(X1, Y1, X2, Y2, X3, Y3) додає кубічний сегмент Безьє від поточної точки до (X3, Y3), згинаючи його до двох контрольних точок, а скорочені варіанти CurveToV та CurveToY покривають випадки, де одна контрольна точка збігається з кінцевою. Контур може вільно змішувати сегменти LineTo та CurveToC, перш ніж єдиний Stroke чи Fill зафіксує його, і саме так будуються заокруглені кути та плавні лінії графіків

Штрихові обведення є станом, точно як товщина лінії. SetDash([3, 3], 0) перемикає кожне подальше обведення на візерунок "три пункти є, три пункти немає", де масив розписує довжини ділянок увімкнено/вимкнено в пунктах, а другий аргумент зсуває фазу початку циклу; NoDash повертає перу суцільну лінію. Задайте його, обведіть ті лінії сітки, яким він потрібен, і скиньте його перед наступною суцільною лінією, інакше штрих тихцем заразить усе, що йде далі

Прозорість проходить через іменований графічний стан, а не через аргумент кольору, бо альфа в PDF є властивістю словника графічного стану. Зареєструйте один на документі через RegisterExtGState, передавши альфу заливки та альфу обведення в діапазоні від 0 до 1, а потім застосуйте повернуте ним ім'я через CurrentPage.SetGraphicsState; заливки та обведення від цієї миті фарбуються із зареєстрованою непрозорістю. Церемонія тут важча, ніж у сетерів кольору, і вона того варта першого ж разу, коли смуга виділення має лягти поверх тексту, не ховаючи його

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

Показані тут виклики MoveTo, LineTo, Stroke, Fill та кольору є частиною HotPDF Delphi Component для Delphi та C++Builder