Tekninen artikkeli

HotPDF-kankaan piirtäminen Delphissä: Vektoripolut ja väri

HotPDF piirtää vektorigrafiikkaa rakentamalla polun (path) nykyiselle sivulle ja pyytämällä sitten sen maalaamista. Välissä ei ole bittikarttavaihetta. Viiva, jonka piirrät MoveTo- ja LineTo-funktioilla, päätyy PDF-polkuoperaattoreiksi sisältövirtaan (content stream), joten se pysyy aitoina vektoreina: terävänä 50% zoomauksella, terävänä 1600% zoomauksella, ja se on murto-osa sen koosta, mitä rasteroitu versio maksaisi. Kaavioille, taulukoiden viivoille, kaavioiden akseleille ja lomakkeiden koristeille se on juuri sitä mitä haluat, ja sen takana oleva API on tarpeeksi pieni opittavaksi yhdeltä istumalta

Koko piirtopinta elää kohteessa THotPDF.CurrentPage. BeginDoc- ja EndDoc-kutsujen välillä asetat värin ja viivan leveyden tälle sivuobjektille, asetat geometrian paikoilleen ja kutsut maalausoperaattoria vahvistaaksesi sen. Neljä primitiiviä, joita käytät eniten, ovat MoveTo ja LineTo mielivaltaisille poluille, Rectangle laatikoille, Circle kiekoille ja kaksi maalausoperaattoria Stroke ja Fill

Koordinaatisto alkaa vasemmasta alakulmasta

Tämä on se yksi asia, joka saa kaikki VCL:stä saapuvat kompastumaan. TCanvas, jolla maalaat kontrolleja, asettaa origon vasempaan yläkulmaan ja Y kasvaa alaspäin. PDF tekee päinvastoin. HotPDF mittaa sivun vasemmasta alakulmasta pisteinä (1/72 tuumaa), ja Y kasvaa, kun siirryt ylöspäin. Piste kohdassa Y := 720 istuu lähellä US Letter -sivun yläreunaa, joka on 792 pistettä korkea, ja Y := 50 istuu lähellä pohjaa. Jos ensimmäinen piirustuksesi tulee ulos pystysuunnassa peilikuvana, tämä on syy: ruutugrafiikasta (screen graphics) siirretty koodi olettaa väärän suunnan ja juoksee ulos alareunasta

Sama konventio ohjaa TextOut-funktiota, joten teksti ja muodot jakavat yhden mentaalimallin, kunhan sisäistät sen. Suunnittele asettelu päättämällä, missä kunkin elementin alaosa istuu, ei yläosa, ja loppu seuraa perässä

Polut: MoveTo, LineTo, Stroke

Piirretty (stroked) polku on kynä, jota on nostettu, joka on asetettu ja jota on vedetty. MoveTo nostaa kynän ja asettaa aloituspisteen merkitsemättä mitään. Jokainen LineTo pidentää nykyistä polkua uuteen pisteeseen. Sivulle ei ilmesty mitään, ennen kuin kutsut Stroke-funktiota, joka piirtää kerätyn polun käyttäen nykyistä piirtoväriä (stroke color) ja viivan leveyttä, ja tyhjentää sitten polun, jotta seuraava MoveTo alkaa puhtaalta pöydältä

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

    // Viivan leveys on pisteinä ja se on voimassa kunnes muutat sitä.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Vaakasuora viiva lähellä sivun yläreunaa (Y mitattuna pohjasta).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // vahvista polku; mitään ei piirretty ennen tätä

    // Paksumpi yhdistetty murtoviiva: kolme segmenttiä yhdessä polussa.
    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;

Kaksi yksityiskohtaa säästää todellista virheenkorjausaikaa. Viivan leveys on tila (state), ei argumentti: SetLineWidth asettaa sen kerran ja jokainen myöhempi Stroke käyttää tuota arvoa, kunnes muutat sitä uudelleen, minkä vuoksi yllä oleva murtoviiva on paksumpi kuin tavallinen viiva. Ja polku nollautuu jokaisen Stroke-kutsun jälkeen, joten unohdettu Stroke tarkoittaa, että geometria, jonka niin huolellisesti asettelit, ei renderöidy koskaan. Jos jokin muoto puuttuu tulosteesta, maalauskutsu on ensimmäinen paikka etsiä vikaa

Koordinaatit ovat pisteitä (points), ja pisteet ovat murtolukuja. MoveTo ja LineTo hyväksyvät Single-arvoja, joten hiusviiva (hairline) arvossa 0.5 pistettä tai sijainti arvossa 72.25 on sallittu ja merkityksellinen, eikä sitä pyöristetä pois lähimpään kokonaisyksikköön. Tuolla tarkkuudella on merkitystä kahteen vastakkaiseen suuntaan. Noin 0.5 pisteen alle jäävä viivan leveys voi renderöityä laitekohtaisena mahdollisimman ohuena viivana, joka katoaa näytöllä ja ilmestyy uudelleen tulostettuna, joten näkyvä viiva haluaa leveyden, jonka asetat tarkoituksella oletusarvon sijaan. Toisessa ääripäässä taulukon viivojen ja ruudukkoviivojen (gridlines) tartuttaminen (snapping) kokonaislukukoordinaatteihin estää tiheää ruudukkoa näyttämästä hieman epätasaiselta siellä, missä vierekkäiset viivat pyöristyvät eri tavalla. Päätä ruudukon välistys pisteinä etukäteen ja loppu asettelu perii sen

Täytetyt muodot ja väri

Suljetut primitiivit voidaan täyttää ääriviivoilla piirtämisen sijaan. Rectangle ottaa sijainnin ja koon, Circle ottaa keskipisteen ja säteen, ja kumpi tahansa vahvistetaan Fill-kutsulla, joka maalaa sisäpuolen nykyisellä täyttövärillä (fill color), tai Stroke-kutsulla vain ääriviivoja varten. Täyttöväri ja piirtoväri ovat erillisiä tiloja (state), jotka asetetaan SetRGBFillColor- ja SetRGBStrokeColor-kutsuilla, jotka molemmat ottavat yhden TColor-arvon. Tämä tarkoittaa, että voit käyttää Delphin värivakioita ja RGB-apufunktiota suoraan

// Rectangle(X, Y, Width, Height): X ja Y ovat vasen alakulma.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X ja Y ovat keskipiste.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Vain ääriviiva: aseta piirtoväri ja leveys, sitten Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Tarkkaile argumenttien muotoa Rectangle-funktiossa. Se on sijainti-plus-koko, X, Y, Width, Height, ei kaksi vastakkaista kulmaa. Delphin kehittäjien tuntema TCanvas.Rectangle ottaa (Left, Top, Right, Bottom), joten lihasmuisti ojentaa HotPDF:lle toisen kulman silloin, kun se odottaa leveyttä ja korkeutta, ja laatikko tulee ulos väärän kokoisena. (X, Y)-pari on vasen alakulma, yhdenmukaisesti sivun origon kanssa. Ympyrälle (X, Y) on keskipiste ja kolmas argumentti on säde pisteinä

Yksi värivalinta, jossa alkuperäinen näyte teki virheen

Vanhempi versio tästä esimerkistä kylvi värejä Random($FFFFFF)-kutsulla jokaiseen muotoon. Se näyttää eloisalta, mutta se on väärä vaisto generoituihin asiakirjoihin. Koodista rakennettava PDF on yleensä jotain, jota haluat myös testata, ja satunnaiset täyttövärit tekevät tulosteen vertailemisesta ajo kerrallaan mahdotonta: tavu tavulta -vertailu tunnetusti hyvää tiedostoa vastaan epäonnistuu joka kerta, ilman todellista syytä. Valitse eksplisiittiset värit. Kun haluat vaihtelua muotojen sarjaan, ohjaa sitä tiedoistasi tai kiinteästä palettitaulukosta (palette array), jotta sama syöte tuottaa aina saman tiedoston. Determinismi on arvokkaampaa kuin uutuudenviehätys, kun artefakti liikkuu julkaisuputken läpi

Primitiivien yhdistäminen: huomautuslaatikko (callout box)

Jokainen primitiivi on yksinään yksinkertainen; hyöty näkyy, kun kourallinen niitä yhdistyy joksikin, jota raportti todella tarvitsee. Huomautuslaatikko, annotoitu laatikko, joka osoittaa kuvaa ja selittää sen, käyttää kaikkea tähän mennessä käsiteltyä: täytetty suorakulmio reunuksella, piirretty osoitinviva (pointer line), piste, joka ankkuroi osoittimen, ja teksti, joka on aseteltu laatikon sisään samojen vasemman alakulman koordinaattien avulla, joita muodot käyttävät. FillAndStroke ansaitsee paikkansa täällä, maalaten yhden polun sisäosan ja ääriviivan yhdellä vahvistuksella sen sijaan, että suorakulmio rakennettaisiin kahdesti

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

    // 1. Laatikko: vaalea täyttö plus näkyvä reunus, yksi polku, yksi vahvistus.
    //    Rectangle on vasen alakulma plus koko, Y mitattuna pohjasta
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // vaalea meripihkanvärinen paneeli
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // tummempi reuna
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. Osoitin: yksi piirretty segmentti laatikon reunasta alaspäin
    //    kohti annotoitavaa asiaa
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // laatikon vasen reuna
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. Täytetty piste ankkuroi osoittimen kohteeseensa
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. Nimike, sijoitettu suhteessa laatikon vasempaan alakulmaan.
    //    Teksti ja muodot jakavat yhden koordinaatiston, joten siirtymät (offsets)
    //    ovat yksinkertaista aritmetiikkaa pistettä (90, 600) vasten
    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;

Huomaa, kuinka vähän tilanhallintaa yhdistelmä tarvitsee. Täyttöväri, piirtoväri ja viivan leveys asetetaan kukin välittömästi ennen niitä käyttävää muotoa, joten jokainen piirustuksen lohko luetaan itsenäisenä yksikkönä ja voidaan järjestää uudelleen tai purkaa apufunktioksi (helper) ilman, että piilotettua tilaa raahataan mukana. Kääri tämä proseduuriin, joka ottaa ankkuripisteen ja merkkijonot, ja sinulla on uudelleenkäytettävä kaavion annotaatio neljänkymmenen koodirivin kustannuksella

Missä vektoripiirros kannattaa, ja missä ei

Tartu näihin polku- ja muotokutsuihin, kun geometria on generoitua: kaavion ruudukkoviivat (gridlines) ja palkit, laskutustaulukon viivat (ruled lines), huomautuslaatikot kaaviossa, logomerkki ilmaistuna kourallisena polkuja. Kaikki tämä skaalautuu ilman sumentumista ja lisää tuskin mitään tiedoston kokoon, koska suorakulmio on muutama numero eikä tuhansia pikseleitä. Kääntöpuoli on myös totta. Jos sinulla on todellisuudessa valokuva tai kuvakaappaus, piirrä se kuvana AddImage- ja ShowImage-kutsuilla sen sijaan; bittikartan jäljittäminen vektorikutsuilla ei osta sinulle mitään. Suorat segmentit, suorakulmiot ja ympyrät edellä kantavat suuren enemmistön todellisesta raportointityöstä, ja kolme hienosäätöä, joita kehittäjät kysyvät seuraavaksi, eli käyrät (curves), katkoviivat (dash patterns) ja läpinäkyvyys (transparency), istuvat samalla sivuobjektilla

Käyrät, katkoviivat ja läpinäkyvyys lyhyesti

Vapaamuotoiset käyrät laajentavat samaa polkukoneistoa, joka sinulla jo on. CurveToC(X1, Y1, X2, Y2, X3, Y3) lisää kuubisen Bezier-segmentin nykyisestä pisteestä pisteeseen (X3, Y3), taipuen kahta ohjauspistettä kohti, ja lyhennysvariantit CurveToV ja CurveToY kattavat tapaukset, joissa yksi ohjauspiste osuu yhteen päätepisteen kanssa. Polku voi sekoittaa LineTo- ja CurveToC-segmenttejä vapaasti ennen kuin yksittäinen Stroke tai Fill vahvistaa sen, millä tavoin pyöristetyt kulmat ja sileät kaavioviivat rakennetaan

Katkoviivoitetut piirrot (dashed strokes) ovat tilaa, täsmälleen samalla tavalla kuin viivan leveys. SetDash([3, 3], 0) muuttaa jokaisen myöhemmän piirron kolme-pistettä-päällä, kolme-pistettä-pois -kuvioksi, jossa taulukko (array) kirjoittaa auki päällä/pois-ajon pituudet pisteinä ja toinen argumentti vaiheistaa (phases) sen, mistä sykli alkaa; NoDash palauttaa kynän kiinteäksi viivaksi (solid line). Aseta se, piirrä ruudukkoviivat jotka haluavat sitä, ja nollaa se ennen seuraavaa kiinteää viivaa, tai katkoviiva tarttuu hiljaa kaikkeen sen jälkeen seuraavaan

Läpinäkyvyys kulkee nimetyn grafiikkatilan (named graphics state) eikä väriargumentin kautta, koska alfa-kanava (alpha) PDF-muodossa on grafiikkatilasanakirjan ominaisuus. Rekisteröi sellainen asiakirjaan RegisterExtGState-kutsulla, välittäen täyttöalfan (fill alpha) ja piirtoalfan (stroke alpha) arvojen 0 ja 1 välillä, ja sovella sitten sen palauttamaa nimeä CurrentPage.SetGraphicsState-kutsulla; täytöt ja piirrot maalaavat tuosta pisteestä eteenpäin rekisteröidyllä opasiteetilla (opacity). Se on raskaampi seremonia kuin värinasettajat, ja sen arvoinen ensimmäisen kerran, kun korostuspalkin (highlight bar) on istuttava tekstin päällä piilottamatta sitä

Jäljellä oleva säilyttämisen arvoinen tapa on varmennus. Generoitu geometria voi mennä läpi koneellasi ja epäonnistua asiakkaan koneella, yleensä johtuen kirjasinten (font) korvaamisesta missä tahansa sekoittamassasi tekstissä tai sivukokoolettamuksesta, joka ei päde. Avaa valmis tiedosto muutamalla zoomaustasolla vahvistaaksesi, että reunat pysyvät puhtaina, ja tarkista, että jokainen muoto laskeutuu tarkoittamasi marginaalilaatikon (margin box) sisään. Deterministisellä väriteemalla (color scheme) tuo tarkistus voidaan automatisoida vertaamalla referenssi-PDF-tiedostoon pikemminkin kuin silmämääräisesti arvioimalla

Tässä esitetyt MoveTo, LineTo, Stroke, Fill ja värikutsut ovat osa HotPDF-komponenttia Delphille ja C++Builderille