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ó PDFlibPas 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.
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 PDFlibPas-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.
// 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
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.
PDFlibPas 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.
// 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
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 PDFlibPas-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.
// 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
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 PDFlibPas 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. 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);
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.