Tehnički članak

Crtanje na HotPDF platnu u Delphiju: Vektorske putanje i boje

HotPDF crta vektorsku grafiku tako što gradi putanju na trenutnoj stranici, a zatim traži da se ona iscrta. Između toga ne postoji korak kreiranja bitmape. Linija koju nacrtate pomoću MoveTo i LineTo završava kao operator putanje PDF-a u toku sadržaja (content stream), tako da ostaje pravi vektor: oštar pri uvećanju od 50%, oštar pri 1600%, a delić veličine koliko bi koštala rasterizovana verzija. Za dijagrame, linije u tabelama, ose grafikona i dekoracije formi, to je upravo ono što želite, a API iza toga je dovoljno mali da se nauči u jednom dahu

Cela površina za crtanje živi na THotPDF.CurrentPage. Između BeginDoc i EndDoc podešavate boju i širinu linije na tom objektu stranice, postavljate geometriju i pozivate operator za iscrtavanje da je primenite. Četiri osnovna elementa (primitives) koja ćete najviše koristiti su MoveTo i LineTo za proizvoljne putanje, Rectangle za okvire, Circle za krugove i dva operatora iscrtavanja, Stroke i Fill

Koordinatni sistem je od donjeg levog ugla

To je jedina stvar koja spotiče svakoga ko dolazi iz VCL-a. TCanvas pomoću kojeg iscrtavate kontrole stavlja nultu tačku (origin) u gornji levi ugao, a Y raste nadole. PDF radi suprotno. HotPDF meri od donjeg levog ugla stranice u tačkama (1/72 inča), sa Y koje se povećava kako se krećete nagore. Tačka na Y := 720 nalazi se blizu vrha stranice US Letter formata, koja je visoka 792 tačke, a Y := 50 nalazi se blizu dna. Ako vaš prvi crtež ispadne vertikalno preslikan, ovo je razlog: kod prebačen (ported) iz ekranske grafike pretpostavlja pogrešan smer i nestaje sa donje ivice

Ista konvencija upravlja funkcijom TextOut, tako da tekst i oblici dele isti mentalni model kada ga jednom usvojite. Planirajte raspored tako što ćete odlučiti gde se nalazi dno svakog elementa, a ne vrh, i sve ostalo dolazi samo od sebe

Putanje: MoveTo, LineTo, Stroke

Iscrtana putanja je olovka koja je podignuta, spuštena i prevučena. MoveTo podiže olovku i postavlja početnu tačku, a da ništa ne obeleži. Svaki LineTo produžava trenutnu putanju do nove tačke. Ništa se ne pojavljuje na stranici dok ne pozovete Stroke, koji crta prikupljenu putanju koristeći trenutnu boju ivice (stroke color) i širinu linije, a zatim čisti putanju tako da sledeći MoveTo započne ponovo

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 detalja štede pravo vreme za debagovanje. Širina linije je stanje, a ne argument: SetLineWidth je postavlja jednom i svaki sledeći Stroke koristi tu vrednost dok je ponovo ne promenite, zbog čega je izlomljena linija (polyline) iznad deblja od prve linije. Takođe, putanja se resetuje nakon svakog Stroke, pa zaboravljeni Stroke znači da se geometrija koju ste tako pažljivo postavili uopšte ne renderuje. Ako oblik nedostaje u izlazu, poziv iscrtavanja je prvo mesto na koje treba pogledati

Koordinate su u tačkama (points), a tačke su frakcione (sa decimalama). MoveTo i LineTo prihvataju vrednosti tipa Single, tako da je veoma tanka linija (hairline) na 0.5 tačaka ili pozicija na 72.25 legalna i smislena, a ne zaokružena na najbližu celu jedinicu. Ta preciznost je važna u dva suprotna smera. Širina linije manja od oko 0.5 može da se renderuje kao najtanja moguća linija zavisna od uređaja koja nestaje na ekranu a ponovo se pojavljuje prilikom štampanja, tako da vidljiva linija zahteva širinu koju namerno postavite, a ne podrazumevanu. Sa druge strane, poravnavanje (snapping) linija tabela i mreže (grid) na koordinate sa celim brojem sprečava da gusta mreža izgleda malo neravnomerno na mestima gde se susedne linije različito zaokružuju. Unapred odlučite o razmaku mreže u tačkama i ostatak izgleda (layout) će to naslediti

Popunjeni oblici i boja

Zatvoreni osnovni elementi mogu biti popunjeni umesto iscrtani spolja. Rectangle prima poziciju i veličinu, Circle prima centar i poluprečnik, a bilo koji od njih se primenjuje sa Fill, koji boji unutrašnjost trenutnom bojom ispune, ili sa Stroke samo za konturu (outline). Boja ispune i boja ivice su odvojeni delovi stanja, postavljeni sa SetRGBFillColor i SetRGBStrokeColor, pri čemu oba primaju po jedan TColor. To znači da možete direktno ponovo da iskoristite Delphi-jeve konstante boja i pomoćnu funkciju 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 oblik argumenata za Rectangle. To je pozicija plus veličina, X, Y, Width, Height, a ne dva suprotna ugla. TCanvas.Rectangle koji programeri u Delphiju poznaju prihvata (Left, Top, Right, Bottom), pa će mišićna memorija HotPDF-u proslediti drugi ugao tamo gde on očekuje širinu i visinu, a pravougaonik ispadne pogrešne veličine. Par (X, Y) je donji levi ugao, dosledan sa nul-tačkom stranice. Za krug, (X, Y) predstavlja centar, a treći argument je poluprečnik u tačkama

Jedan izbor boje kod kojeg je originalni primer pogrešio

Starija verzija ovog primera inicijalizovala je boje pomoću Random($FFFFFF) na svakom obliku. To izgleda živo, ali je to pogrešan instinkt za generisane dokumente. PDF koji gradite iz koda je obično nešto što takođe želite da testirate, a nasumične boje popunjavanja čine izlaz nemogućim za poređenje od pokretanja do pokretanja: poređenje bajt po bajt u odnosu na provereno ispravnu datoteku (known-good) pada svaki put, bez pravog razloga. Izaberite eksplicitne boje. Kada želite raznolikost kroz seriju oblika, vodite je iz svojih podataka ili niza fiksne palete, tako da isti unos uvek proizvodi istu datoteku. Determinizam vredi više od nečeg novog kada se radni materijal (artifact) kreće kroz kanal za izdavanje (release pipeline)

Spajanje osnovnih elemenata: okvir sa objašnjenjem (callout box)

Svaki osnovni element je jednostavan sam po sebi; isplativost se pokazuje kada se nekoliko njih spoji u nešto što izveštaj zapravo zahteva. Okvir sa objašnjenjem (callout), obeleženo polje koje pokazuje na sliku i objašnjava je, koristi sve do sada pređeno: popunjen pravougaonik sa okvirom, obeleženu liniju pokazivača (pointer line), tačku koja sidri pokazivač, kao i tekst postavljen unutar pravougaonika koristeći iste koordinate donjeg levog ugla koje koriste oblici. FillAndStroke ovde zaslužuje svoje mesto, bojadišući i unutrašnjost i konturu jedne putanje u jednoj primeni umesto da dvaput gradi pravougaonik

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;

Primetite koliko malo upravljanja stanjem treba složenom crtežu. Boja ispune, boja ivice i širina linije su postavljene neposredno pre oblika koji ih koristi, pa se svaki blok crteža čita kao samostalna celina i može se preurediti ili izvući u pomoćnu funkciju (helper) bez povlačenja skrivenog stanja sa sobom. Umetnite ovo u proceduru koja uzima tačku sidrenja i tekst (strings), i imate napomenu dijagrama (diagram annotation) spremnu za ponovnu upotrebu (reusable) za cenu od četrdesetak linija

Gde se isplati vektorsko crtanje, a gde ne

Posegnite za ovim pozivima za putanju i oblik kada se geometrija generiše: linije i trake na grafikonu, linije tabele fakture, okviri sa objašnjenjima na dijagramu, logo znak izražen kao šačica putanja. Sve se to skalira bez zamućenja i ne dodaje gotovo ništa na veličinu datoteke, jer je pravougaonik samo nekoliko brojeva a ne hiljade piksela. Druga strana medalje je takođe iskrena. Ako ono što zapravo imate jeste fotografija ili snimak ekrana, nacrtajte ih kao sliku pomoću funkcija AddImage i ShowImage; praćenje (tracing) bitmape pomoću vektorskih poziva vam ne donosi ništa. Ravni segmenti, pravougaonici i krugovi iznad nose ogromnu većinu stvarnog posla oko pravljenja izveštaja, a tri usavršavanja koja programeri dalje traže – krive, isprekidane linije (dash patterns) i providnost (transparency) – nalaze se na istom objektu stranice

Krive, isprekidane linije i providnost ukratko

Krive slobodnog oblika (Freeform) produžavaju istu mašineriju putanja koju već imate. CurveToC(X1, Y1, X2, Y2, X3, Y3) dodaje kubični Beziyeov segment od trenutne tačke do (X3, Y3), savijajući se ka dve kontrolne tačke, a skraćene varijante CurveToV i CurveToY pokrivaju slučajeve u kojima se jedna kontrolna tačka poklapa sa završnom. Putanja može slobodno da meša LineTo i CurveToC segmente pre nego što je jedan Stroke ili Fill primeni (commits), a to je način na koji se kreiraju zaobljeni uglovi i glatke linije grafikona

Isprekidane ivice su stanje, tačno kao i širina linije. SetDash([3, 3], 0) prebacuje svaki sledeći Stroke u obrazac od tri tačke uključeno i tri tačke isključeno (three-point-on, three-point-off), gde niz (array) navodi dužinu crtica/razmaka u tačkama, a drugi argument faza definiše gde ciklus počinje; NoDash vraća olovku u punu liniju. Podesite to, iscrtajte linije mreže kojima to treba, a zatim vratite na podrazumevano stanje (reset) pre sledeće pune linije, inače će isprekidana linija tiho zaraziti sve što sledi

Providnost ide kroz imenovani grafički status (named graphics state), a ne argument boje, zato što je alfa kanal u PDF-u svojstvo rečnika grafičkih stanja. Registrujte jedno u dokumentu pomoću funkcije RegisterExtGState, prosleđujući fill alpha (alfu ispune) i stroke alpha (alfu ivice) između 0 i 1, a onda primenite ime koje ona vrati pomoću CurrentPage.SetGraphicsState; od tog trenutka ispuna (fills) i iscrtavanje (strokes) se obavljaju po registrovanom stepenu neprozirnosti (opacity). To je malo zahtevniji posao u poređenju s postavljačima (setters) za boju, i to se isplati prvi put kada traka za isticanje (highlight bar) mora da stoji preko teksta a da ga ne sakrije

Preostala navika koju vredi zadržati je potvrđivanje (verification). Generisana geometrija može da prođe na vašoj mašini, a da otkaže na mašini klijenta, najčešće zbog zamene fonta u bilo kom tekstu koji umešate ili zbog pretpostavke veličine stranice koja više ne važi. Otvorite završenu datoteku na par nivoa uveličanja (zoom) da biste proverili da ivice ostaju čiste i proverite da svaki oblik upada unutar okvira margine koju ste zamislili. Zbog determinističke sheme boja, provera može da bude automatska umesto oslanjanja samo na vizuelni pregled upoređivanjem sa referentnim PDF-om

Pozivi MoveTo, LineTo, Stroke, Fill i funkcije za boje koji su ovde prikazani deo su HotPDF komponente za Delphi i C++Builder