Een string op (72, 72) komt een inch onder de bovenrand van de pagina terecht, wat iedereen verrast die PDF vanuit de content stream heeft geleerd, waar y = 72 een inch boven de onderkant zit. De tweede verrassing volgt een paar honderd regels later. Een rapport opgezet voor US Letter komt terug met een eerste pagina van 612 bij 792 points en elke pagina daarna op 595 bij 842, omdat de regel die het papierformaat instelde precies één keer liep
Beide effecten hebben dezelfde wortel. Tekst in een PDF is een korte reeks statuswijzigingen (font en grootte, tekenafstand, woordafstand, een tekstmatrix voor positie en rotatie) gevolgd door één show-text-operator, en een high-level-tekstaanroep is een dunne wikkel om die reeks. De aanroep leest de status. Hij zet hem nooit voor je terug, en een nieuwe pagina begint vanaf een vaste set standaardwaarden in plaats van vanaf waar de vorige pagina eindigde
Waar TextOut (X, Y) op de pagina neerzet
HotPDF tekent tekst met THPDFPage.TextOut(X, Y: Single; angle: Extended; Text: WideString), bereikt als Pdf.CurrentPage.TextOut. X en Y worden gemeten vanaf de linkerbovenhoek van de pagina, met Y omlaag oplopend, en bij de standaard Resolution van 72 is één unit één point, 1/72 van een inch. De pagina zet elke Y om naar PDF user space, die van onder naar boven loopt, door hem af te trekken van de paginahoogte. De hoek is in graden, en omdat de tekst een WideString is, gaat Unicode er ongemoeid doorheen. Een tweede overload neemt een PWORD plus een lengte, voor code die al UTF-16-buffers vasthoudt
De pagina-geometrie staat ook vast. BeginDoc voegt de eerste pagina toe, en elke pagina die BeginDoc of AddPage maakt, begint als A4, 595 bij 842 points, met Arial 12 als huidige font. US Letter (612 bij 792) is één toewijzing verwijderd via CurrentPage.Size := Letter, maar die toewijzing hoort bij de pagina, dus hij moet voor elke nieuwe pagina herhaald worden
Standaard is de Y die je doorgeeft de bovenkant van de tekst, en zit de basislijn één font-ascent eronder. TextFromBaseLine(True) schakelt de pagina over op basislijnpositionering. Zet hem aan voor alles wat groottes op één regel mengt. In bovenrandmodus hangt een valutacode van 9 points naast een bedrag van 14 points met zijn bovenkant op de bovenkant van het bedrag, en rijdt hij zichtbaar hoog; in basislijnmodus staan beide runs op dezelfde regel, zoals een zetter ze zou zetten. Het laatste blok van het volgende voorbeeld doet precies dat
Grootte en stijl zitten in SetFont
TextOut heeft geen grootte- of stijlparameter. SetFont(FontName, FontStyle, ASize, FontCharset, IsVertical) draagt het allemaal, met de laatste twee argumenten als standaard ANSI_CHARSET en horizontale tekst, en wat het instelt blijft van kracht voor elke TextOut erna, tot de volgende SetFont. De meeste verwarring op dag één komt daarop terug. Een regel valt vet uit omdat drie aanroepen eerder iets met [fsBold] instelde en niets het weer wegnam
var
W: Single;
begin
Pdf.BeginDoc; // eerste pagina: A4, Arial 12
Pdf.CurrentPage.TextFromBaseLine(True); // elke Y hieronder is een basislijn
Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
Pdf.CurrentPage.TextOut(72, 72, 0, 'Quarterly Report'); // 24 pt normaal
Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
Pdf.CurrentPage.TextOut(72, 104, 0, 'Revenue'); // 12 pt vet
Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
Pdf.CurrentPage.TextOut(72, 120, 0, 'figures in thousands'); // 11 pt cursief
Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsUnderline], 10);
Pdf.CurrentPage.TextOut(72, 136, 0, '+18.4% YoY'); // stijlen combineren
// Gemengde groottes op één basislijn: meet terwijl het 14 pt-lettertype actief is
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
W := Pdf.CurrentPage.GetWideTextWidth('1,284.50');
Pdf.CurrentPage.TextOut(72, 160, 0, '1,284.50');
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(72 + W + 3, 160, 0, 'EUR'); // 3 pt tussenruimte
...
end;
Het stijlargument is een gewone TFontStyles-set, dus [] is normaal en [fsBold, fsItalic] is vet cursief. fsUnderline en fsStrikeOut worden getekend als dunne gevulde balken onder of door de run, en ze volgen de run als die gedraaid is. De grootte is in points, dezelfde unit als de coördinaten, wat de verticale rekenkunde eenvoudig houdt: bodytekst van 11 points met een stap van 16 points laat zo'n vijf points lucht tussen de regels. Er is geen automatische regelafmars. Elke Y bereken je zelf, wat voor een alinea vervelend is en voor een formulier waarvan de velden op vaste posities zitten precies goed
GetWideTextWidth meet met het font dat op het moment van de aanroep actief is, dus in het laatste blok moet de meting gebeuren terwijl het vette 14-point-lettertype nog staat. Zet hem onder de tweede SetFont en het suffix van 9 points landt op de laatste cijfers van het bedrag
De fontnaam wordt opgelost tegen de fonts die geïnstalleerd zijn op de machine die de PDF bouwt, en een lettertype dat op een ontwikkelaarsdesktop resolut heeft, kan op een buildserver ontbreken, dus het lettertype in het definitieve bestand hangt af van welke machine het produceerde. Scriptdekking is de andere helft. Een lettertype met alleen Latijn dat Cyrillisch of CJK moet tekenen, levert missing-glyph-blokjes op, en dat is een eigenschap van het fontbestand, niet van de aanroep. Als één string scripts mengt die geen enkel lettertype allemaal dekt, neemt TextOutWithFallback dezelfde vier argumenten, wisselt per run van lettertype voor tekens die het primaire font mist en zet daarna het originele font terug
Hoe draait het hoekargument de tekst?
Een hoek ongelijk nul draait de run tegen de klok in om die vele graden rond zijn anker, en het anker is de linkerbovenhoek van de tekst in het eigen vlak van de tekst. Het anker beweegt niet. Alleen de richting waarin de glyphs marcheren verandert, dus de coördinaat die een horizontaal label plaatste, plaatst ook zijn gedraaide tweeling. Gedraaide runs houden dat linkerbovenanker, ook als de pagina in basislijnmodus staat; TextFromBaseLine verschuift alleen horizontale runs
var
W, AxisMidY: Single;
begin
AxisMidY := 400; // midden van de Y-as van de grafiek
// Een run van 90 graden leest van onder naar boven en groeit vanaf Y omhoog,
// dus centreren op de as betekent een halve breedte onder het midden beginnen
Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);
W := Pdf.CurrentPage.GetWideTextWidth('Units sold');
Pdf.CurrentPage.TextOut(40, AxisMidY + W / 2, 90, 'Units sold');
// Een schuin watermerk
Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');
// Kolomkoppen 60 graden gekanteld: zelfde Y, stap in X
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');
end;
Negentig graden dekt aslabels en rugtitels. Op 90 graden staan de glyphs rechts van de X van het anker, één regelhoogte breed, en de run klimt naar de bovenkant van de pagina over zijn eigen breedte. Daarom telt centreren een halve breedte bij Y op in plaats van die af te trekken, en zit een label dat op het midden zelf verankerd is er helemaal boven. Zestig graden past bij kolomkoppen: een lang label kan boven een smalle kolom staan zonder in de buur over te lopen, en 45 graden is de gebruikelijke watermerkhoek. Runs die een basislijn delen krijgen dezelfde Y en een gestapte X, precies zoals gestapelde horizontale regels een X delen en in Y stappen
Waarom komt de tweede pagina eruit als A4 in Arial 12?
AddPage geeft de index van de nieuwe pagina terug en maakt hem actueel, en de nieuwe pagina begint vanaf de standaardwaarden: A4, Arial 12, bovenrandpositionering, nul teken- en woordafstand. Niets van de vorige pagina wordt meegenomen. Dat ontwerp houdt elke pagina op zichzelf staand, want de content stream van een PDF-pagina begint toch al met een verse graphics state, maar het betekent dat lay-outcode die de pagina één keer bovenaan het document instelt, op pagina twee stilletjes van papierformaat en lettertype wisselt. De oplossing is alle per-pagina-instellingen in één routine te stoppen en die na BeginDoc en na elke AddPage aan te roepen
procedure StartReportPage(Pdf: THotPDF);
begin
Pdf.CurrentPage.Size := Letter; // een nieuwe pagina begint als A4
Pdf.CurrentPage.TextFromBaseLine(True); // een nieuwe pagina begint in bovenrandmodus
Pdf.CurrentPage.SetFont('Arial', [], 11); // een nieuwe pagina begint in Arial 12
end;
procedure WriteReport(Pdf: THotPDF; Lines: TStrings);
const
LeftMargin = 72; // 1 inch
TopLine = 72; // eerste basislijn, 1 inch omlaag
BottomMargin = 72;
Leading = 16;
var
I: Integer;
Y: Single;
begin
StartReportPage(Pdf);
Y := TopLine;
for I := 0 to Lines.Count - 1 do
begin
if Y > Pdf.CurrentPage.Height - BottomMargin then // 720 op Letter
begin
Pdf.AddPage;
StartReportPage(Pdf);
Y := TopLine;
end;
Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Lines[I]);
Y := Y + Leading;
end;
end;
Onder TextOut zit geen flow layout, en een Y voorbij de onderkant van de pagina is voor de content stream nog steeds een geldige coördinaat: de tekst wordt weggeschreven, valt alleen buiten de media box en geen enkele viewer toont hem. Daarom controleert de lus Y vóór elke regel en opent zelf een nieuwe pagina. De limiet uit CurrentPage.Height lezen in plaats van 720 hard te coderen, houdt de lus correct als iemand de helper later terugzet op A4, waar dezelfde marge van een inch de limiet op 770 legt
De snelste controle is degene die het afdrukvenster van een reader zou doen. Open de uitvoer, ga naar pagina twee en kijk in de documenteigenschappen: een Letter-pagina meldt 8,5 bij 11 inch, een A4-pagina zo'n 8,27 bij 11,69. Als de eerste en de tweede pagina het oneens zijn, woont er een per-pagina-instelling buiten de helper
Teken- en woordafstand zijn ook paginastatus
Soms is de string goed en de breedte verkeerd: een kop die een vaste lijn moet overspannen, een code die leesbaarder is met lossere cijfers, een rij labels waarvan de tussenruimten open moeten. PDF heeft daarvoor twee tekststatusparameters in ISO 32000-1: tekenafstand Tc (§9.3.2), toegevoegd na elke glyph, en woordafstand Tw (§9.3.3), toegevoegd bij elke spatie. HotPDF stelt ze in met SetCharacterSpacing(Spacing: Single) en SetWordSpacing(Spacing: Single), in points bij de standaardresolutie, en zoals het font blijven ze van kracht tot ze veranderd worden
Woordafstand heeft een valkuil die in de specificatie staat. §9.3.3 past Tw alleen toe op de karaktercode 32 van één byte, en uitdrukkelijk niet op een byte 32 binnen een code van meerdere bytes, dus op zichzelf doet de operator niets voor tekst die met glyphkodes van twee bytes is geschreven, en zo worden ingebedde Unicodefonts normaal geschreven. HotPDF schrijft Unicode-runs op die manier, en op dat pad past hij de woordafstand toe als expliciete positiecorrecties na elke spatieglyph, dus SetWordSpacing verwijdt de tussenruimten hoe dan ook. De trigger blijft de gewone spatie, U+0020. Een run Chinees of Japans zonder ASCII-spaties krijgt helemaal geen extra ruimte, en een ideografische spatie (U+3000) wordt als gewone glyph behandeld
Tekenafstand is het juiste gereedschap als een kop een doelbreedte moet halen, want het spreidt de verandering gelijkmatig in plaats van die bij de spaties te bundelen. GetWideTextWidth telt de teken- en woordafstand die op dat moment van kracht is, dus meet met beide op nul en leid de afstand daarna af:
const
Heading = 'SUMMARY';
Target = 200; // breedte van de lijn die de kop moet overspannen, in points
var
Natural, Tc: Single;
begin
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.SetCharacterSpacing(0); // meten zonder achtergebleven afstand
Natural := Pdf.CurrentPage.GetWideTextWidth(Heading);
// Tc komt na elke glyph, maar alleen de ruimtes tussen glyphs zijn zichtbaar
Tc := (Target - Natural) / (Length(Heading) - 1);
Pdf.CurrentPage.SetCharacterSpacing(Tc); // negatieve waarden trekken aan
Pdf.CurrentPage.TextOut(72, 72, 0, Heading);
Pdf.CurrentPage.SetCharacterSpacing(0); // resetten vóór de bodytekst
Pdf.CurrentPage.SetWordSpacing(6);
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 100, 0, 'Name Department Extension');
Pdf.CurrentPage.SetWordSpacing(0);
end;
De resetregels tellen zwaarder dan de setregels. Laat een tekenafstand van 4 points staan na één kop, en elke alinea daaronder erft de uitrekking, wat leest als een vaag gevoel van fout dat een snelle proeflezing overleeft en dan in de gedrukte kopij opduikt. Stel de waarde in, teken de ene run die hem nodig heeft, en zet hem op de volgende regel op nul, zodat latere code nooit hoeft te weten wat een eerdere sectie deed. Dezelfde pagina biedt SetHorizontalScaling (Tz, standaard 100) om een run te verdichten zonder zijn hoogte te veranderen, en SetTextRenderingMode met waarden als trStroke voor tekst met alleen omtrek, die allebei op exact dezelfde manier blijven staan
Is dit je eerste pagina output, dan behandelt het HotPDF Hello World-voorbeeld de documentopzet en het opslaan. Voor tekst die een kolom van rand tot rand moet vullen, heeft volledige uitvulling voor PDF-tekst een eigen walkthrough, en van boven naar beneden lopende CJK-tekst komt aan bod in verticale tekstlay-out in HotPDF. TextOut, SetFont, de afstandsaanroepen en de paginaformaatinstellingen hierboven leveren allemaal mee met het HotPDF Delphi Component voor Delphi en C++Builder