Техническа статия

Чертаене на Canvas в HotPDF в Delphi: Векторни пътища и цвят

HotPDF чертае векторна графика чрез изграждане на път върху текущата страница и след това искане той да бъде нарисуван (painted). Няма междинна стъпка с растерно изображение (bitmap). Линия, която чертаете с MoveTo и LineTo, завършва като PDF оператори за път в потока от съдържание (content stream), така че тя остава истински вектор: ясна на 50% увеличение, ясна на 1600% и струваща малка част от размера на растеризирана версия. За диаграми, линии на таблици (table rules), оси на диаграми и декорации на формуляри, това е точно това, което искате, и API-то зад него е достатъчно малко, за да се научи на едно сядане

Цялата повърхност за чертаене живее в THotPDF.CurrentPage. Между BeginDoc и EndDoc задавате цвят и ширина на линията върху този обект на страницата, полагате геометрия и извиквате оператор за рисуване, за да го ангажирате (commit). Четирите примитива, които ще използвате най-много, са MoveTo и LineTo за произволни пътища, Rectangle за кутии, Circle за дискове и двата оператора за рисуване Stroke и Fill

Координатната система е от долния ляв ъгъл

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

Същата конвенция управлява TextOut, така че текстът и формите споделят един мисловен модел, след като го усвоите вътрешно. Планирайте оформление, като решите къде седи дъното на всеки елемент, а не върхът, и останалото следва

Пътища: MoveTo, LineTo, Stroke

Щрихован път (stroked path) е писалка, повдигната, поставена и влачена. MoveTo повдига писалката и задава началната точка, без да маркира нищо. Всяко LineTo удължава текущия път до нова точка. Нищо не се появява на страницата, докато не извикате Stroke, което начертава натрупания път с помощта на текущия цвят на щриха (stroke color) и ширина на линията, след което изчиства пътя, така че следващото 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;

Два детайла спестяват реално време за отстраняване на грешки. Ширината на линията е състояние (state), а не аргумент: SetLineWidth я задава веднъж и всяко последващо Stroke използва тази стойност, докато не я промените отново, поради което начупената линия (polyline) по-горе е по-дебела от правилото. И пътят се нулира (resets) след всяко Stroke, така че забравено Stroke означава, че геометрията, която сте оформили толкова внимателно, никога не се рендира изобщо. Ако липсва форма от изхода, извикването за рисуване е първото място, където трябва да погледнете

Координатите са пунктове (points), а пунктовете са дробни. MoveTo и LineTo приемат Single стойности, така че линия-косъм (hairline) при 0.5 пункта или позиция при 72.25 е валидна и значима, а не закръглена до най-близката цяла единица. Тази прецизност има значение в две противоположни посоки. Ширина на линията под около 0.5 може да се рендира като зависима от устройството възможно най-тънка линия, която изчезва на екрана и се появява отново при отпечатване, така че видима линия иска ширина, която задавате умишлено, а не по подразбиране. От друга страна, прилепването на линиите на таблицата (table rules) и линиите на мрежата (gridlines) към координати в цели пунктове предпазва гъста мрежа от това да изглежда леко неравномерна там, където съседни линии се закръгляват различно. Решете разстоянието на мрежата в пунктове предварително и останалата част от оформлението ще го наследи

Запълнени форми и цвят

Затворените примитиви могат да бъдат запълнени (filled) вместо очертани (outlined). Rectangle приема позиция и размер, Circle приема център и радиус и всеки от тях се ангажира (commits) с Fill, което боядисва вътрешността в текущия цвят на запълване (fill color), или със 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) е центърът и третият аргумент е радиусът в пунктове

Един избор на цвят, който оригиналният пример обърка

По-стара версия на този пример засяваше (seeded) цветове с Random($FFFFFF) на всяка форма. Изглежда оживено и е грешен инстинкт за генерирани документи. PDF, който изграждате от код, обикновено е нещо, което искате и да тествате, а произволните цветове за запълване правят изхода невъзможен за сравняване между отделните изпълнения: байт-по-байт diff срещу известен-добър (known-good) файл се проваля всеки път, без реална причина. Избирайте изрични цветове. Когато искате разнообразие в серия от форми, управлявайте го от вашите данни или фиксиран масив с палитра, така че същият вход винаги да произвежда същия файл. Детерминизмът струва повече от новостта, когато артефактът се движи през конвейер за издаване (release pipeline)

Сглобяване на примитивите: кутия за пояснение (callout box)

Всеки примитив е прост сам по себе си; отплатата се показва, когато шепа от тях се композират в нещо, от което даден отчет реално се нуждае. Пояснение (callout) – анотираната кутия, която сочи към фигура и я обяснява – използва всичко обхванато дотук: запълнен правоъгълник с граница, щрихована линия на показалеца, точка за закрепване на показалеца и текст, разположен вътре в кутията, използващ същите координати от долния ляв ъгъл, които използват формите. FillAndStroke печели мястото си тук, рисувайки вътрешността и очертанието на един път в един ангажимент (commit), вместо да изгражда правоъгълника два пъти

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

    // 1. The box: pale fill plus a visible border, one path, one commit.
    //    Rectangle is lower-left corner plus size, Y measured from the bottom
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // pale amber panel
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // darker rim
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. The pointer: one stroked segment from the box edge down
    //    toward the thing being annotated
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // left edge of the box
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. A filled dot anchors the pointer at its target
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. The label, positioned relative to the box's lower-left corner.
    //    Text and shapes share one coordinate system, so the offsets
    //    are plain arithmetic against (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 вместо това; трасирането на растерно изображение с векторни извиквания не ви купува нищо. Правите сегменти, правоъгълниците и кръговете по-горе носят по-голямата част от реалната работа по отчетите, а трите усъвършенствания, за които питат разработчиците след това – криви, модели с тирета и прозрачност – седят на същия обект на страницата

Криви, тирета (dashes) и прозрачност накратко

Свободните криви (Freeform curves) разширяват същата машинария за пътища, която вече имате. CurveToC(X1, Y1, X2, Y2, X3, Y3) добавя кубичен сегмент на Безие от текущата точка до (X3, Y3), огъвайки се към двете контролни точки, а съкратените варианти CurveToV и CurveToY покриват случаите, когато една контролна точка съвпада с крайна точка. Един път може да смесва свободно сегменти LineTo и CurveToC преди едно-единствено Stroke или Fill да го ангажира, което е начинът, по който се изграждат заоблени ъгли и гладки линии на диаграми

Щрихите с тирета (Dashed strokes) са състояние, точно като ширината на линията. SetDash([3, 3], 0) превключва всеки последващ щрих към модел (pattern) три пункта включено, три пункта изключено, като масивът изписва дължините на ходовете за включено/изключено в пунктове, а вторият аргумент фазира къде започва цикълът; NoDash връща писалката към плътна линия. Задайте го, щриховайте линиите на мрежата, които го искат, и го нулирайте преди следващата плътна линия (rule), или тиретата тихо ще заразят всичко, което следва

Прозрачността протича през наименувано графично състояние, а не през аргумент за цвят, защото алфа в PDF е свойство на речника за графично състояние. Регистрирайте такова в документа с RegisterExtGState, подавайки алфа на запълване и алфа на щриха между 0 и 1, след което приложете името, което връща, с CurrentPage.SetGraphicsState; запълванията и щрихите от този момент нататък се рисуват с регистрираната непрозрачност (opacity). Това е по-тежка церемония от задаващите цветове (color setters), и си струва първия път, когато лента за открояване (highlight bar) трябва да седи над текст, без да го скрива

Оставащият навик, който си струва да се запази, е верификацията. Генерираната геометрия може да мине на вашата машина и да се провали на клиентската, обикновено заради подмяна на шрифт във всеки текст, който смесвате, или предположение за размер на страницата, което не се потвърждава. Отворете готовия файл на няколко нива на увеличение, за да потвърдите, че ръбовете остават чисти, и проверете дали всяка форма попада вътре в полето за марж, което сте възнамерявали. С детерминистична цветова схема тази проверка може да бъде автоматизирана срещу референтен PDF, вместо да се преценява на око

Извикванията MoveTo, LineTo, Stroke, Fill и цветовите извиквания, показани тук, са част от компонента HotPDF за Delphi и C++Builder