De meeste Delphi-code die met PDF werkt, behandelt het formaat als een container voor twee dingen: tekstblokken en een paar geplaatste bitmaps. Dat beeld is op zich correct, maar laat het krachtigste deel van het formaat onbenut. Een PDF-pagina is een resolutie-onafhankelijk 2D-canvas dat is gebouwd op hetzelfde beeldmodel als PostScript. Het kan lijnen, curven, gevulde gebieden, verlopen en herhalende patronen tekenen, allemaal als vectoren die scherp blijven bij elke zoomfactor en worden afgedrukt op de volledige resolutie van het apparaat. Als u een logo, een grafiek, een watermerk of een certificaatrand tekent, is het vectorpad bijna altijd het juiste basiselement. Het is kleiner en scherper dan de gerasterde afbeelding waar veel programma's in plaats daarvan naar grijpen.
Dit artikel bespreekt het vectormodel zoals gedefinieerd in ISO 32000-1 en laat de bijbehorende PDFlibPas-aanroepen zien. Het doel is om de specificatie concreet te maken, omdat de API er nauw op aansluit. Het begrijpen van de specificatie helpt u de API te begrijpen en andersom.
De pagina is een padenmachine
ISO 32000-1 §8.5 beschrijft afbeeldingen in twee fasen die elkaar nooit overlappen. Eerst construeert u een pad, wat pure geometrie is zonder zichtbaar resultaat. Daarna tekent u dat pad in een enkele bewerking die de omtrek omlijnt (strokes), het binnenste vult (fills) of beide doet. Tijdens de constructie verschijnt er niets op de pagina. Het pad is een abstracte reeks punten en segmenten die in de grafische status worden bewaard totdat een tekenbewerking (painting operator) deze verbruikt, waarna het pad wordt gerenderd en verwijderd.
Een pad bestaat uit een of meer subpaden. Een subpad begint bij een punt en groeit door segmenten toe te voegen: rechte lijnen, kubische Bezier-curven en op sommige platforms hele rechthoeken die als hun eigen gesloten subpad worden toegevoegd. In PDFlibPas opent u een pad met StartPath, wat het startpunt instelt, en breidt u het vervolgens uit met AddLineToPath en AddCurveToPath. Elke aanroep verplaatst een impliciet huidig punt, zodat het volgende segment doorgaat vanaf het punt waar het vorige eindigde. ClosePath tekent een definitief recht segment terug naar het begin van het subpad. Dit is belangrijk voor het omlijnen, omdat het zorgt voor een echte lijnverbinding bij het sluitende hoekpunt in plaats van twee losse uiteinden.
// 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
Curven gebruiken AddCurveToPath, wat twee Bezier-controlepunten en een eindpunt vereist: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). De curve loopt van het huidige punt naar (EndX, EndY), en wordt onderweg naar de twee controlepunten getrokken. Cirkelbogen zijn beschikbaar via AddArcToPath(CenterX, CenterY, TotalAngle), waarbij de straal wordt afgeleid van de afstand tussen het huidige punt en het middelpunt, en de engine de boog verzendt als een keten van Bezier-segmenten. Rechthoeken hebben een kortere weg, AddBoxToPath(Left, Top, Width, Height), die een volledige gesloten rechthoek toevoegt als een eigen subpad zonder een voorafgaande StartPath.
Twee vulregels en waarom ze verschillen
Wanneer u een pad vult dat zichzelf kruist of een binnenste lus bevat, de renderer heeft een regel nodig om te bepalen welke regio's zich binnen de vorm bevinden en welke gaten zijn. ISO 32000-1 §8.5.3.3 definieert er twee, en deze kunnen dezelfde geometrie verschillend inkleuren. De nonzero winding-regel telt de getekende kruisingen van een straal die vanaf een testpunt naar het oneindige wordt gewerpen, waarbij er één wordt opgeteld voor elk segment dat van links naar rechts kruist en één wordt afgetrokken voor elk segment dat de andere kant op kruist; het punt bevindt zich binnen de vorm als het totaal niet nul is. De even-odd-regel negeert de richting en telt simpelweg het aantal kruisingen, waarbij het punt als binnen de vorm wordt beschouwd als het aantal oneven is.
Het klassieke geval waarin ze uiteenlopen is een vorm met een gat, zoals een donut of een ring. Teken een buitenste grens en een binnenste grens daarbinnen. Onder de even-odd-regel snijdt de binnenste lus altijd een gat uit, omdat elk punt tussen de twee grenzen één keer wordt gekruist en elk punt binnen de binnenste lus twee keer. Onder de nonzero winding-regel verschijnt het gat alleen als de binnenste lus in de tegenovergestelde richting van de buitenste draait; als ze in dezelfde richting draaien, versterken de wikkelingen elkaar in plaats van elkaar op te heffen, en wordt het binnenste gebied volledig gevuld. Een vijfpuntige ster die als een enkele zichzelf kruisende omtrek is getekend, laat dezelfde splitsing zien: even-odd laat de centrale vijfhoek leeg, terwijl nonzero winding deze vult.
PDFlibPas selecteert de regel op basis van de aanroep die u doet om te tekenen, en niet via een vlag. DrawPath vult met de nonzero winding-regel; DrawPathEvenOdd vult met de even-odd-regel. Beide vereisen dezelfde integer-modus: 0 omlijnt alleen de omtrek, 1 vult alleen, en 2 vult en omlijnt. De even-odd-regel is het gemakkelijkere hulpmiddel voor het uitsnijden van gaten, juist omdat u de richting van het subpad niet hoeft te beheren.
// 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
Axiale verlopen variëren de kleur langs een lijn
Een vlakke vulkleur heeft één waarde over het hele gebied. Een verloop varieert de kleur continu, en the eenvoudigste soort is het axiale, of lineaire, verloop. ISO 32000-1 §8.7.4.5 specificeert dit als een Type 2 axiale arcering (axial shading): u geeft twee punten op die een as definieëren, een startkleur bij het eerste punt en een eindkleur bij het tweede, en de renderer interpoleert de kleur langs die as. Elk punt in het gevulde gebied krijgt de kleur van zijn loodrechte projectie op de as, zodat het verloop in banden haaks op de lijn tussen de twee punten loopt.
In PDFlibPas is een verloop een benoemde documentbron die u eenmalig maakt en vervolgens selecteert als de actieve kleuring. NewRGBAxialShader registreert dit. De signatuur is NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): de twee eindpunten van de as, de RGB-triplets aan elk uiteinde als waarden in het bereik van 0 tot 1, en een Extend-vlag. Met Extend ingesteld op 1 lopen de eindkleuren door als een effen vulling voorbij de eindpunten van de as. Dit is meestal gewenst, zodat de hoeken van een gebied buiten de as niet onbeschilderd blijven; 0 laat ze ongemoeid. Zodra de shader bestaat, koppelt u deze met SetFillShader voor gevulde gebieden, SetLineShader voor omlijnde omtrekken of SetTextShader voor tekst. De koppeling blijft actief voor de tekenaanroepen die volgen, zodat het pad dat u daarna tekent het verloop aanneemt in plaats van een vlakke kleur.
// 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
De as is hier verticaal, van y=100 naar y=260 bij een vaste x, dus de kleurbanden lopen horizontaal en de rechthoek vervaagt van blauw aan de basis naar wit aan de bovenkant. Omdat de shader via een naam wordt geïdentificeerd, kan één definitie een willekeurig aantal vormen op de pagina vullen, en terugschakelen naar een vlakke kleur is gewoon een kwestie van een nieuwe SetFillColor-aanroep voor het volgende pad.
Tiling-patronen herhalen een cel
Waar een verloop een enkele kleur vloeiend varieert, een tiling-patroon herhaalt een klein stukje artwork over een gebied. ISO 32000-1 §8.7.3.1 definieert een tiling-patroon als een patrooncel, een onafhankelijk stukje inhoud dat de renderer op een vast grid repliceert om het te tekenen gebied te betegelen (tilen). Dit is hoe u arceringen bouwt voor een technische vulling, een herhalend merkmotief achter een header of een getextureerde achtergrond die vector-scherp blijft en bijna niets weegt, hoe groot het gebied ook is, omdat de cel eenmalig wordt opgeslagen en overal wordt gerefereerd.
PDFlibPas bouwt de patrooncel op uit vastgelegde pagina-inhoud. U legt een pagina of een gebied vast met CapturePage, zet de vastlegging om in een benoemd patroon met NewTilingPatternFromCapturedPage(PatternName, CaptureID), en selecteert vervolgens dat patroon als de huidige vulling met SetFillTilingPattern(PatternName). Vanaf dat moment wordt elk pad dat u vult geverfd met de herhalende cel in plaats van een vlakke kleur, precies zoals een shader-vulling werkt, maar met een betegelde cel als bron. De volgorde is complexer dan een enkele aanroep, dus als de vastleggingsstap onbekend is, behandel het patroon dan als een bewerking in twee fasen: produceer eerst de vastgelegde cel en koppel deze vervolgens als vulling op naam voordat u het gebied tekent dat u wilt betegelen.
De basiselementen combineren
De onderdelen laten zich direct combineren. Een gevulde Bezier-vorm is een pad van curven dat is getekend met DrawPath. Dezelfde omtrek getekend met DrawPathEvenOdd na het toevoegen van een binnenste lus toont een gat dat de winding-vulling gesloten zou hebben. Een met een verloop gevulde rechthoek is een box die gekoppeld is aan een shader. Het onderstaande voorbeeld tekent ze alle drie achter elkaar, zodat het verschil tussen de twee vulregels zichtbaar is op één pagina, en legt er vervolgens een verlooppaneel onder.
// 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);
Twee details zijn belangrijk om te onthouden. De tekenaanroep bepaalt de vulregel, dus de keuze tussen DrawPath en DrawPathEvenOdd is de keuze tussen nonzero winding en even-odd, en voor vormen met gaten de even-odd-regel bespaart u het nadenken over de richting van het subpad. En de grafische status wordt gesampled op het moment dat u tekent: stel uw kleuren, lijnbreedte en shader-koppeling in vóór de tekenaanroep, want dat is de status die de engine leest. Eerst construeren, de status configureren, als laatste tekenen, en het vectormodel gedraagt zich elke keer voorspelbaar.
Vanaf hier zijn de logische volgende stappen het teruglezen van vectoren en tekst uit een bestaand document, behandeld in ons artikel over tekst-, afbeelding- en lettertype-extractie, en het renderen van hetzelfde tekenmodel naar een Windows-apparaatcontext (device context) voor preview op het scherm en afdrukken, behandeld in de handleiding voor afdrukken en previews. De pad-, shader- en patroonaanroepen die hier worden beschreven, worden geleverd als onderdeel van de Delphi PDF Library naast de API's voor tekst, afbeeldingen, formulieren en handtekeningen die elders op deze blog worden besproken.