Műszaki cikk

Vektorgrafika PDF-ben Delphi segítségével: Útvonalak és színátmenetek

A legtöbb Delphi kód, amely a PDF-et kezeli, a formátumot két dolog tárolójaként kezeli: szövegblokkok és néhány elhelyezett bitkép. Ez a szemlélet igaz, amennyiben eddig terjed, de kihasználatlanul hagyja a formátum legképesebb részét. A PDF oldal egy felbontásfüggetlen 2D vászon, amely ugyanarra a képalkotási modellre épül, mint a PostScript. Képes vonalakat, görbéket, kitöltött területeket, színátmeneteket és ismétlődő mintákat rajzolni, mindezt olyan vektorokként, amelyek bármilyen nagyításnál élesek maradnak, és az eszköz teljes felbontásában nyomtathatók. Ha logót, diagramot, vízjelet vagy tanúsítványkeretet rajzol, a vektoros útvonal (vector path) szinte mindig a megfelelő primitív, és kisebb méretű, valamint élesebb, mint a raszterizált kép, amelyhez sok program ehelyett nyúl

Ez a cikk végigvezeti a vektormodellen úgy, ahogyan azt az ISO 32000-1 meghatározza, és bemutatja a hozzá tartozó PDF Library for Delphi hívásokat. A cél a specifikáció konkrétabbá tétele, mivel az API szorosan leképezi azt, és az egyik megértése megtanítja a másikat is

Az oldal egy útvonalgép

Az ISO 32000-1 8.5. szakasza a grafikát két olyan fázisban írja le, amelyek soha nem fedik egymást. Először felépít egy útvonalad, ami tiszta geometria, látható eredmény nélkül. Ezután ezt az útvonalat egyetlen műveletben kirajzolja (paint), amely körberajzolja a körvonalát (stroke), kitölti a belsejét (fill), vagy mindkettőt elvégzi. A felépítés során semmi sem jelenik meg az oldalon. Az útvonal pontok és szegmensek absztrakt sorozata, amelyet a grafikai állapotban tartunk, amíg egy rajzoló operátor fel nem használja, és ekkor kerül leképezésre és eldobásra

PDF Library for Delphi: PDF-útvonal életciklus diagram: szerkesztés láthatatlan tiszta geometriaként, majd egy rajzoló operátor, amely körvonalaz, tölt vagy mindkettőt, majd elfogyasztja és eldobja az útvonalat
A négyszög felépítése semmit nem fest — a DrawPath hívás egyszer olvassa a grafikaállapotot, kontúrt, kitöltést vagy mindkettőt renderel, és eldobja az útvonalat

Az útvonal egy vagy több részútvonalból (subpath) áll. A részútvonal egy pontnál kezdődik, és szegmensek hozzáfűzésével növekszik: egyenes vonalak, köbös Bezier-görbék, és egyes platformokon teljes téglalapok, amelyeket saját zárt részútvonalként adnak hozzá. A PDF Library for Delphi-ban az útvonalat a StartPath paranccsal nyitja meg, amely beállítja a kezdőpontot, majd az AddLineToPath és AddCurveToPath segítségével bővíti azt. Minden hívás előreléptet egy implicit aktuális pontot, így a következő szegmens onnan folytatódik, ahol az előző véget ért. A ClosePath egy utolsó egyenes szakaszt rajzol vissza a részútvonal kezdetéhez, ami a körvonalazásnál (stroke) fontos, mert valódi vonalkapcsolódást (line join) hoz létre a záró csúcspontnál a ke két laza végpont (end cap) helyett

// Zárt négyszög, körvonalazva, majd kitöltve
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // az útvonal megnyitása az első csúcsnál
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // egyenes szakasz vissza a (150, 100) ponthoz
PDF.DrawPath(2);                   // 2 = kitöltés és körvonal; az útvonal felhasználva

A görbék az AddCurveToPath függvényt használják, amely két Bezier kontrollpontot és egy végpontot vár: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). A görbe az aktuális ponttól a (EndX, EndY) pontig fut, útközben a két kontrollpont felé húzva. Körívek az AddArcToPath(CenterX, CenterY, TotalAngle) segítségével érhetők el, ahol a sugár az aktuális pont és a középpont távolságából adódik, és a motor az ívet Bezier-szegmensek láncolataként adja ki. A téglalapokhoz van egy parancsikon, az AddBoxToPath(Left, Top, Width, Height), amely egy teljes zárt téglalapot fűz hozzá saját részútvonalként megelőző StartPath nélkül

Két kitöltési szabály, és miért térnek el egymástól

Amikor olyan útvonalat tölt ki, amely önmagát keresztezi vagy belső hurkot tartalmaz, a leképezőnek szabályra van szüksége annak eldöntéséhez, hogy mely területek esnek az alakzaton belülre, és melyek a lyukak. Az ISO 32000-1 8.5.3.3. szakasza kettőt határoz meg, és ezek ugyanazt a geometriát eltérő módon festhetik ki. A nemnulla körülfordulási szabály (nonzero winding rule) a tesztpontból a végtelenbe vetített sugár előjeles kereszteződéseit számolja: hozzáad egyet minden balról jobbra keresztező szakaszért, és levon egyet a másik irányúért; a pont akkor van belül, ha az összeg nem nulla. A páros-páratlan szabály (even-odd rule) figyelmen kívül hagyja az irányt, és egyszerűen számolja a kereszteződéseket, a pontot belsőnek tekintve, ha a szám páratlan

A klasszikus eset, amikor elválnak egymástól, egy lyukas alakzat, egy fánk vagy egy alátét. Rajzoljon egy külső határt és egy azon belüli belső határt. A páros-páratlan szabály szerint a belső hurok mindig kivág egy lyukat, mert a két határ közötti bármely pontot egyszer keresztezi, a belső hurkon belüli pontokat pedig kétszer. A nemnulla körülfordulási szabály szerint a lyuk csak akkor jelenik meg, ha a belső hurok a külsővel ellentétes irányban fut; ha azonos irányban futnak, a körülfordulások erősítik egymást a kioltás helyett, és a belső terület teljesen kitöltődik. Egyetlen önmagát keresztező körvonallal megrajzolt ötágú csillag ugyanezt a megoszlást mutatja: a páros-páratlan üresen hagyja a középső ötszöget, míg a nemnulla körülfordulási szabály kitölti azt

PDF Library for Delphi a kirajzoláshoz használt hívás alapján választja ki a szabályt, nem pedig egy jelzővel. A DrawPath a nemnulla körülfordulási szabállyal tölt ki; a DrawPathEvenOdd a páros-páratlan szabállyal. Mindkettő ugyanazt az egész szám típusú módot fogadja el: a 0 csak a körvonalat rajzolja meg (stroke), az 1 csak kitölt (fill), a 2 pedig kitölt és körvonalaz. A páros-páratlan szabály egyszerűbb eszköz a lyukak kivágásához, éppen azért, mert nem követeli meg a részútvonal irányának kezelését

PDF Library for Delphi: egymásba ágyazott téglalapok kitöltése páros-páratlan és nemnulla fonási szabály alatt: a páros-páratlan kivájja a belső téglalapot lyukként, míg a nemnulla fonás tömören tölt, kivéve ha a belső hurok az ellentétes irányban fon
Ugyanaz az alátét-geometria két legális válasz alatt — a DrawPathEvenOdd figyelmen kívül hagyja a körüljárás irányát, és mindig kivájja a lyukat, míg a DrawPath előjeles körüljárásokat ad össze, amelyek megerősítik egymást, ha az alútvonalak azonos irányúak
// Ugyanaz a két doboz, két kitöltési szabály, két különböző eredmény
// Nem nulla körbefordulás: mindkét doboz ugyanabba az irányba fordul, ezért a belső
// NEM vág lyukat, és a teljes külső doboz tömören kitöltődik
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

// Páros-páratlan: a belső dobozt páros számú alkalommal keresztezi
// ezért tiszta téglalap alakú lyukat üt a külső dobozon
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

A tengelyirányú színátmenetek egy vonal mentén változtatják a színt

A sík kitöltőszín egyetlen érték a teljes területen. A színátmenet folyamatosan változtatja a színt, a legegyszerűbb fajtája pedig a tengelyirányú (axial) vagy lineáris színátmenet. Az ISO 32000-1 8.7.4.5. szakasza Type 2 tengelyirányú árnyékolásként (axial shading) határozza meg: megad két pontot, amelyek meghatározzák a tengelyt, egy kezdő színt az első pontnál és egy záró színt a másodiknál, és a leképező interpolálja a színt ezen tengely mentén. A kitöltött terület minden pontja a tengelyre való merőleges vetületének színét veszi fel, így a színátmenet a két pont közötti vonalra merőleges sávokban fut

A PDF Library for Delphi-ban a színátmenet egy nevesített dokumentum-erőforrás, amelyet egyszer hoz létre, majd aktív festékként választ ki. A NewRGBAxialShader regisztrálja azt. A szignatúra: NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): a két tengelyvégpont, az RGB hármasok a végeken 0 és 1 közötti értékként, és egy Extend jelző. Ha az Extend értéke 1, a vég színei szilárd kitöltésként folytatódnak a tengelyvégpontokon túl is, ami általában kívánatos, hogy a tengelyen kívüli terület sarkai ne maradjanak festetlenül; a 0 érintetlenül hagyja őket. Miután az árnyékoló (shader) létezik, összekapcsolhatja a SetFillShader segítségével kitöltött területekhez, a SetLineShader segítségével körvonalakhoz, vagy a SetTextShader segítségével szöveghez. Az összekapcsolás aktív marad a következő rajzolási hívásoknál, így a legközelebb megrajzolt útvonal a színátmenetet veszi fel a sík szín helyett

PDF Library for Delphi: axiális RGB gradiens anatómia: az árnyalattengelyre merőleges színsávok, a tengelyre vetítés, és az Extend, amely a végpontokon túl tömör kitöltéssel folytatja a végszíneket
Egy axiális árnyaló két végponti szín tengelye mentén interpolál, minden pont a vonalra eső merőleges vetületét mintavételezi — az Extend = 1 a végszíneket a végpontokon túl is futtatja, hogy a sarkak festve maradjanak
// Definiáljon egyszer egy függőleges gradienst: alul kék, felül fehér
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // kezdőpont és kezdő RGB
  0, 260,   1.00, 1.00, 1.00,    // végpont és vég RGB
  1);                            // 1 = a végek kiterjesztése homogén színként

// Válassza a gradienst kitöltésként, majd fessen vele egy téglalapot
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = kitöltés, most a shader tölti ki

A tengely itt függőleges, y=100 és y=260 között egy fix x pozíciónál, így a színsávok vízszintesen futnak, és a téglalap az alján lévő kékből a tetején lévő fehérbe halványul. Mivel az árnyékoló név alapján van kulcsolva, egyetlen definíció tetszőleges számú alakzatot tölthet ki az oldalon, és a sík színre való visszaváltás csak egy újabb SetFillColor hívás a következő útvonal előtt

A minták ismétlődő cellát használnak

Ahol a színátmenet egyetlen színt változtat simán, ott az ismétlődő minta (tiling pattern) egy kis művészeti alkotást ismétel egy területen. Az ISO 32000-1 8.7.3.1. szakasza az ismétlődő mintát mintacellaként (pattern cell), egy független tartalomdarabként határozza meg, amelyet a leképező egy fix rácson replikál a festendő terület lefedéséhez. Így építhet sraffozást egy mérnöki kitöltéshez, ismétlődő márkamotívumot egy fejléc mögé, vagy egy texturált hátteret, amely vektorosan éles marad, és szinte semmit sem nyom a fájlméretben, függetlenül attól, hogy mekkora a terület, mivel a cella egyszer van tárolva, és mindenhol hivatkoznak rá

A PDF Library for Delphi a rögzített oldaltartalomból építi fel a mintacellát. Rögzít egy oldalt vagy egy régiót a CapturePage segítségével, a rögzítést nevesített mintává alakítja a NewTilingPatternFromCapturedPage(PatternName, CaptureID) segítségével, majd kiválasztja ezt a mintát aktuális kitöltésként a SetFillTilingPattern(PatternName) függvénnyel. Ettől a ponttól kezdve minden kitöltött útvonal az ismétlődő cellával lesz megfestve sík szín helyett, pontosan úgy, ahogyan az árnyékoló kitöltése működik, de a festék forrása a mintacella. A folyamat összetettebb, mint egyetlen hívás, így ha a rögzítési lépés szokatlan, kezelje a mintát kétlépcsős műveletként: először hozza létre a rögzített cellát, majd kösse össze kitöltésként név szerint, mielőtt megrajzolná a mintázni kívánt területet

A primitívek összeillesztése

A darabok közvetlenül összekapcsolhatók. Egy kitöltött Bezier alakzat egy görbékből álló útvonal, amelyet a DrawPath rajzol ki. Ugyanez a körvonal a DrawPathEvenOdd segítségével megrajzolva, egy belső hurok hozzáadása után egy olyan lyukat mutat, amelyet a körülfordulási kitöltés lezárt volna. A színátmenettel kitöltött téglalap egy árnyékolóhoz kötött doboz. Az alábbi példa mindhármat sorban rajzolja le, így a két kitöltési szabály közötti különbség látható egy oldalon, majd egy színátmenetes panelt helyez el alattuk

// 1. Kitöltött Bezier-alakzat (nem nulla körbefordulás)
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. Ugyanaz a körvonal belső hurokkal, páros-páratlan kitöltéssel a lyuk megjelenítéséhez
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);                              // a lyuk kivágva

// 3. Téglalap axiális gradienssel kitöltve
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);

Két részletet érdemes megjegyezni. A rajzolási hívás dönti el a kitöltési szabályt, így a DrawPath és a DrawPathEvenOdd közötti választás a nemnulla körülfordulás és a páros-páratlan közötti választást jelenti, és a lyukas alakzatoknál a páros-páratlan szabály megkíméli a részútvonal irányáról való gondolkodástól. A grafikai állapot pedig a kirajzolás pillanatában kerül mintavételezésre: állítsa be a színeket, a vonalvastagságot és az árnyékoló kötését a rajzolási hívás előtt, mert ezt az állapotot olvassa a motor. Először építsen fel, konfigurálja az állapotot, rajzoljon utoljára, és a vektormodell minden alkalommal kiszámíthatóan fog viselkedni

Innen a természetes következő lépés a vektorok és a szöveg visszaolvasása egy meglévő dokumentumból, amelyet a szöveg-, kép- és betűtípus-kivonásról szóló cikkünk tárgyal, valamint ugyanezen rajzi modell leképezése egy Windows eszközkontextusra a képernyőn történő előnézethez és nyomtatáshoz, amelyet a nyomtatási és előnézeti bemutató ismertet. Az itt leírt útvonal-, árnyékoló- és mintahívások a Delphi PDF Library részeként érhetők el, a blogon máshol tárgyalt szöveg-, kép-, űrlap- és aláírás API-kkal együtt