Technisch artikel

Delphi PDF-rapporten in HotPDF: TextOut, fonts, afbeeldingen

Een rapport genereren komt neer op drie dingen op een pagina plaatsen en ze op één lijn krijgen over waar ze staan: tekst op bekende coördinaten, fonts die op de server hetzelfde renderen als op uw desktop, en afbeeldingen passend gemaakt. Alles wat een rapportbibliotheek verder doet, is om die drie heen ingericht. HotPDF, de PDF-generatiebibliotheek van losLab voor Delphi en C++Builder, geeft u elk daarvan als een directe aanroep op het pagina-object, en de enige echte frictie is het onderliggende coördinaatsysteem, dat de tegenovergestelde kant op werkt ten opzichte van het VCL-canvas dat u gewend bent. Zet die oriëntatie eerst op een rij en de rest van het lay-outwerk werkt niet meer tegen u

Tekstplaatsing en de oorsprong linksonder

Bijna ieders eerste rapport komt ondersteboven uit. De titel belandt vlak bij de onderrand en elke regel eronder klimt naar boven. Er werkt niets fout. De PDF-gebruikersruimte, gedefinieerd in ISO 32000-1 §8.3, plaatst de oorsprong in de hoek linksonder met Y opwaarts groeiend, wat het spiegelbeeld is van het GDI-canvas waar Y van linksboven naar beneden groeit. Vijf minuten om dat te accepteren bespaart een lay-out die u anders opnieuw schrijft zodra de cijfers niet meer kloppen

Diagram van HotPDF dat de VCL-oorsprong linksboven afzet tegen de PDF-oorsprong linksonder, waarbij TextOut een titel 50 punten vanaf de bovenkant van een Letter-pagina plaatst op Y 792 min 50
PDF user space spiegelt het VCL-canvas, dus een titel 50pt van de bovenkant van een Letter-pagina is TextOut(50, 792 - 50, 0, 'INVOICE') en dezelfde conversie houdt elke rapportcoördinaat intuïtief

De centrale aanroep van het pagina-object is TextOut(X, Y, Angle, Text). X en Y plaatsen de tekst in punten vanaf de hoek linksonder, en Angle roteert deze in graden, wat is hoe een diagonale DRAFT- of COPY-stempel zonder enige speciale ondersteuning wordt getekend. De truc die VCL-getrainde intuitie laat blijven werken, is om Y uit te drukken als de paginahoogte minus de afstand die u vanaf de bovenkant wilt:

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 vanaf de bovenkant van Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // geroteerde stempel
    Pdf.AddPage;                                                // CurrentPage wijst nu hierheen
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // fontstatus wordt niet overgenomen
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

De twee stateful gedragingen in die listing zijn verantwoordelijk voor de meeste bugs die pas op pagina twee opduiken. AddPage wijst CurrentPage opnieuw naar de pagina die het zojuist heeft gemaakt, dus een paginaverwijzing die u eerder hebt vastgelegd, tekent niet meer waar u verwacht. Fontkeuze is ook per pagina in plaats van per document. Als u de SetFont na een AddPage overslaat, valt de eerste TextOut op de verse pagina terug op de standaardwaarde waarmee de pagina begon, niet op het vetgedrukte koptekstfont dat u drie pagina's geleden instelde. De veilige gewoonte is om 'een nieuwe pagina beginnen' en 'de tekststatus opnieuw instellen' te behandelen als één onafscheidelijke stap in de rapportlus

Fonts die op de server bestaan, niet alleen op uw desktop

De meeste fontproblemen zijn in werkelijkheid implementatieproblemen in vermomming. Uw ontwikkelmachine heeft het bedrijfsfont geïnstalleerd, dus het rapport ziet er op uw scherm goed uit en wordt verzonden. De productiehost draait de taak onder een serviceaccount dat dat font nooit heeft gehad, de renderer substitueert stilletjes iets wat het kan vinden, en het eerste wat iemand ervan hoort is een klant die vraagt waarom het briefhoofd is veranderd. De uitweg is om de OS-fontmap niet meer te vertrouwen en het font te laden uit een bestand dat uw installatieprogramma op schijf zet. HotPDF's Unicode-registratieaanroep neemt een pad en doet precies dat:

Diagram van een Delphi PDF-fontuitrolprobleem: de productieserver vervangt stilletjes een ontbrekend font, terwijl RegisterUnicodeTTF de TTF uit een uitgerold bestand laadt en in de PDF inbedt
Vertrouwen op de OS-fontmap breekt wanneer de productie-serviceaccount het lettertype mist, terwijl het laden van de TTF vanuit een gedeployed bestand de glyphs insluit en elke host identiek rendert
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 accepteert direct een WideString, wat meer uitmaakt dan het in eerste instantie lijkt. Een klantnaam met een accent, een Duitse straat, een Poolse stad: dit zijn geen randgevallen, het is de normale inhoud van een klanttabel, en ze gaan door dezelfde aanroep als de ASCII-labels die u hardcodeert, zolang het geregistreerde font de glyphs daadwerkelijk bevat. Eén versiebeperking komt mee met ingesloten fonts: het document moet PDF 1.5 of nieuwer zijn, dus als een andere eis u aan een oudere versie bindt, is dat het ding dat stilletjes breekt. Rechts-naar-links-scripts zoals Arabisch en Hebreeuws hebben echte shaping nodig in plaats van een rechte glyph-opzoeking, en dat heeft een eigen pijplijn; zie ons artikel over complexe-script-tekstshaping met HotPDF

Als geen geïnstalleerd font kan uitdrukken wat u nodig hebt, denk aan MICR-tekens op een cheque of een eigen symboolset, dan vullen Type 3-fonts de kloof. U definieert elke glyph als een kleine contentstream via RegisterType3Font en AddType3Glyph. Het is een gespecialiseerde hoek van de API en u zult er zelden naar grijpen, maar het is veel schoner dan honderden kleine symbool-bitmaps over een pagina verspreiden

Afbeeldingen: de middelste argumenten zijn een breedte en hoogte, geen hoek

Afbeeldingsverwerking splitst zich in twee stappen, en ze gescheiden houden is precies het punt. AddImage neemt een TBitmap of TJPEGImage, sluit deze eenmaal in, en geeft een index terug. PNG-illustratie moet worden gedecodeerd naar een bitmap voordat het daar aankomt. ShowImage tekent die index vervolgens waar en zo vaak u maar wilt. De argumentvolgorde op ShowImage is de enige plek waar het de moeite loont om te vertragen en te lezen:

Diagram van de HotPDF-afbeeldingspijplijn waarin AddImage de bitmap één keer inbedt en een index teruggeeft, ShowImage hem plaatst op breedte en hoogte, en de argumentvolgorde geen hoekpaar is
AddImage sluit de pixels één keer in en elke ShowImage-aanroep hergebruikt die index, en de middelste argumenten zijn een breedte en hoogte in plaats van de coördinaten van een tegenoverliggende hoek
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // decodeer PNG naar een bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless voor illustraties met vlakke kleuren
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): niet (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

De twee getallen na de positie zijn een breedte en een hoogte. Het zijn niet de coördinaten van de tegenovergestelde hoek, en het afsluitende argument is een rotatiehoek in graden. Lees de signatuur als een X1/Y1/X2/Y2-vak en een 120-bij-40-logo geplaatst op (50, 700) rekt zich van daaruit uit naar (120, 40), en verspreidt zich over het grootste deel van de pagina. De uitvoer maakt de fout duidelijk terwijl de broncode er volkomen redelijk uitziet, wat het is wat het een middag laat verspillen. KeepImageAspectRatio staat standaard op True, dus een vak met de verkeerde verhoudingen letterboxt de afbeelding in plaats van deze te vervormen; zet het alleen op False als u werkelijk wilt uitrekken

De scheiding tussen registreren en plaatsen betaalt zich uit bij lange runs. Omdat AddImage de pixels eenmaal insluit en elke ShowImage met die index terugwijst naar hetzelfde ingesloten object, bepaalt waar u AddImage aanroept de bestandsgrootte. Roep het aan binnen de paginalus voor een afschrift van 500 pagina's en hetzelfde logo wordt 500 keer ingesloten. Roep het eenmaal aan vóór de lus, bewaar de index, en het logo wordt eenmaal opgeslagen. Een kleine dictionary geïndexeerd op het asset-pad is genoeg om ervoor te zorgen dat elke afzonderlijke afbeelding precies eenmaal wordt geregistreerd

De keuze van de codec is de andere hendel voor de grootte. Fotografische inhoud, gescande bijlagen en dergelijke, hoort in JPEG: geef icJpeg door aan AddImage en zet JpegQuality op ongeveer 85, aangezien de eigenschap op 100 begint en het verschil op 85 onzichtbaar is op een afgedrukte pagina. Illustratie met vlakke kleuren zoals logo's, grafieken en lijntekeningen hoort in icFlate, waar lossless compressie al compact is en JPEG zichtbare ringing zou uitsmeren langs de harde randen. Een afschriftrun die één foto in volle kwaliteit op elke pagina duwt, kan uitgroeien tot gigabytes; dezelfde inhoud bij JPEG 85 landt op ongeveer een tiende van de grootte, en geen lezer ziet het verschil

Lijnen, vakken en arcering met padprimitieven

De horizontale lijn onder een tabelkop en het grijze vak achter een totaalfiguur hoeven geen afbeeldingen te zijn. Teken ze als vectoren en ze blijven scherp bij elke zoom, printen helder, en voegen bijna niets toe aan het bestand. HotPDF volgt hetzelfde model dat ruwe PDF-contentstreams gebruiken: bouw een pad, roep vervolgens een operator aan die het verft

// Horizontale lijn onder de tabelkop
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Gearceerd totaalvak: X, Y, breedte, hoogte
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

De volgorde is niet optioneel: stel de verfstatus in, construeer het pad, en roep dan Stroke of Fill aan. Een pad dat u bouwt maar nooit verft, draagt niets bij aan de pagina, wat vrijwel altijd het antwoord is als een lijn 'niet verschijnt'. SetRGBFillColor neemt één TColor, dus de bekende VCL-constanten zoals clNavy en clBlack passen er direct in, en Rectangle gebruikt dezelfde breedte-en-hoogte-argumenten als afbeeldingsplaatsing in plaats van twee hoeken. Eén waarschuwing over dunne lijnen: alles onder ongeveer een halve punt kan er elegant uitzien op een monitor en dan verdwijnen op een 600 dpi-kantoorprinter, dus 0,75pt is een redelijke vloer voor elke lijn die een afdruk moet overleven

Paginering tegen echte data, niet tegen voorbeelddata

Eén detail om goed te krijgen voordat de lay-out vastligt: numerieke kolommen moeten op hun rechterrand worden uitgelijnd, en de manier om dat te doen is de gerenderde breedte van elke waarde te meten en deze terug te plaatsen vanaf de kolomgrens, niet om de string op te vullen met voorloopspaties. Spatie-opvulling lijnt alleen uit in een monospaced font, en niemand zet een financieel rapport in een monospaced font. Voer de waarden eerst door Delphi's locale-bewuste routines zoals FormatFloat, zodat het scheidingsteken voor duizendtallen waarvan u de breedte meet hetzelfde is als wat de locale van de klant daadwerkelijk zal weergeven

Het gevaar bij paginering is dat u het schrijft tegen de demodataset, waar tien korte rijen op één pagina passen en de lus nooit hoeft te breken. Productie overhandigt u een klant wiens bedrijfsnaam 140 tekens lang is en een afschrift met 4.000 regelitems, en nu moet de lus elke keer correct breken. Het patroon dat standhoudt is één Y-cursor die naar beneden beweegt terwijl u de hoogte van elke rij aftrekt, en een controle die een nieuwe pagina start zodra de cursor de onderrand zou kruisen. Naar beneden betekent hier Y verlagen, wat de enige plek is waar de oorsprong linksonder contra-intuïtief blijft. Houd al dat in één routine die ook SetFont opnieuw afgeeft en de lopende koptekst op de nieuwe pagina hertekent, en de off-by-one-pagina-bugs krijgen nooit voet aan de grond. Als dezelfde rapporten ook aan archief- of toegankelijkheidsregels moeten voldoen, zijn de keuzes die u hier precies maakt, welke fonts u insluit, of de uitvoer tagged is, welke kleurruimten u gebruikt, degene die die standaarden controleren; de HotPDF PDF/A-, PDF/X- en PDF/UA-gids is het lezen waard voordat de sjabloon hard wordt

Elke aanroep die hier wordt getoond, de tekstpositionering, fontregistratie, afbeeldingsinsluiting en padtekenen, zit in de HotPDF Delphi Component voor Delphi en C++Builder, waarvan de referentie de volledige output-API documenteert naast de formulier-, versleutelings- en ondertekeningsfuncties die ernaast staan