Teknisk artikel

HotPDF: report output with fonts and images in Delphi

Att generera en rapport handlar i grunden om att placera tre saker på en sida och få dem att komma överens om var de sitter: text vid kända koordinater, typsnitt som återges likadant på servern som på skrivbordet, och bilder anpassade i storlek. Allt annat ett rapportbibliotek gör är ordnat kring dessa tre. HotPDF, losLabs PDF-genereringsbibliotek för Delphi och C++Builder, ger dig var och en av dem som ett direkt anrop på sidobjektet, och den enda verkliga friktionen är koordinatsystemet under ytan, som löper åt motsatt håll jämfört med VCL-ritytan du är van vid. Reda ut den orienteringen först, så slutar resten av layoutarbetet kämpa emot dig

Textplacering och origo nere till vänster

Nästan allas första rapport kommer ut upp och ned. Rubriken hamnar nära underkanten och varje rad under den klättrar mot toppen. Ingenting är trasigt. PDF:ens användarrymd, definierad i ISO 32000-1 §8.3, lägger origo i hörnet nere till vänster med Y växande uppåt, vilket är spegelbilden av GDI-ritytan där Y växer nedåt från övre vänstra hörnet. Fem minuter spenderade på att göra upp med det sparar en layout du annars skulle skriva om så fort siffrorna slutar gå ihop

HotPDF-diagram som kontrasterar VCL:s origo uppe till vänster med PDF:s origo nere till vänster, där TextOut placerar en titel 50 punkter från toppen av en Letter-sida vid Y 792 minus 50
PDF-användarrymden speglar VCL-canvasen, så en rubrik 50pt från toppen av en Letter-sida är TextOut(50, 792 - 50, 0, 'INVOICE') och samma omvandling håller varje rapportkoordinat intuitiv

Sidobjektets centrala anrop är TextOut(X, Y, Angle, Text). X och Y lokaliserar texten i punkter från hörnet nere till vänster, och Angle roterar den i grader, vilket är hur en diagonal DRAFT- eller COPY-stämpel ritas utan något särskilt stöd. Knepet som låter VCL-tränad intuition fortsätta fungera är att uttrycka Y som sidhöjden minus avståndet du vill ha från 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 från toppen av Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // roterad stämpel
    Pdf.AddPage;                                                // CurrentPage pekar nu hit
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // typsnittstillståndet följer inte med
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

De två tillståndsberoende beteendena i den listningen står för de flesta buggarna som bara dyker upp på sida två. AddPage pekar om CurrentPage mot sidan den just skapade, så en sidreferens du cachat tidigare ritar inte längre där du förväntar dig. Typsnittsvalet är också per sida snarare än per dokument. Hoppar du över SetFont efter en AddPage faller den första TextOut på den nya sidan tillbaka på vad sidan än startade med som standard, inte det feta rubriktypsnitt du satte tre sidor tidigare. Den säkra vanan är att behandla "starta en ny sida" och "återupprätta texttillståndet" som ett enda oskiljaktigt steg i rapportloopen

Typsnitt som finns på servern, inte bara på skrivbordet

De flesta typsnittsproblem är egentligen distributionsproblem i förklädnad. Din utvecklingsmaskin har företagets typsnitt installerat, så rapporten ser rätt ut på din skärm och skickas iväg. Produktionsservern kör jobbet under ett tjänstekonto som aldrig haft det typsnittet installerat, renderaren byter tyst ut det mot något den kan hitta, och det första någon hör om det är en kund som frågar varför brevhuvudet ändrades. Vägen ut är att sluta lita på operativsystemets typsnittskatalog och i stället läsa in typsnittet från en fil din installationsrutin lägger på disk. HotPDF:s Unicode-registreringsanrop tar en sökväg och gör precis det:

Diagram över ett Delphi PDF-typsnittsdistribueringsproblem: produktionsservern byter tyst ut ett saknat typsnitt, medan RegisterUnicodeTTF läser in TTF:en från en distribuerad fil och bäddar in den i PDF:en
Att förlita sig på OS-typsnittskatalogen bryts när produktionskontot för tjänsten saknar typsnittet, medan inläsning av TTF:n från en utrullad fil bäddar in glyferna och varje värd renderar identiskt
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 tar emot en WideString direkt, vilket betyder mer än det ser ut vid första anblicken. Ett kundnamn med en accent, en tysk gata, en polsk stad: dessa är inte specialfall, de är det normala innehållet i en kundtabell, och de går genom samma anrop som de ASCII-etiketter du hårdkodar, så länge det registrerade typsnittet faktiskt innehåller glyferna. En versionsbegränsning följer med inbäddade typsnitt: dokumentet måste vara PDF 1.5 eller senare, så om ett orelaterat krav låser dig vid en äldre version är det den saken som tyst går sönder. Höger-till-vänster-skriftsystem som arabiska och hebreiska behöver riktig formgivning i stället för en rak glyfuppslagning, och det har sin egen pipeline; se vår artikel om formgivning av komplexa skriftsystem med HotPDF

När inget installerat typsnitt kan uttrycka det du behöver, tänk MICR-tecken på en check eller en egen symboluppsättning, fyller Type 3-typsnitt luckan. Du definierar varje glyf som en liten innehållsström via RegisterType3Font och AddType3Glyph. Det är ett specialiserat hörn av API:et och du kommer sällan att behöva ta till det, men det är betydligt renare än att strö ut hundratals små symbolbitmappar över en sida

Bilder: de mittersta argumenten är en bredd och höjd, inte ett hörn

Bildhantering delas upp i två steg, och att hålla dem åtskilda är själva poängen. AddImage tar en TBitmap eller TJPEGImage, bäddar in den en gång, och lämnar tillbaka ett index. PNG-grafik måste avkodas till en bitmapp innan den kommer dit. ShowImage ritar sedan det indexet var och hur ofta du vill. Argumentordningen på ShowImage är den enda platsen värd att sakta ner och läsa noga:

Diagram över HotPDF:s bildpipeline där AddImage bäddar in bitmappen en gång och returnerar ett index, ShowImage placerar den efter bredd och höjd, och argumentordningen inte är ett hörnpar
AddImage bäddar in pixlarna en gång och varje ShowImage-anrop återanvänder det indexet, och de mellersta argumenten är en bredd och höjd i stället för koordinaterna för ett motstående hörn
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // avkoda PNG till en bitmapp
    LogoIdx := Pdf.AddImage(Logo, icFlate); // förlustfritt för platt-färgad grafik
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): inte (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

De två talen efter positionen är en bredd och en höjd. De är inte koordinaterna för det motsatta hörnet, och det sista argumentet är en rotationsvinkel i grader. Läs signaturen som en X1/Y1/X2/Y2-ruta och en 120-gånger-40-logotyp placerad vid (50, 700) sträcker sig i stället därifrån till (120, 40), och breder ut sig över större delen av sidan. Utdatan gör misstaget uppenbart medan källkoden ser fullt rimlig ut, vilket är det som gör att det tar en eftermiddag att spåra. KeepImageAspectRatio är som standard True, så en ruta med fel proportioner brevlådeformaterar bilden i stället för att förvränga den; slå om den till False bara när du verkligen menar att sträcka ut

Uppdelningen mellan registrering och placering lönar sig på långa körningar. Eftersom AddImage bäddar in pixlarna en gång och varje ShowImage med det indexet pekar tillbaka på samma inbäddade objekt, är det var du anropar AddImage som avgör filstorleken. Anropa den inuti sidloopen för ett 500-sidigt kontoutdrag och samma logotyp bäddas in 500 gånger. Anropa den en gång före loopen, spara indexet, och logotypen lagras en enda gång. En liten uppslagstabell nyckelbaserad på tillgångens sökväg räcker för att säkerställa att varje distinkt bild registreras exakt en gång

Valet av kodek är den andra spaken för storlek. Fotografiskt innehåll, skannade bilagor och liknande, hör hemma i JPEG: skicka icJpeg till AddImage och sänk JpegQuality till runt 85, eftersom egenskapen börjar på 100 och skillnaden vid 85 är osynlig på en utskriven sida. Platt-färgad grafik som logotyper, diagram och linjeteckningar hör hemma i icFlate, där förlustfri komprimering redan är kompakt och JPEG skulle smeta ut synlig ringning kring de skarpa kanterna. En körning av kontoutdrag som trycker in ett foto i full kvalitet på varje sida kan svälla till gigabyte; samma innehåll vid JPEG 85 landar på ungefär en tiondel av storleken, och ingen läsare kan märka skillnaden

Linjer, rutor och skuggning med grundläggande banor

Den horisontella linjen under en tabellrubrik och den grå rutan bakom en totalsumma behöver inte vara bilder. Rita dem som vektorer och de förblir skarpa vid vilken zoomnivå som helst, skrivs ut skarpt, och lägger till nästan ingenting till filen. HotPDF följer samma modell som råa PDF-innehållsströmmar använder: bygg en bana, anropa sedan en operator som målar den

// Horisontell linje under tabellrubriken
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Skuggad totalsummeruta: X, Y, bredd, höjd
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

Ordningen är inte valfri: sätt måltillståndet, konstruera banan, anropa sedan Stroke eller Fill. En bana du bygger men aldrig målar bidrar med ingenting till sidan, vilket nästan alltid är svaret när en linje "inte syns". SetRGBFillColor tar en enda TColor, så de välbekanta VCL-konstanterna som clNavy och clBlack passar rakt in, och Rectangle använder samma bredd-och-höjd-argument som bildplacering i stället för två hörn. En varning om tunna linjer: allt under ungefär en halv punkt kan se elegant ut på en skärm och sedan försvinna på en kontorsskrivare med 600 dpi, så 0,75pt är en rimlig lägstanivå för varje linje som måste överleva att bli utskriven

Paginering mot verklig data, inte exempeldata

En detalj att få rätt innan layouten fastnar: numeriska kolumner ska högerjusteras, och sättet att göra det är att mäta den renderade bredden på varje värde och positionera det tillbaka från kolumngränsen, inte att fylla ut strängen med inledande mellanslag. Utfyllnad med mellanslag ligger bara i linje i ett typsnitt med fast breddsteg, och ingen sätter en finansiell rapport i ett typsnitt med fast breddsteg. Kör värdena genom Delphis lokalmedvetna rutiner som FormatFloat först, så att tusentalsavgränsaren du mäter bredden på är samma som kundens lokal faktiskt kommer att visa

Faran med paginering är att du skriver den mot exempeldatamängden, där tio korta rader ryms på en sida och loopen aldrig behöver bryta. Produktionen ger dig en kund vars företagsnamn löper på 140 tecken och ett kontoutdrag med 4 000 radposter, och nu måste loopen bryta korrekt varje gång. Mönstret som håller är en enda Y-markör som rör sig nedåt när du drar av varje rads höjd, och en kontroll som startar en ny sida i samma ögonblick markören skulle korsa nedre marginalen. Nedåt betyder här minskande Y, vilket är den enda plats där origo nere till vänster förblir kontraintuitivt. Håll allt det i en enda rutin som också återutfärdar SetFont och ritar om den löpande sidhuvudet på den nya sidan, så får off-by-one-sidbuggarna aldrig fotfäste. När samma rapporter också måste uppfylla arkiverings- eller tillgänglighetsregler är valen du gör just här, vilka typsnitt du bäddar in, om utdatan är taggad, vilka färgrymder du använder, de som de standarderna kontrollerar; HotPDF:s guide till PDF/A, PDF/X och PDF/UA är värd att läsa innan mallen stelnar

Varje anrop som visas här, textpositioneringen, typsnittsregistreringen, bildinbäddningen och banritningen, levereras med HotPDF Delphi Component för Delphi och C++Builder, vars referens dokumenterar hela utdata-API:et tillsammans med formulär-, krypterings- och signeringsfunktionerna det står bredvid