Technisch artikel

HotPDF Canvas Tekenen in Delphi: Vectorpaden en Kleur

HotPDF tekent vectorafbeeldingen door een pad op de huidige pagina op te bouwen en er vervolgens om te vragen dat het wordt geschilderd. Er is geen bitmapstap tussendoor. Een lijn die u tekent met MoveTo en LineTo eindigt als PDF-padoperatoren in de contentstream, dus het blijft een echte vector: scherp bij 50% zoom, scherp bij 1600%, en een fractie van de grootte die een gerasterde versie zou kosten. Voor diagrammen, tabelregels, grafiekassen en formulierdecoraties is dat precies wat u wilt, en de API erachter is klein genoeg om in één zitting te leren

Het hele tekenoppervlak leeft op THotPDF.CurrentPage. Tussen BeginDoc en EndDoc stelt u kleur en lijndikte in op dat paginaobject, legt u geometrie neer en roept u een tekenoperator aan om het vast te leggen. De vier primitieven die u het meest zult gebruiken, zijn MoveTo en LineTo voor willekeurige paden, Rectangle voor vakken, Circle voor schijven, en de twee schilderoperatoren Stroke en Fill

Het coördinatensysteem is linksonder

Dit is het enige dat iedereen laat struikelen die vanuit VCL komt. Het TCanvas waarmee u controls schildert, plaatst de oorsprong in de linkerbovenhoek waarbij Y naar beneden groeit. PDF doet het tegenovergestelde. HotPDF meet vanaf de linkerbenedenhoek van de pagina in punten (1/72 inch), waarbij Y toeneemt naarmate u omhoog gaat. Een punt op Y := 720 bevindt zich nabij de bovenkant van een US Letter-pagina, die 792 punten hoog is, en Y := 50 bevindt zich nabij de onderkant. Als uw eerste tekening verticaal gespiegeld verschijnt, komt dit hierdoor: code geporteerd van schermgrafiek veronderstelt de verkeerde richting en loopt van de onderrand af

Dezelfde conventie is van toepassing op TextOut, dus tekst en vormen delen één mentaal model zodra u het zich eigen maakt. Plan een lay-out door te beslissen waar de onderkant van elk element zich bevindt, niet de bovenkant, en de rest volgt vanzelf

Vergelijking van de oorsprong linksboven van het schermcanvas en de PDF-oorsprong linksonder: hetzelfde punt nabij de bovenkant van een US Letter-pagina leest Y = 72 in TCanvas-code maar Y = 720 in HotPDF-punten, dus geportte code zonder het omslaan van Y tekent gespiegeld
HotPDF meet vanaf de hoek linksonder in punten, dus een plek nabij de bovenkant van de 612-bij-792-pagina draagt Y = 720 — dezelfde fysieke locatie die TCanvas-code aanspreekt als een kleine neerwaartse Y

Paden: MoveTo, LineTo, Stroke

Een omlijnd pad is een pen die is opgetild, geplaatst en gesleept. MoveTo tilt de pen op en stelt het startpunt in zonder iets te markeren. Elke LineTo breidt het huidige pad uit naar een nieuw punt. Er verschijnt niets op de pagina totdat u Stroke aanroept, dat het verzamelde pad tekent met behulp van de huidige omlijningskleur en lijndikte, en vervolgens het pad wist zodat de volgende MoveTo vers begint

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // Lijndikte is in punten en geldt totdat u deze wijzigt.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Een horizontale lijn bovenaan de pagina (Y gemeten vanaf de onderkant).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // leg het pad vast; hiervoor is nog niets getekend

    // Een dikkere verbonden polylijn: drie segmenten in één pad.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Twee details besparen echt debugging-tijd. Lijndikte is status, geen argument: SetLineWidth stelt het eenmaal in en elke daaropvolgende Stroke gebruikt die waarde totdat u deze opnieuw wijzigt, wat de reden is waarom de bovenstaande polylijn dikker is dan de regel. En het pad wordt na elke Stroke gereset, dus een vergeten Stroke betekent dat de geometrie die u zo zorgvuldig hebt opgebouwd helemaal nooit wordt weergegeven. Als er een vorm ontbreekt in de uitvoer, is de schilderaanroep de eerste plek om te kijken

De coördinaten zijn punten, en punten zijn fractioneel. MoveTo en LineTo accepteren Single waarden, dus een haarlijn van 0.5 punten of een positie op 72.25 is geldig en betekenisvol, en wordt niet afgerond naar de dichtstbijzijnde hele eenheid. Die precisie is in twee tegengestelde richtingen van belang. Een lijndikte van minder dan ongeveer 0.5 kan worden weergegeven als een apparaatafhankelijke kleinst mogelijke lijn die op het scherm verdwijnt en bij het afdrukken weer verschijnt, dus een zichtbare regel vereist een dikte die u met opzet instelt in plaats van de standaardwaarde. Aan de andere kant voorkomt het uitlijnen van tabelregels en rasterlijnen op hele puntcoördinaten dat een dicht raster er enigszins ongelijkmatig uitziet, waar aangrenzende lijnen anders zouden afronden. Bepaal de rasterafstand vooraf in punten en de rest van de lay-out neemt dit over

Gevulde vormen en kleur

Gesloten primitieven kunnen worden gevuld in plaats van omlijnd. Rectangle neemt een positie en een grootte, Circle neemt een middelpunt en een radius, en beide worden vastgelegd met Fill, dat het binnenste in de huidige vulkleur schildert, of met Stroke voor alleen een omlijning. Vulkleur en omlijningskleur zijn afzonderlijke statuselementen, ingesteld met SetRGBFillColor en SetRGBStrokeColor, die beide een enkele TColor aannemen. Dat betekent dat u Delphi's kleurconstanten en de RGB-helper direct kunt hergebruiken

HotPDF-padmodel: MoveTo, LineTo, Rectangle en Circle bouwen een onzichtbaar huidig pad in het geheugen, en alleen de commit Stroke, Fill of FillAndStroke verft het, gebruikmakend van de persistente graphics state van strokekleur, fillkleur en lijnbreedte, voordat de buffer wordt gewist
Geometrie hoopt zich stilletjes op in het huidige pad totdat een painting-operator hem commit met de bewaarde stroke-kleur, vulkleur en lijndikte — een vergeten paint-aanroep laat de vorm ongetekend
// Rectangle(X, Y, Width, Height): X en Y zijn de linkerbenedenhoek.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X en Y zijn het middelpunt.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Alleen omlijning: stel een lijnkleur en -dikte in, en roep dan Stroke aan.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Let op de vorm van de argumenten bij Rectangle. Het is positie-plus-grootte, X, Y, Width, Height, niet twee tegenovergestelde hoeken. De TCanvas.Rectangle die Delphi-ontwikkelaars kennen neemt (Left, Top, Right, Bottom), dus door spiergeheugen zult u HotPDF een tweede hoek aanreiken waar het een breedte en een hoogte verwacht, en het vak krijgt de verkeerde grootte. Het (X, Y)-paar is de linkerbenedenhoek, consistent met de oorsprong van de pagina. Voor een cirkel is (X, Y) het middelpunt en het derde argument de straal in punten

Één kleurkeuze die het originele voorbeeld verkeerd had

Een oudere versie van dit voorbeeld voorzag kleuren van een beginwaarde met Random($FFFFFF) voor elke vorm. Het ziet er levendig uit, en het is het verkeerde instinct voor gegenereerde documenten. Een PDF die u via code bouwt, is meestal iets dat u ook wilt testen, en willekeurige vulkleuren maken de uitvoer onmogelijk te vergelijken van de ene run op de andere: een byte-voor-byte vergelijking (diff) tegen een bekend-goed bestand mislukt elke keer, zonder echte reden. Kies expliciete kleuren. Wanneer u variatie wilt in een reeks vormen, stuur dit dan aan vanuit uw gegevens of een vaste palet-array, zodat dezelfde invoer altijd hetzelfde bestand oplevert. Determinisme is meer waard dan originaliteit wanneer het artefact door een release-pijplijn beweegt

De primitieven samenvoegen: een callout-box

Elke primitief is op zichzelf eenvoudig; de opbrengst blijkt wanneer een handvol ervan samensmelt tot iets wat een rapport daadwerkelijk nodig heeft. Een callout, het geannoteerde vak dat naar een figuur wijst en dit uitlegt, gebruikt alles wat tot nu toe is behandeld: een gevulde rechthoek met een rand, een omlijnde aanwijzerlijn, een stip om de aanwijzer te verankeren, en tekst die binnen het vak is opgemaakt met dezelfde linksonder-coördinaten die de vormen gebruiken. FillAndStroke verdient hier zijn plek, door het binnenste en de omlijning van een pad in een enkele actie te schilderen in plaats van de rechthoek twee keer op te bouwen

Anatomie van een callout-box samengesteld uit HotPDF-primitieven: een rechthoek met FillAndStroke vastgelegd, een getrokken aanwijslijn uit MoveTo en LineTo, een gevulde ankerpunt, en TextOut-labelbalken die hetzelfde linksonder-raster delen als de vormen
Vier commits bouwen de callout — FillAndStroke verft paneel en rand samen, de aanwijzer en stip hergebruiken stroke- en fill-status, en elke label-offset is simpele rekenkunde tegen de boxhoek (90, 600)
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. Het vak: lichte vulling plus een zichtbare rand, één pad, één commit.
    //    Rectangle is linkerbenedenhoek plus grootte, Y gemeten vanaf de onderkant
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // lichtamberkleurig paneel
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // donkerdere rand
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. De aanwijzer: één omlijnd segment vanaf de rand van het vak naar beneden
    //    richting het geannoteerde object
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // linkerrand van het vak
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. Een gevulde stip verankert de aanwijzer op zijn doel
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. Het label, gepositioneerd ten opzichte van de linkerbenedenhoek van het vak.
    //    Tekst en vormen delen één coördinatensysteem, dus de offsets
    //    zijn gewone rekenkunde ten opzichte van (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Merk op hoe weinig statusbeheer de compositie nodig heeft. Vulkleur, omlijningskleur en lijndikte worden elk direct vóór de vorm die ze gebruikt ingesteld, zodat elk blok van de tekening leest als een opzichzelfstaande eenheid en kan worden herschikt of geëxtraheerd naar een helper zonder verborgen status mee te slepen. Verpak dit in een procedure die het ankerpunt en de strings aanneemt, en u heeft een herbruikbare diagram-annotatie voor de moeite van veertig regels

Waar vector tekenen loont, en waar niet

Grijp naar deze pad- en vorm-aanroepen wanneer de geometrie wordt gegenereerd: rasterlijnen en balken van een grafiek, de gelinieerde regels van een factuurtabel, callout-boxen in een diagram, een logo uitgedrukt als een handvol paden. Dit alles schaalt zonder vervaging en voegt bijna niets toe aan de bestandsgrootte, want een rechthoek bestaat uit een paar getallen in plaats van duizenden pixels. De keerzijde is ook eerlijk. Als wat u daadwerkelijk heeft een foto of een screenshot is, teken het dan in plaats daarvan als een afbeelding met AddImage en ShowImage; een bitmap overtrekken met vector-aanroepen levert u niets op. De rechte segmenten, rechthoeken en cirkels hierboven dragen de grote meerderheid van het echte rapportagewerk, en de drie verfijningen waar ontwikkelaars als volgende naar vragen, krommen, stippelpatronen en transparantie, bevinden zich op hetzelfde paginaobject

Krommen, stippellijnen en transparantie in het kort

Vrije-vorm krommen (curves) breiden hetzelfde padmechanisme uit dat u al heeft. CurveToC(X1, Y1, X2, Y2, X3, Y3) voegt een kubisch Bezier-segment toe van het huidige punt naar (X3, Y3), buigend naar de twee controlepunten, en de steno-varianten CurveToV en CurveToY dekken de gevallen waarbij één controlepunt samenvalt met een eindpunt. Een pad kan LineTo en CurveToC segmenten vrijelijk mixen voordat een enkele Stroke of Fill het vastlegt, wat de manier is waarop afgeronde hoeken en vloeiende grafieklijnen worden gebouwd

Onderbroken omlijningen (dashes) zijn status, precies zoals lijndikte. SetDash([3, 3], 0) schakelt elke daaropvolgende omlijning over naar een drie-punten-aan, drie-punten-uit patroon, waarbij de array de aan/uit-runlengtes in punten specificeert en het tweede argument faseert waar de cyclus start; NoDash keert de pen terug naar een ononderbroken lijn. Stel het in, trek de rasterlijnen die het nodig hebben, en reset het voor de volgende ononderbroken regel, anders infecteert het stippelpatroon stilletjes alles wat volgt

Transparantie verloopt via een benoemde grafische status in plaats van een kleurargument, omdat alfa in PDF een eigenschap is van de graphics state dictionary. Registreer er een op het document met RegisterExtGState, waarbij u een vul-alfa en een omlijnings-alfa tussen 0 en 1 doorgeeft, en pas vervolgens de naam die dit retourneert toe met CurrentPage.SetGraphicsState; vullingen en omlijningen vanaf dat punt worden geschilderd met de geregistreerde dekking. Het is een zwaardere ceremonie dan de kleurinstellingen, maar is het waard de eerste keer dat een markeerbalk over tekst heen moet liggen zonder deze te verbergen

De resterende gewoonte die het waard is om te behouden, is verificatie. Gegenereerde geometrie kan op uw machine slagen en bij een klant falen, meestal vanwege lettertype-substitutie in tekst die u erin mengt, of een aanname over het paginaformaat die geen standhoudt. Open het voltooide bestand op een paar zoomniveaus om te bevestigen dat de randen strak blijven, en controleer of elke vorm binnen het bedoelde margekader landt. Met een deterministisch kleurenschema kan die controle worden geautomatiseerd tegen een referentie-PDF in plaats van op het oog te worden beoordeeld

De hier getoonde MoveTo, LineTo, Stroke, Fill en kleur-aanroepen maken deel uit van de HotPDF Delphi Component voor Delphi en C++Builder