Tekninen artikkeli

Tietotaulukon renderöinti PDF:ksi HotPDFillä

Dataset koostuu riveistä ja sarakkeista, mutta PDF-sivu on tyhjä koordinaattiruudukko ilman kumpaakaan käsitettä. HotPDFissä ei ole DrawTable-kutsua valmiin ruudukon tuottamiseen. Taulukko rakennetaan primitiiveistä: TextOut sijoittaa tekstin, SetFont valitsee fontin, Rectangle ja Fill varjostavat alueen ja MoveTo, LineTo sekä Stroke piirtävät viivat. Luotettava viejä muuntaa rivi- ja sarakemallin eksplisiittisiksi X/Y-koordinaateiksi ja ylläpitää niitä datan ylittäessä sivun alareunan

Seuraava esimerkki raportoi asiakastietueita, mutta mikään piirtokoodissa ei tiedä tai välitä, mistä rivit tulevat. Alkuperäinen käytti vanhaa TTable-komponenttia; FireDAC-kysely, muistin sisäinen tietojoukko tai tavallinen tietuetaulukko (array of records) syöttää samoja rutiineja muuttamattomina. Tärkeintä on, että voit käydä datan läpi rivi kerrallaan ja lukea jokaisesta neljä merkkijonokenttää. Pidä renderöinti erillään tietolähteestä, ja voit muuttaa kumpaakin puolta häiritsemättä toista

Sarakkeiden geometria tulee ensin

Ennen kuin yhtäkään merkkiä on piirretty, päätä, missä kukin sarake asuu. Tässä taulukossa on neljä saraketta, joten se tarvitsee neljä vasenta reunaa ja tunnetun oikean marginaalin. Maagisen numeron koodaaminen kiinteästi jokaiseen TextOut-kutsuun, kuten pikaisilla esimerkeillä on tapana, on juuri se, mikä tekee taulukon leventämisestä myöhemmin tuskallista. Nimeä reunat kerran pisteinä (points) vasemmasta alakulmasta (origo), ja jokainen piirtokutsu viittaa niihin nimeltä:

HotPDF-taulukon sarakegeometria Delphissä: nimetyt x-reunat 70, 110, 300 ja 480 pisteessä kehyksen viivojen 50 ja 570 pisteen välillä
Neljä nimettyä vasenta reunaa ja tunnettu oikea marginaali lukitsevat koko taulukon geometrian ennen ensimmäistä TextOut-kutsua
const
  ColNo   = 70;    // "No."-sarakkeen vasen reuna
  ColName = 110;   // company name
  ColAddr = 300;   // street address
  ColCity = 480;   // city
  RowLeft = 50;    // table frame: left rule
  RowRight = 570;  // table frame: right rule
  RowStep = 20;    // peruslinjojen välinen pystyetäisyys

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // varjostettu kaista rivin takana. Rectangle ottaa X, Y, Width, Height
    Page.SetRGBFillColor($00FFF3DD);
    Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
    Page.Fill;
    Page.SetRGBFillColor(clBlack);
  end;
  Page.TextOut(ColNo,   Y, 0, ANo);
  Page.TextOut(ColName, Y, 0, AName);
  Page.TextOut(ColAddr, Y, 0, AAddr);
  Page.TextOut(ColCity, Y, 0, ACity);
end;

Kaksi yksityiskohtaa ansaitsevat paikkansa tässä. Varjostettu nauha piirretään ensin, sitten teksti sen päälle, koska maalausjärjestys on z-järjestys PDF:ssä: täytä suorakulmio tekstin jälkeen, ja hautaat rivin. Vaihteleva varjostus ei myöskään ole koriste vain sen itsensä vuoksi. Tiheässä raportissa se on halvin tapa estää silmää liukumasta väärälle riville, minkä vuoksi silmukka myöhemmin kääntää totuusarvon (boolean) jokaisella rivillä ja välittää sen suoraan Shaded-parametrille

Yllä olevat sarakkeiden sijainnit ovat kiinteät, mikä on reilua raportille, jonka skeemaa hallitset. Kun data on muuttuvaa, mittaa arvaamisen sijaan. HotPDF paljastaa tekstin leveyden mittauksen sivuobjektissa, joten PrintRow:n tuotantoversio voi ottaa pisimmän odotetun arvon kussakin sarakkeessa, mitata sen kerran valitulla fonttikoolla ja johtaa vasemmat reunat näistä leveyksistä plus välistä (gutter). Rutiinin muoto ei muutu; vain vakioiden lähde muuttuu

Otsikko, viivat ja yksi paikka, joka omistaa ne

Taulukko, joka jatkuu sivun yli ja jatkuu seuraavalla ilman sarakeotsikoita, on lukukelvoton. Korjaus on käsitellä otsikkoa asiana, jonka piirrät uudelleen, ei asiana, jonka piirrät kerran. Laita sarakkeiden otsikot ja niitä kehystävät vaakaviivat yhteen rutiiniin, ja kutsu tuota rutiinia sekä alussa että uudelleen joka kerta, kun avaat uuden sivun. Koska otsikko ja runko jakavat samat sarakevakiot, ne kohdistuvat toisiinsa rakenteensa ansiosta

HotPDF piirtää DrawHeader-otsikot ja -viivat sivulle yksi ja uudelleen jokaisen AddPagen jälkeen, joten molemmat PDF-sivut avautuvat samalla otsikolla
Ylätunnisterutiini ajetaan uudelleen jokaisella uudella sivulla, joten otsikot ja viivat laskeutuvat samaan paikkaan rakenteen ansiosta
procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Vasen: lähdetunniste ja sivunumero. Oikea: luontiaika
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // kaksi vaakaviivaa sarakeotsikoiden ympärillä
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // sarakeotsikot paksummalla fontilla, jotta ne erottuvat otsikkoina
  Page.SetFont('Times New Roman', [fsBold], 12);
  Page.SetRGBFillColor(clNavy);
  PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
  Page.SetRGBFillColor(clBlack);

  Y := Y + RowStep + 45;  // siirry kehystetyn otsakkeen ohi ennen ensimmäistä tietoriviä
end;

Huomaa, että DrawHeader ottaa Y:n viitteenä (by reference) ja siirtää sitä eteenpäin. Kutsujan ei koskaan tarvitse muistaa, kuinka korkea otsikko on; rutiini, joka piirtää sen, on rutiini, joka tietää. Tämä yhden omistajan sääntö pitää asettelun ajelehtimasta, kun myöhemmin lisäät logon tai suodattimen yhteenvedon otsikkonauhaan. Runkosilmukka pysyy tietämättömänä. Se jatkaa vain rivien piirtämistä siitä, mihin Y tällä hetkellä osoittaa

Itse viivat ovat ero luettelon ja taulukon välillä. Pystysuorat sarake-erottimet ovat sama idea sovellettuna x-akseliin: MoveTo / LineTo / Stroke kunkin sarakkeen reunassa, vedettynä yläviivasta sivun viimeisen rivin pohjaan. Esimerkki pitäytyy vaakaviivoissa pysyäkseen luettavana, mutta tuotantovaihe on mekaaninen, kunhan sarakevakiot ovat olemassa

Kohdistinsilmukka omistaa sivunvaihdon

Piirtäminen on helppo puolisko. Se puolisko, joka erottaa lelun raportista, on sivutus (pagination): sen tietäminen, ennen kuin piirrät rivin, mahtuuko se vielä, ja uuden sivun aloittaminen tuoreella otsikolla, kun se ei mahdu. Tämä päätös kuuluu tasan yhteen paikkaan, datan läpikäyvään silmukkaan, eikä minnekään muualle

Delphi-kursorisilmukan vuokaavio, jossa Y alle 60 laukaisee AddPagin, CurrentPage-uudelleenluvun, SetFont-uudelleenannon ja toistetun otsikon ennen seuraavaa taulukkoriviä
Kohdistinsilmukka on ainoa paikka, joka avaa tuoreen sivun ja alustaa sen uudelleen, kun Y laskee alamarginaalin alle
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Y: Single;
  PageNo: Integer;
  Shaded: boolean;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'CustomerReport.pdf';
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;

    // raportin otsikko kerran ensimmäisen sivun yläreunaan
    Page.SetFont('Arial', [fsBold], 24);
    Page.TextOut(200, 800, 0, 'Customer Report');

    PageNo := 1;
    Y := 760;
    DrawHeader(Page, Y, PageNo);
    Shaded := False;

    CustomerTable.First;
    while not CustomerTable.Eof do
    begin
      // tila loppu? avaa uusi sivu ja toista otsake siellä
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage siirtää CurrentPage-arvoa eteenpäin
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont on annettava uudelleen jokaisella uudella sivulla
      PrintRow(Page, Y,
        VarToStr(CustomerTable['CustNo']),
        VarToStr(CustomerTable['Company']),
        VarToStr(CustomerTable['Addr1']),
        VarToStr(CustomerTable['City']),
        Shaded);

      Y := Y - RowStep;
      CustomerTable.Next;
    end;

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

Kaksi koordinaattitietoa ohjaa koko silmukkaa. PDF mittaa y:tä ylöspäin vasemmasta alakulmasta, joten rivit marssivat alaspäin sivua pitkin vähentämällä RowStep:n Y:stä joka kerta, ja sivu-täynnä-testi laukeaa, kun Y putoaa alamarginaalin alle pikemminkin kuin jonkin yläreunan yläpuolelle. Jos suunta on väärin, ensimmäinen rivi tulostuu alareunan ulkopuolelle samalla, kun silmukka luulee, että sillä on koko sivu tilaa

Toinen tieto nappaa melkein jokaisen kerran. AddPage luo uuden sivun ja osoittaa CurrentPage:n siihen, mutta se ei siirrä mitään mukanaan: ei fonttia, ei täyttöväriä, ei sijaintia. Siksi Page luetaan uudelleen CurrentPage:stä jokaisen AddPage:n jälkeen, ja siksi SetFont annetaan uudelleen ennen runkorivejä. Ohita uudelleenluku, ja jatkat piirtämistä sivulle, jonka juuri jätit taaksesi; ohita fontti, ja uusi sivu renderöidään millä tahansa oletusarvolla, mihin katseluohjelma turvautuu

Tapaukset, jotka rikkovat taulukkoviejän

Useimmat taulukkobugit eivät näy muutaman kymmenen siistin rivin onnellisella polulla. Ne elävät reunoilla, ja reunat ovat halpoja testata, kun tiedät, missä ne ovat

  • Tyhjät tietojoukot. Nollan rivin yli menevä silmukka tuottaa sivun, jossa on otsikko ja ei mitään sen alla, mikä ainakin näyttää tarkoitukselliselta. Tyhjä sivu ilman otsikkoa näyttää virheeltä. Päätä, kumman haluat ennen toimittamista
  • Rivi, joka osuu tasan rajalle. Luo raportti, jonka viimeinen rivi on yhden askeleen marginaalin yläpuolella, ja sitten sellainen, jonka seuraava rivi on yhden askeleen sen alapuolella. Yhdellä heittävä (off-by-one) sivutus piileskelee, kunnes data on tasan väärän pituista
  • Ylipitkät arvot. Sarakettaan leveämpi yrityksen nimi juoksee seuraavaan. Mittaa kenttä ja päätä käytännöstä: rivitä toiselle riville, leikkaa (clip) tai katkaise ellipsillä. Hiljaisuus ei ole käytäntö
  • Null-kentät. Null-arvon lukeminen suoraan TextOut-kutsuun voi tulla esiin kirjaimellisena tekstinä Null tai tyhjänä riippuen siitä, miten muunnat sen. Valitse renderöinti tarkoituksella mieluummin kuin antaisit varianttimuunnoksen valita puolestasi

Aja tulos useamman kuin yhden katseluohjelman läpi, ennen kuin kutsut sitä valmiiksi. Fontin korvaaminen ja leikkaus (clipping) käyttäytyvät eri tavalla eri renderöijissä, ja taulukko, joka näyttää suorakulmaiselta yhdessä PDF-lukijassa, voi näyttää väärin kohdistetun sarakkeen tai leikatun kaupungin toisessa. Varmista, että toistettu otsikko, rivivarjostus ja marginaalit selviävät siirrosta, ja että sivunumerot pysyvät jatkuvina sen jälkeen, kun data ylittää rajan

Ruudukon piirtäminen itse visuaaliseen raporttisuunnittelijaan tukeutumisen sijaan on enemmän koodia, ja kompromissi on syytä nimetä selvästi: omistat jokaisen koordinaatin, mikä on juuri sitä, mitä haluat palvelinpuolen eräajoille, laskuille ja tarkastusvienneille (audit exports), joiden on renderöidyttävä identtisesti jokaisella koneella, ja juuri sellaista lisätyötä (overhead), jota välttäisit mieluummin kertaluonteisessa sisäisessä listauksessa. Edellisten osalta hallinta maksaa itsensä takaisin ensimmäisen kerran, kun raportin on näytettävä samalta tuotannossa kuin se näytti työpöydälläsi

Yllä olevat viivat ja varjostetut nauhat nojaavat samoihin vektori- ja väriprimitiiveihin, joita käsiteltiin kankaan (canvas) piirtämisen läpikäynnissä, jos haluat, että Rectangle-, MoveTo- ja LineTo-kutsut käsitellään ensin erikseen. Tässä käytetyt piirtoprimitiivit ovat osa HotPDF Delphi Componentia Delphille ja C++Builderille