Повечето Delphi кодове, които докосват PDF, третират формата като контейнер за две неща: текстови блокове и няколко разположени растерни изображения. Тази гледна точка е правилна, доколкото се простира, но оставя най-способната част от формата неизползвана. Страницата на PDF е независимо от разделителната способност 2D платно, изградено върху съния модел на изобразяване като PostScript. То може да рисува линии, криви, запълнени области, градиенти и повтарящи се шарки, всичко това как вектори, които остават остри при всяко увеличение и се отпечатват с пълната разделителна способност на устройството. Ако рисувате лого, диаграма, воден знак или рамка на сертификат, векторният път почти винаги е правилният примитив и е по-малък и по-ясен от растеризираното изображение, към което вместо това се насочват много програми.
Тази статия разглежда векторния модел, както го дефинира ISO 32000-1, и показва съответстващите извиквания на PDFlibPas. Целта е да се конкретизира спецификацията, тъй като API съвпада плътно с нея, а разбирането на едното ви учи на другото.
Страницата е машина за пътища
ISO 32000-1 §8.5 описва графиките в две фази, които никога не се застъпват. Първо конструирате път (path), което е чиста геометрия без видим резултат. След това изрисувате този път в една операция, която очертава контура му, запълва вътрешността му или прави и двете. Нищо не се появява на страницата по време на строителството. Пътят е абстрактна последователност от точки и сегменти, съхранявани в графичното състояние (graphics state), докато оператор за рисуване не го консумира, при което той се рендерира и изхвърля.
Пътят се състои от один или повече подпътища (subpaths). Подпътят започва в точка и расте чрез добавяне на сегменти: прави линии, кубични криви на Безие, а на някои платформи и цели правоъгълници, добавени как собствен затворен подпът. В 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.
Две правила за запълване и защо те се разминават
Когато запълвате път, който се пресича сам или съдържа вътрешен контур, рендерерът се нуждае от правило, по което да реши кои области са във вътрешността на формата и кои са дупки. ISO 32000-1 §8.5.3.3 дефинира две правила и те могат да изрисуват една и съща геометрия по различен начин. Правилото за ненулево навиване (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 leaves them untouched. След като шейдърът (shader) съществува, вие го обвързвате с 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 patterns) повтарят клетка
Докато градиентът променя плавно един цвят, повтарящата се шарка (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 контекст на устройство (device context) за предварителен преглед на екрана и печат, разгледано в ръководството за печат и предварителен преглед. Извикванията за пътища, шейдъри и шарки, описани тук, се доставят как част от Delphi PDF Library заедно с API за текст, изображения, форми и подписи, разгледани на други места в този блог.