Tehnični članak

Risanje po platnu HotPDF v Delphiju: Vektorske poti in barve

HotPDF riše vektorsko grafiko tako, da zgradi pot (path) na trenutni strani in nato zahteva, da se le-ta pobarva (painted). Vmes ni nobenega bitnega koraka (bitmap step). Črta, ki jo narišete z MoveTo in LineTo, konča kot operaterji poti PDF (PDF path operators) v toku vsebine, tako da ostane pravi vektor: ostra pri 50-odstotni povečavi, ostra pri 1600-odstotni in v velikosti le delčka tistega, kar bi stala rastrirana različica (rasterized version). Za diagrame, mrežne črte tabel (table rules), osi grafikonov in okraske obrazcev je to točno to, kar želite, in API, ki stoji za tem, je dovolj majhen, da se ga naučite v enem sedenju

Celotna risalna površina živi na THotPDF.CurrentPage. Med BeginDoc in EndDoc na tem predmetu strani (page object) nastavite barvo in debelino črte (line width), položite geometrijo in pokličete operaterja barvanja, da jo uveljavi (commit). Štirje primitivi, ki jih boste največ uporabljali, so MoveTo in LineTo za poljubne poti, Rectangle za škatle, Circle za kroge in dva operaterja barvanja, Stroke in Fill

Koordinatni sistem je spodaj-levo

To je tista stvar, ob katero se spotakne vsakdo, ki prihaja iz VCL. TCanvas, s katerim barvate kontrolnike, postavi izhodišče (origin) v zgornji levi kot, pri čemer Y raste navzdol. PDF počne nasprotno. HotPDF meri od spodnjega levega kota strani v točkah (points - 1/72 palca), pri čemer Y narašča, ko se premikate navzgor. Točka pri Y := 720 leži blizu vrha strani velikosti US Letter, ki je visoka 792 točk, točka pri Y := 50 pa leži blizu dna. Če je vaša prva risba zrcaljena navpično, je to razlog: koda, prenesena iz zaslonske grafike, predpostavlja napačno smer in zbeži z dna

Ista konvencija velja za TextOut, tako da si besedilo in oblike delijo en miselni model, ko ga enkrat ponotranjite. Razporeditev (layout) načrtujte tako, da se odločite, kje sedi dno vsakega elementa, in ne vrh, ostalo pa sledi

Poti: MoveTo, LineTo, Stroke

Zarisana pot (stroked path) je dvignjeno, postavljeno in povlečeno pisalo (pen). MoveTo dvigne pisalo in nastavi začetno točko (start point), ne da bi karkoli označil. Vsak LineTo podaljša trenutno pot na novo točko. Na strani se ne prikaže nič, dokler ne pokličete Stroke, ki nariše nabrano pot z uporabo trenutne barve črte (stroke color) in debeline črte, nato pa pot počisti (clears), da se naslednji MoveTo začne na novo

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

    // Line width is in points and applies until you change it.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // A horizontal rule near the top of the page (Y measured from bottom).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // commit the path; nothing drew before this

    // A thicker connected polyline: three segments in one path.
    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;

Dva detajla prihranita dejanski čas pri odpravljanju napak. Debelina črte je stanje (state), ne argument: SetLineWidth jo nastavi enkrat in vsak nadaljnji Stroke uporablja to vrednost, dokler je znova ne spremenite, in to je razlog, zakaj je zgornja polilinija (polyline) debelejša od prejšnje črte. In pot se ponastavi po vsakem Stroke, zato pozabljen Stroke pomeni, da se geometrija, ki ste jo tako skrbno določili, sploh nikoli ne upodobi. Če na izhodu manjka oblika, je klic barvanja prvo mesto za pregled

Koordinate so točke (points), točke pa so ulomki. MoveTo in LineTo sprejemata vrednosti Single, zato sta lasnica (hairline) pri debelini 0.5 točke ali položaj pri 72.25 dovoljeni in smiselni vrednosti, in ne zaokroženi na najbližjo celo enoto. Ta natančnost je pomembna v dveh nasprotnih smereh. Debelina črte pod približno 0.5 se lahko upodobi kot od naprave odvisna najtanjša možna črta (device-dependent thinnest-possible line), ki na zaslonu izgine in se pri tiskanju ponovno pojavi, zato vidno pravilo zahteva debelino, ki jo namerno nastavite vi, namesto privzete vrednosti. Na drugi strani pa pripenjanje (snapping) mrežnih črt in robov tabel na koordinate v celih točkah preprečuje, da bi gosta mreža izgledala nekoliko neenakomerno tam, kjer se sosednje črte različno zaokrožijo (round differently). Vnaprej se odločite za razmik mreže (grid spacing) v točkah in preostanek postavitve (layout) ga bo podedoval

Polnjene oblike in barve

Zaprte primitive lahko namesto orisov polnite (fill). Rectangle vzame položaj in velikost, Circle vzame središče in polmer (radius), vsakega pa potrdite s Fill, ki pobarva notranjost v trenutni barvi polnila (fill color), ali s Stroke za samo oris (outline). Barva polnila in barva črte (stroke color) sta ločena dela stanja (state), nastavljena s SetRGBFillColor in SetRGBStrokeColor, ki oba vzameta posamezen TColor. To pomeni, da lahko neposredno uporabite Delphijeve barvne konstante in pomočnika RGB

// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Pazite na obliko argumenta na Rectangle. Gre za položaj-plus-velikost, X, Y, Width, Height, ne za dva nasprotna kota. TCanvas.Rectangle, ki ga poznajo razvijalci v Delphiju, vzame (Left, Top, Right, Bottom), zato bo mišični spomin HotPDF-ju podal drugi kot na mestu, kjer pričakuje širino in višino, in škatla bo izpadla napačne velikosti. Par (X, Y) je spodnji levi kot, skladen z izhodiščem strani (page origin). Za krog, (X, Y) je središče, tretji argument pa je polmer (radius) v točkah

Ena izbira barve, v kateri se je izvirni vzorec zmotil

Starejša različica tega primera je barve naključno nastavila z Random($FFFFFF) na vsaki obliki. Izgleda živahno, a to je napačen instinkt za generirane dokumente. PDF, ki ga zgradite iz kode, je običajno nekaj, kar želite tudi preizkusiti, zaradi naključnih barv polnil (fill colors) pa je izhod nemogoče primerjati med zagoni (run to run): primerjava bajt za bajtom (byte-for-byte diff) v primerjavi z znano dobro datoteko pade vsakič in to brez pravega razloga. Izberite eksplicitne barve. Ko želite raznolikost med serijo oblik, jo poganjajte iz svojih podatkov ali fiksnega niza palete (fixed palette array), tako da isti vnos vedno proizvede isto datoteko. Determinizem (determinism) je vreden več kot novost, ko se izdelek premika po verigi za izdajo (release pipeline)

Sestavljanje primitivov: okence z oblačkom (callout box)

Vsak primitiv je preprost sam po sebi; učinek se pokaže, ko jih peščica sestavi (compose) v nekaj, kar poročilo dejansko potrebuje. Oblaček (callout), škatla z opombami (annotated box), ki kaže na sliko in jo razloži, uporablja vse doslej obravnavano: zapolnjen pravokotnik z obrobo, potegnjeno (stroked) kazalno črto, piko, ki zasidra kazalec, in besedilo, razporejeno v škatli z uporabo enakih koordinat spodaj levo, kot jih uporabljajo oblike. FillAndStroke si tukaj prisluži svoje mesto, saj prebarva notranjost in obris ene poti (path) v eni sami potrditvi (single commit), namesto da bi dvakrat zgradil pravokotnik

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

    // 1. The box: pale fill plus a visible border, one path, one commit.
    //    Rectangle is lower-left corner plus size, Y measured from the bottom
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // pale amber panel
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // darker rim
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. The pointer: one stroked segment from the box edge down
    //    toward the thing being annotated
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // left edge of the box
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. A filled dot anchors the pointer at its target
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. The label, positioned relative to the box's lower-left corner.
    //    Text and shapes share one coordinate system, so the offsets
    //    are plain arithmetic against (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;

Opazite, kako malo upravljanja stanja (state management) potrebuje sestavljanka. Barva polnila (fill color), barva črte (stroke color) in debelina črte so vse nastavljene neposredno pred obliko, ki jih uporablja, tako da se vsak blok risbe bere kot samostojna enota (self-contained unit) in se ga lahko prerazporedi ali izloči (extracted) v pomočnika, ne da bi ob tem vlekel s sabo skrito stanje. Zavijte to v proceduro, ki vzame sidrišče (anchor point) in nize (strings), in imate ponovno uporabno anotacijo diagrama za ceno štiridesetih vrstic

Kje se vektorsko risanje obrestuje in kje ne

Posezite po teh klicih poti in oblik (path and shape calls), ko je geometrija generirana: mrežne črte in stolpci na grafikonih, zarisanje črt in vrstic v tabeli računov, okenca z oblački na diagramu, znak logotipa, izražen kot peščica poti (paths). Vse to se poveča (scales) brez zamegljenosti in k velikosti datoteke skorajda ne doda ničesar, saj je pravokotnik le nekaj številk namesto tisočev slikovnih pik. Tudi nasprotna stran je iskrena. Če imate dejansko fotografijo ali posnetek zaslona, jo raje narišite kot sliko z AddImage in ShowImage; sledenje bitni sliki (tracing a bitmap) z vektorskimi klici vam ne prinese ničesar. Ravni segmenti, pravokotniki in krogi zgoraj predstavljajo veliko večino pravega poročevalskega dela, in tri izboljšave, o katerih razvijalci sprašujejo kot naslednjih, krivulje, vzorci črtic (dash patterns) in prosojnost, sedijo na istem predmetu strani (page object)

Na kratko o krivuljah, črticah in prosojnosti

Krivulje proste oblike (freeform curves) razširijo isti mehanizem poti (path machinery), kot ga že imate. CurveToC(X1, Y1, X2, Y2, X3, Y3) pripne kubični segment Bezier iz trenutne točke v (X3, Y3), ki se upogiba proti dvema kontrolnima točkama (control points), bližnjični različici (shorthand variants) CurveToV in CurveToY pa pokrijeta primere, ko ena kontrolna točka sovpada s končno točko (endpoint). Pot lahko prosto meša segmente LineTo in CurveToC preden jo potrdi (commits) en posamezen Stroke ali Fill, s čimer se zgradijo zaobljeni robovi in gladke črte na grafikonih

Črtkane poteze (dashed strokes) so stanje (state), popolnoma enako kot debelina črte. SetDash([3, 3], 0) preklopi vsak nadaljnji poteg črte na vzorec "tri-točke-vklopljeno, tri-točke-izklopljeno", pri čemer polje izčrkuje (spelling out) dolžine nizov vklopljeno/izklopljeno v točkah in drugi argument fazira (phasing), kje se cikel začne; NoDash vrne pero v polno črto. Nastavite to, povlecite mrežne črte (gridlines), ki to zahtevajo, in to ponastavite (reset) pred naslednjim polnim pravilom (solid rule), ali pa bo črtkanje tiho okužilo vse, kar sledi

Prosojnost (transparency) gre skozi imenovano grafično stanje (named graphics state) namesto skozi argument barve, ker je alfa (alpha) v PDF-ju lastnost slovarja grafičnega stanja. Na dokumentu registrirajte enega s pomočjo RegisterExtGState, ki posreduje alfo za polnilo in alfo za črto med 0 in 1, nato pa uporabite ime, ki ga vrne s pomočjo CurrentPage.SetGraphicsState; polnila in poteze se od te točke naprej barvajo z registrirano motnostjo (opacity). To je težja ceremonija kot nastavljalniki barv (color setters), a vredna, ko mora vrstica za poudarjanje (highlight bar) sedeti nad besedilom in ga ob tem ne skrivati

Preostala navada, ki jo je vredno ohraniti, je preverjanje (verification). Ustvarjena geometrija lahko prestane test na vašem računalniku in pade na strankinem, običajno zaradi zamenjave pisave v besedilu, ki ste ga vmešali, ali zaradi predpostavke o velikosti strani, ki ne zdrži. Odprite končano datoteko pri nekaj stopnjah povečave (zoom levels), da se prepričate, da robovi ostanejo čisti, in preverite, da vsaka oblika pristane v škatli za rob (margin box), ki ste jo načrtovali. Z deterministično barvno shemo je mogoče to preverjanje avtomatizirati glede na referenčni PDF, namesto da se to ocenjuje na oko (eyeballed)

Tukaj prikazani klici MoveTo, LineTo, Stroke, Fill in barvni klici (color calls) so del komponente HotPDF Component za Delphi in C++Builder