Technisch artikel

Delphi HotPDF Hello World: Uw Eerste PDF Programma

Een eerste regel geschreven vanuit het standpunt van de specificatie belandt op y = 770. De redenering klopt op papier: ISO 32000-1 legt de oorsprong van user space in de hoek linksonder, een A4-pagina is 842 punten hoog, dus 770 zit één inch onder de bovenrand. Draai dat programma tegen een component die vanaf de bovenkant meet en de groet belandt zo’n inch boven de onderrand, met de rest van de pagina leeg erboven. Voeg een tweede pagina toe en het zorgvuldig gekozen font verandert terug in 12-punts Arial. Beide verrassingen komen uit dezelfde twee beslissingen die elk PDF-programma op zijn eerste pagina neemt: welk font actueel is, en vanaf welke hoek de getallen tellen

Diagram van de vieraanroepen HotPDF Hello World-programmastroom in Delphi met de pagina-oorsprong linksboven en twee stille valkuilen uitgelicht
Create, begin, draw, end is de hele ruggengraat, en de oorsprong linksboven verklaart waarom latere regels grotere Y-waarden gebruiken

Het kleinste HotPDF-programma in Delphi

De HotPDF Hello World is een consoleprogramma met vier aanroepen in vaste volgorde: documentproperties zetten, BeginDoc, tekenen met SetFont en TextOut, dan EndDoc. Alles daaromheen is gewoon Delphi. Hier is hij, en hij schrijft één pagina die de lezer in elf talen begroet:

program HelloWorld;

{$APPTYPE CONSOLE}

uses
  Winapi.Windows,   // DEFAULT_CHARSET; op Delphi vóór XE2 gebruik je kale Windows
  HPDFDoc;          // hoofdunit van HotPDF

procedure CreateHelloWorld(const FileName: string);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Documentbrede instellingen: dit alles gaat vóór BeginDoc
    Pdf.FileName := FileName;
    Pdf.AutoLaunch := False;            // default is True: opent de viewer na EndDoc
    Pdf.Compression := cmFlateDecode;   // default is cmNone
    Pdf.FontEmbedding := True;          // al de default, uitgeschreven voor de lezer
    Pdf.EnableFontSubsetting := True;   // sluit alleen de getekende glyphs in
    Pdf.BeginDoc;                       // maakt pagina 1 aan (A4)

    // Het 4e argument heeft default ANSI_CHARSET; geef DEFAULT_CHARSET expliciet door
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);

    // X, Y in punten vanaf de hoek LINKSBOVEN; Y is de bovenrand van de regel
    Pdf.CurrentPage.TextOut(72,  72, 0, 'Hello, Delphi PDF world!');      // English
    Pdf.CurrentPage.TextOut(72, 102, 0, 'Hola, mundo Delphi PDF!');       // Spanish
    Pdf.CurrentPage.TextOut(72, 132, 0, 'Hallo, Delphi PDF Welt!');       // German
    Pdf.CurrentPage.TextOut(72, 162, 0, 'Bonjour, monde PDF Delphi!');    // French
    Pdf.CurrentPage.TextOut(72, 192, 0, 'Ciao, mondo Delphi PDF!');       // Italian
    Pdf.CurrentPage.TextOut(72, 222, 0, 'Olá, mundo Delphi PDF!');        // Portuguese
    Pdf.CurrentPage.TextOut(72, 252, 0, 'Здравствуйте, Delphi PDF мир!'); // Russian
    Pdf.CurrentPage.TextOut(72, 282, 0, 'こんにちは、Delphi PDFの世界!');    // Japanese
    Pdf.CurrentPage.TextOut(72, 312, 0, 'Merhaba, Delphi PDF dünyası!');  // Turkish
    Pdf.CurrentPage.TextOut(72, 342, 0, '你好,Delphi PDF世界');           // Chinese
    Pdf.CurrentPage.TextOut(72, 372, 0, '여보세요, Delphi PDF 세계!');      // Korean

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

begin
  CreateHelloWorld('HelloWorld.pdf');
  Writeln('Wrote HelloWorld.pdf');
end.

Meertalige Hello World PDF geproduceerd door de HotPDF Delphi component in Delphi

THotPDF.Create(nil) samen met try/finally ... Free is het gebruikelijke Delphi-eigendomspatroon, en een PDF produceren verandert daar niets aan. De regels lopen vanaf Y = 72 naar beneden, 30 punten uit elkaar, wat comfortabel regelaandacht overlaat voor tekst van 14 punten. Draai het uit en HelloWorld.pdf verschijnt in de werkmap: één pagina, gecomprimeerde content, het font ingesloten als subset

Documentproperties horen vóór BeginDoc

BeginDoc start het document en maakt zijn eerste pagina aan, dus alles dat de hele bestandsvorm bepaalt moet vóór die aanroep worden toegekend. HotPDF dwingt dit voor sommige properties af in plaats van het aan conventie over te laten. Wijst u Compression ná BeginDoc toe, dan gooit de setter een exceptie met de melding Cannot set Compression value - document in progress.; AutoLaunch wordt op dezelfde manier bewaakt. Luid is hier prima. Stop elke documentinstelling in één blok boven BeginDoc en u ziet die melding nooit

Twee van de defaults verdienen een tweede blik. Compression begint als cmNone, dus een programma dat hem nooit aanraakt schrijft ongecomprimeerde content streams; voor een pagina tekst is dat slechts verspilling, voor een rapport met honderden pagina’s is het het eerste wat iemand aan de bestandsgrootte opvalt. AutoLaunch begint als True, wat betekent dat een consoletool de afgeronde PDF opent in de gekoppelde viewer op het moment dat EndDoc terugkeert. Dat is prettig aan uw bureau en verkeerd in een service, een geplande taak of een build agent. Zet hem voor die omgevingen op False in code; bij CI-runs waar u de code niet kunt veranderen, zet de omgevingsvariabele HOTPDF_SUPPRESS_AUTO_LAUNCH op 1 de start ook uit

Fontafhandeling is waar we een kant kiezen. FontEmbedding staat standaard op True, dus het face reist met het bestand mee en rendert ook op een machine waar Arial Unicode MS nooit is geïnstalleerd. EnableFontSubsetting staat daarentegen standaard uit om volledige fonts beschikbaar te houden voor bewerkworkflows. Voor een gegenereerde groet, factuur of rapport zet u subsetting aan. Arial Unicode MS dekt een enorm bereik aan scripts, en het hele face insluiten om elf korte regels te tonen maakt het bestand megabytes groter zonder zichtbaar voordeel. Laat hem alleen uit staan wanneer iemand verderop de tekst zal bewerken en glyphs nodig heeft die u niet hebt getekend

Waar ligt (0, 0) op een HotPDF-pagina?

HotPDF meet pagina-coördinaten vanaf de hoek linksboven met Y oplopend naar beneden, hetzelfde kader als een VCL TCanvas gebruikt. Bij de default Resolution van 72 is één unit één punt, 1/72 inch. Het PDF-bestand zelf bewaart nog steeds coördinaten van onder naar boven, zoals ISO 32000-1 §8.3.2.3 vereist; HotPDF zet elke Y die u doorgeeft om door hem af te trekken van de paginahoogte wanneer hij de content stream wegschrijft

De rekenkunde achter het openingsvoorbeeld is kort. De standaardpagina die BeginDoc en AddPage aanmaken is A4, 595 bij 842 punten. Een regel die één inch onder de bovenrand hoort te staan is Y = 72 in HotPDF-termen en y = 770 in rauwe PDF-termen, want 842 − 72 = 770. Geef 770 door aan TextOut en de tekst gaat 770 punten omlaag vanaf de bovenkant, wat 72 punten boven de onderrand is

Er schuilt in dezelfde aanroep nog een tweede, kleinere valkuil. Standaard is de Y die u doorgeeft de bovenrand van de tekst, en HotPDF schuift omlaag over de texthoogte voordat hij de baseline neerzet. Code geporteerd uit GDI verwacht meestal precies dat. Code geporteerd uit een andere PDF-library, of uit uw eigen content-stream-writer, verwacht meestal dat Y de baseline is, en elke regel komt dan één texthoogte lager uit dan gepland. THPDFPage.TextFromBaseLine(True) zet de huidige pagina over op baseline-positionering:

// Brug voor coördinaten die uit een bottom-up bron komen (een PDF-layout,
// een diagram uit een specificatie, een andere library). Helper geschreven
// voor dit artikel, geen deel van HotPDF. Geldig bij de default Resolution van 72
function FromBottomUp(Page: THPDFPage; PdfY: Single): Single;
begin
  Result := Page.Height - PdfY;
end;

procedure DrawFromPdfCoordinates(Pdf: THotPDF);
begin
  Pdf.CurrentPage.SetFont('Arial', [], 12);
  // Y benoemt nu de baseline, zoals de eigen tekstoperatoren van PDF glyphs plaatsen
  Pdf.CurrentPage.TextFromBaseLine(True);
  // Baseline 770 punten boven de onderrand, dus Y = 72 vanaf de bovenkant
  Pdf.CurrentPage.TextOut(72, FromBottomUp(Pdf.CurrentPage, 770), 0,
    'Baseline one inch below the top edge');
end;

Resolution is de andere instelling die verandert wat de getallen betekenen. Zet THotPDF.Resolution op 96 en X/Y-coördinaten, marges en TextWidth-resultaten worden uitgedrukt in 1/96 inch, handig wanneer de indeling in schermpixels werd ontworpen. Pagina-Width en Height blijven echter punten rapporteren, dus indelingscode die paginagrenzen met tekencoördinaten mengt leest beter CurrentPage.UserWidth en CurrentPage.UserHeight, die in dezelfde eenheden terugkomen als X en Y. Bij 72 zijn de twee paren gelijk. Wilt u liever in millimeters denken, dan is de property MeasurementUnit (standaard muPoints, met muMM, muInches, muCentimeters, muTwips en muPixels beschikbaar) de nettere schakelaar. Kies één conventie per programma. Stapel ze nooit

Waarom print pagina twee in 12-punts Arial?

Omdat elke nieuwe pagina vanuit een bekende toestand begint. THotPDF.AddPage maakt de nieuwe pagina actueel, stelt hem in op A4 en roept er SetFont('Arial', [], 12) op aan, met de grootte op 12 punten ongeacht de Resolution. BeginDoc maakt pagina één via hetzelfde pad aan, en dat is waarom de Hello World zijn font direct ná BeginDoc zet en niet ervoor. De consequentie voor uitvoer met meerdere pagina’s is makkelijk te missen: het SetFont dat u op pagina één aanriep draagt niet over. Latijnse regels op pagina twee zien er in Arial nog prima uit, alleen kleiner. De CJK-regels verraden het, want Arial heeft daar geen glyphs voor en de reader toont lege vierkantjes of helemaal niets

Een voorspelbaar startfont is het juiste ontwerp voor een pagina-object: teencode voor pagina vijf hangt niet stilletjes af van wat pagina vier toevallig naliet. De prijs is dat pagina-instelling moet worden herhaald, en de netste manier om dat te herhalen is één procedure die elke pagina doorloopt, ook de eerste:

uses
  Winapi.Windows, Vcl.Graphics, HPDFDoc;

// Uw eigen helper: één plek die weet wat een verse pagina nodig heeft
procedure PreparePage(Pdf: THotPDF; IsFirst: Boolean);
begin
  if not IsFirst then
    Pdf.AddPage;                       // zet het font terug op Arial 12
  Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
end;

procedure WriteGreetings(Pdf: THotPDF; Lines: array of string);
const
  TopMargin = 72;
  Leading   = 30;
  LinesPerPage = 20;
var
  I, Row: Integer;
begin
  PreparePage(Pdf, True);              // pagina 1 bestaat na BeginDoc al
  Row := 0;
  for I := 0 to High(Lines) do
  begin
    if Row = LinesPerPage then
    begin
      PreparePage(Pdf, False);
      Row := 0;
    end;
    Pdf.CurrentPage.TextOut(72, TopMargin + Row * Leading, 0, Lines[I]);
    Inc(Row);
  end;

  // Een bold kop heeft zijn eigen SetFont nodig, daarna komt het bodyfont terug
  Pdf.AddPage;
  Pdf.CurrentPage.SetFont('Arial Unicode MS', [fsBold], 18, DEFAULT_CHARSET);
  Pdf.CurrentPage.TextOut(72, 72, 0, 'Appendix');
  Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
  Pdf.CurrentPage.TextOut(72, 110, 0, 'Body text after the heading');
end;

Schakelen uw pagina’s ook over op baseline-positionering met TextFromBaseLine, zet die aanroep dan ook in PreparePage, naast het font. Elke per-pagina-instelling in één procedure houden betekent dat een nieuwe eis, een kleur of een andere grootte één keer wordt toegevoegd en elke pagina bereikt

Eén font voor veel scripts, en waar het stopt

Eén SetFont kan Latijns, Cyrillisch, Japans en Chinees op één pagina dragen, omdat Arial Unicode MS glyphs voor een zeer breed scala aan scripts in één face bundelt. Het vierde argument van SetFont is de Windows character set, en zijn default is ANSI_CHARSET, wat fontkeuze aan de westerse codepagina koppelt. DEFAULT_CHARSET (waarde 1) doorgeven laat Windows het face op naam kiezen, en dat wil een meertalige pagina hebben

Het font moet bestaan op de machine die de PDF bouwt. HotPDF sluit in welk font Windows de naam ook naar oplost, en wanneer Arial Unicode MS ontbreekt substitueert Windows zonder klagen een ander face. De uitvoer sluit dan de vervanger in, en regels in scripts dat dat face niet dekt komen eruit als lege vierkantjes. Een buildserver is de gebruikelijke plek waar dit opduikt, omdat daar niemand Office-fonts heeft geïnstalleerd. Controleer de lijst ingesloten fonts van het eerste bestand dat een nieuwe machine produceert voordat u haar een batchverloop toevertrouwt

Wanneer één face niet elke string dekt, neemt TextOutWithFallback dezelfde argumenten als TextOut, lost per teken een font op, tekent elke reeks met het face dat hem kan renderen en herstelt daarna uw oorspronkelijke font. Right-to-left-scripts zoals Arabisch en Hebreeuws vragen meer dan glyphdekking: ze vragen shaping en omgekeerde reeksvolgorde, en dat is het terrein van de RtLTextOut-functie, doorgaans samen met een TrueType-bestand geladen via RegisterUnicodeTTF

Van hieruit zijn de natuurlijke volgende stappen grootte, stijl en rotatie van tekst, en bewust meerdere blokken op één pagina plaatsen, wat de TextOut-sample doorwerkt. Lijnen, vakken en afbeeldingen gebruiken hetzelfde CurrentPage-object en dezelfde coördinaten vanaf linksboven, dus alles hierboven over oorsprongen en per-pagina-instelling geldt voor hen ongewijzigd

De pagina HotPDF Delphi Component dekt de rest van de teken-API voor Delphi en C++Builder, van vormen en afbeeldingen tot formulieren, zodra deze Hello World schoon draait tegen uw eigen fonts