At generere en rapport handler i bund og grund om at placere tre ting på en side og få dem til at blive enige om, hvor de sidder: tekst ved kendte koordinater, skrifttyper, der gengives ens på serveren som på ens skrivebord, og billeder tilpasset til at passe. Alt andet, et rapportbibliotek gør, er arrangeret omkring disse tre. HotPDF, losLabs PDF-genereringsbibliotek til Delphi og C++Builder, giver dig hver af dem som et direkte kald på side-objektet, og den eneste reelle friktion er koordinatsystemet nedenunder, som løber den modsatte vej af det VCL-lærred, man er vant til. Få den orientering på plads først, og resten af layout-arbejdet holder op med at kæmpe imod dig
Tekstplacering og origo nederst til venstre
Næsten alles første rapport kommer ud på hovedet. Titlen lander nær bundkanten, og hver linje under den kravler mod toppen. Intet fungerer forkert. PDF user space, defineret i ISO 32000-1 §8.3, placerer origo i nederste venstre hjørne med Y voksende opad, hvilket er spejlbilledet af GDI-lærredet, hvor Y vokser nedad fra øverste venstre hjørne. Fem minutters fred med det sparer et layout, man ellers ville omskrive, den dag tallene holder op med at give mening
Side-objektets centrale kald er TextOut(X, Y, Angle, Text). X og Y placerer teksten i punkter fra nederste venstre hjørne, og Angle roterer den i grader, hvilket er, hvordan et diagonalt DRAFT- eller COPY-stempel bliver tegnet uden nogen særlig understøttelse. Tricket, der lader VCL-trænet intuition blive ved med at virke, er at udtrykke Y som sidehøjden minus den afstand, man vil have fra toppen:
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice-0001.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE'); // 50pt fra toppen af Letter
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY'); // roteret stempel
Pdf.AddPage; // CurrentPage peger nu her
Pdf.CurrentPage.SetFont('Arial', [], 10); // skrifttypetilstanden overføres ikke
Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
De to tilstandsafhængige opførsler i den liste står bag de fleste af de fejl, der først viser sig på side to. AddPage omdirigerer CurrentPage til den side, den lige har oprettet, så en sidereference, man cachede tidligere, ikke længere tegner, hvor man forventer. Skrifttypevalg er også per side snarere end per dokument. Springer man SetFont over efter en AddPage, falder den første TextOut på den friske side tilbage til, hvad end standard, siden startede med, ikke den fede overskriftsskrifttype, man satte tre sider tidligere. Den sikre vane er at behandle "start en ny side" og "genetablér tekst-tilstanden" som ét udeleligt trin i rapportløkken
Skrifttyper, der findes på serveren, ikke kun på dit skrivebord
De fleste skrifttypeproblemer er reelt deployment-problemer i forklædning. Din udviklingsmaskine har den virksomhedsspecifikke skrifttype installeret, så rapporten ser rigtig ud på din skærm, og den bliver sendt af sted. Produktionsmaskinen kører jobbet under en servicekonto, der aldrig har haft den skrifttype installeret, rendereren erstatter den stille med noget, den kan finde, og det første, nogen hører om det, er en kunde, der spørger, hvorfor brevpapiret ændrede sig. Vejen ud er at holde op med at stole på OS'ets skrifttypemappe og indlæse skrifttypen fra en fil, ens installer lægger på disken. HotPDF's Unicode-registreringskald tager en sti og gør præcis det:
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));
TextOut accepterer en WideString direkte, hvilket betyder mere, end det ser ud til ved første øjekast. Et kundenavn med en accent, en tysk gade, en polsk by: det er ikke kantsager, det er det normale indhold i en kundetabel, og de går gennem det samme kald som de ASCII-labels, man hardkoder, så længe den registrerede skrifttype rent faktisk indeholder glyferne. Én versionsbegrænsning følger med indlejrede skrifttyper: dokumentet skal være PDF 1.5 eller nyere, så hvis et urelateret krav fastholder dig på en ældre version, er det dét, der stille vil gå i stykker. Højre-mod-venstre-skrifter som arabisk og hebraisk kræver reel shaping frem for et lige glyf-opslag, og det har sin egen pipeline; se vores artikel om kompleks skrift-shaping med HotPDF
Når ingen installeret skrifttype kan udtrykke, hvad man har brug for, tænk MICR-tegn på en check eller et proprietært symbolsæt, fylder Type 3-skrifttyper hullet ud. Man definerer hvert glyf som en lille content stream via RegisterType3Font og AddType3Glyph. Det er et specialiseret hjørne af API'et, og man vil sjældent ræk efter det, men det er langt renere end at sprede hundredvis af små symbol-bitmaps ud over en side
Billeder: de midterste argumenter er en bredde og en højde, ikke et hjørne
Billedhåndtering deler sig i to trin, og at holde dem adskilt er hele pointen. AddImage tager en TBitmap eller TJPEGImage, indlejrer det én gang og returnerer et indeks. PNG-grafik skal afkodes til en bitmap, før den når dertil. ShowImage tegner derefter det indeks, hvor og hvor ofte man vil. Argumentrækkefølgen på ShowImage er det ene sted, det er værd at sætte farten ned og læse:
var
Png: TPngImage;
Logo: TBitmap;
LogoIdx: Integer;
begin
Png := TPngImage.Create;
Logo := TBitmap.Create;
try
Png.LoadFromFile('brand-logo.png');
Logo.Assign(Png); // afkod PNG til en bitmap
LogoIdx := Pdf.AddImage(Logo, icFlate); // tabsfrit til fladfarvet grafik
finally
Logo.Free;
Png.Free;
end;
// (Index, X, Y, Width, Height, Angle): ikke (X1, Y1, X2, Y2)
Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;
De to tal efter positionen er en bredde og en højde. De er ikke koordinaterne for det modsatte hjørne, og det afsluttende argument er en rotationsvinkel i grader. Læs signaturen som en X1/Y1/X2/Y2-boks, og et 120-gange-40-logo placeret ved (50, 700) strækker sig i stedet derfra til (120, 40), spredt ud over det meste af siden. Outputtet gør fejlen indlysende, mens kildekoden ser fuldstændig fornuftig ud, hvilket er det, der gør, at den æder en eftermiddag. KeepImageAspectRatio er som standard True, så en boks med de forkerte proportioner letterboxer billedet i stedet for at forvrænge det; slå den til False kun, når man rent faktisk mener at strække
Opdelingen mellem registrering og placering betaler sig på lange kørsler. Fordi AddImage indlejrer pixels én gang, og hver ShowImage med det indeks peger tilbage på det samme indlejrede objekt, afgør det, hvor man kalder AddImage, filstørrelsen. Kald den inde i sideløkken for et 500-siders kontoudtog, og det samme logo bliver indlejret 500 gange. Kald den én gang før løkken, behold indekset, og logoet gemmes én gang. Et lille dictionary nøglet efter aktivstien er nok til at sikre, at hvert distinkt billede registreres præcis én gang
Valget af codec er den anden størrelseshåndtag. Fotografisk indhold, scannede vedhæftninger og lignende, hører hjemme i JPEG: send icJpeg til AddImage, og sæt JpegQuality ned til omkring 85, da egenskaben starter ved 100, og forskellen ved 85 er usynlig på en trykt side. Fladfarvet grafik som logoer, diagrammer og linjetegninger hører hjemme i icFlate, hvor tabsfri kompression allerede er kompakt, og JPEG ville udviske synlig ringing omkring de hårde kanter. En kontoudtogskørsel, der presser ét fuldkvalitetsfoto ind på hver side, kan svulme op til gigabyte; det samme indhold ved JPEG 85 lander omkring en tiendedel af størrelsen, og ingen læser kan mærke forskel
Streger, bokse og skygning med path-primitiver
Den vandrette linje under en tabeloverskrift og den grå boks bag et totaltal behøver ikke være billeder. Tegn dem som vektorer, og de forbliver skarpe ved enhver zoom, printer skarpt og tilføjer næsten intet til filen. HotPDF følger den samme model, rå PDF-content-streams bruger: byg en path, kald derefter en operator, der maler den
// Vandret streg under tabeloverskriften
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;
// Skygget totalboks: X, Y, bredde, højde
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;
Rækkefølgen er ikke valgfri: sæt maletilstanden, konstruér stien, kald så Stroke eller Fill. En sti, man bygger, men aldrig maler, bidrager intet til siden, hvilket næsten altid er svaret, når en streg "ikke dukker op". SetRGBFillColor tager en enkelt TColor, så de velkendte VCL-konstanter som clNavy og clBlack passer lige ind, og Rectangle bruger de samme bredde-og-højde-argumenter som billedplacering frem for to hjørner. Én advarsel om tynde linjer: alt under omkring en halv punkt kan se elegant ud på en skærm og så forsvinde på en 600 dpi-kontorprinter, så 0,75pt er et rimeligt gulv for enhver streg, der skal overleve at blive printet
Paginering mod rigtige data, ikke eksempeldata
Én detalje, man skal have styr på, før layoutet sætter sig: numeriske kolonner bør justeres på deres højre kant, og måden at gøre det på er at måle den gengivne bredde af hver værdi og placere den tilbage fra kolonnegrænsen, ikke at padde strengen med foranstillede mellemrum. Mellemrums-padding stemmer kun i en monospace-skrifttype, og ingen sætter en finansiel rapport i en monospace-skrifttype. Kør værdierne gennem Delphis lokalt-bevidste rutiner som FormatFloat først, så det tusindtalsseparator, man måler bredden af, er den samme, som kundens lokalitet rent faktisk vil vise
Faren ved paginering er, at man skriver den mod demo-datasættet, hvor ti korte rækker passer på én side, og løkken aldrig behøver at bryde. Produktionen giver dig en kunde, hvis firmanavn løber over 140 tegn, og et kontoudtog med 4.000 linjeposter, og nu skal løkken bryde korrekt hver gang. Det mønster, der holder, er én enkelt Y-markør, der bevæger sig nedad, mens man trækker hver rækkes højde fra, og et tjek, der starter en ny side, i det øjeblik markøren ville krydse bundmargenen. Nedad betyder her aftagende Y, hvilket er det ene sted, origo nederst til venstre forbliver kontraintuitiv. Hold alt det i én rutine, der også genudsteder SetFont og gentegner den løbende header på den nye side, og off-by-one-side-fejlene får aldrig fodfæste. Når de samme rapporter også skal opfylde arkiv- eller tilgængelighedsregler, er de valg, man træffer lige her, hvilke skrifttyper man indlejrer, om outputtet er tagged, hvilke farverum man bruger, dem, de standarder politipatruljerer; HotPDF-guiden til PDF/A, PDF/X og PDF/UA er værd at læse, før skabelonen hærder
Hvert kald vist her, tekstpositionering, skrifttyperegistrering, billedindlejring og path-tegning, leveres i HotPDF Delphi Component til Delphi og C++Builder, hvis reference dokumenterer det fulde output-API sammen med de formular-, kryptering- og signeringsfunktioner, det sidder ved siden af