Повечето Delphi кодове, които докосват PDF, третират формата като контейнер за две неща: текстови блокове и няколко разположени растерни изображения. Тази гледна точка е правилна, доколкото се простира, но оставя най-способната част от формата неизползвана. Страницата на PDF е независимо от разделителната способност 2D платно, изградено върху съния модел на изобразяване като PostScript. То може да рисува линии, криви, запълнени области, градиенти и повтарящи се шарки, всичко това как вектори, които остават остри при всяко увеличение и се отпечатват с пълната разделителна способност на устройството. Ако рисувате лого, диаграма, воден знак или рамка на сертификат, векторният път почти винаги е правилният примитив и е по-малък и по-ясен от растеризираното изображение, към което вместо това се насочват много програми
Тази статия разглежда векторния модел, както го дефинира ISO 32000-1, и показва съответстващите извиквания на PDF Library for Delphi. Целта е да се конкретизира спецификацията, тъй като API съвпада плътно с нея, а разбирането на едното ви учи на другото
Страницата е машина за пътища
ISO 32000-1 §8.5 описва графиките в две фази, които никога не се застъпват. Първо конструирате път (path), което е чиста геометрия без видим резултат. След това изрисувате този път в една операция, която очертава контура му, запълва вътрешността му или прави и двете. Нищо не се появява на страницата по време на строителството. Пътят е абстрактна последователност от точки и сегменти, съхранявани в графичното състояние (graphics state), докато оператор за рисуване не го консумира, при което той се рендерира и изхвърля
Пътят се състои от один или повече подпътища (subpaths). Подпътят започва в точка и расте чрез добавяне на сегменти: прави линии, кубични криви на Безие, а на някои платформи и цели правоъгълници, добавени как собствен затворен подпът. В 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
Две правила за запълване и защо те се разминават
Когато запълвате път, който се пресича сам или съдържа вътрешен контур, рендерерът се нуждае от правило, по което да реши кои области са във вътрешността на формата и кои са дупки. ISO 32000-1 §8.5.3.3 дефинира две правила и те могат да изрисуват една и съща геометрия по различен начин. Правилото за ненулево навиване (nonzero winding rule) брои подписаните пресичания на лъч, хвърлен от тестова точка към безкрайността, като добавя едно за всеки сегмент, който пресича от ляво на дясно, и изважда едно за всеки, който пресича в другата посока; точката е вътре, когато общата сума не е нула. Правилото за четно-нечетно (even-odd rule) игнорира посоката и просто брои пресичанията, като нарича точката вътрешна, когато броят е нечетен
Класическият случай, в който те се разминават, е форма с дупка, поничка или шайба. Начертайте външна граница и вътрешна граница в нея. Съгласно правилото за четно-нечетно, вътрешният контур винаги издълбава дупка, тъй като всяка точка между двете граници се пресича веднъж, а всяка точка във вътрешния контур се пресича два пъти. При правилото за ненулево навиване дупката се появява само ако вътрешният контур се навива в посока, обратна на външния; навийте ги в една и съща посока и навивките се подсилват, вместо да се анулират, и вътрешната област се запълва изцяло. Петлъчева звезда, нарисувана как един контур, който се самопресича, показва същото разделение: четно-нечетно оставя централния петоъгълник празен, докато ненулевото навиване го запълва
PDF Library for Delphi избира правилото според извикването, което правите за изрисуване, а не чрез флаг. DrawPath запълва по правилото за ненулево навиване; DrawPathEvenOdd запълва по правилото за четно-нечетно. И двете приемат един и същ целочислен режим: 0 само очертава контура, 1 само запълва, а 2 запълва и очертава. Правилото за четно-нечетно е по-лесният инструмент за перфорирани дупки именно защото не изисква от вас да управлявате посоката на подпътя
// Същите два правоъгълника, две правила за запълване, два различни резултата
// Nonzero winding: и двата правоъгълника се завиват в една посока, затова вътрешният
// НЕ изрязва отвор и целият външен правоъгълник се запълва плътно
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: вътрешният правоъгълник се пресича четен брой пъти
// затова пробива чист правоъгълен отвор през външния правоъгълник
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 leaves them untouched. След като шейдърът (shader) съществува, вие го обвързвате с SetFillShader за запълнени области, SetLineShader за очертани контури или SetTextShader за текст. Обвързването остава активно за следващите извиквания за рисуване, така че пътят, който рисувате след това, приема градиента вместо плосък цвят
// Дефинирайте вертикален градиент веднъж: синьо долу към бяло горе
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 patterns) повтарят клетка
Докато градиентът променя плавно един цвят, повтарящата се шарка (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 фигура (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. Същият контур плюс вътрешен цикъл, запълнен по even-odd, за да покаже отвор
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 контекст на устройство (device context) за предварителен преглед на екрана и печат, разгледано в ръководството за печат и предварителен преглед. Извикванията за пътища, шейдъри и шарки, описани тук, се доставят как част от Delphi PDF Library заедно с API за текст, изображения, форми и подписи, разгледани на други места в този блог