Teknisk artikel

Att skapa PDF:er från grunden med PDFium Component i Delphi

PDFium har ett rykte om sig som en visningsmotor (viewer engine), renderaren bakom Chromes PDF-flik, så den första saken att klara upp är att PDFium Component också kan bygga ett dokument som aldrig existerat förut. Författarsidan (authoring side) lindar in (wraps) PDFiums sidobjekts-API: du gör ett tomt dokument, lägger till sidor med explicita dimensioner och släpper in text, vektorbanor och bilder på varje sida vid koordinater du väljer. Det finns inget sidbeskrivningsspråk att lära sig och ingen skrivardrivrutin i slingan (in the loop). Du anropar metoder, biblioteket sätter ihop PDF-objekt och SaveAs serialiserar resultatet

Vad du inte får är en layoutmotor (layout engine). Det är viktigt nog att säga direkt, eftersom det formar varje exempel nedan. PDFium Component placerar innehåll där du säger åt det, i absoluta koordinater, och ingen annanstans. Det radbryter (wrap) inte ett stycke, flödar inte text över en sidbrytning (page break) eller beräknar en tabell från rader och kolumner. De sakerna är ditt jobb. Om du kom hit och förväntade dig något som omflödar (reflows) prosa så som en ordbehandlare gör, kalibrera dig nu: det här är ett precist, lågnivå-placerings-API, närmare att rita på en duk (canvas) än att sätta (typesetting) ett dokument. För genererade fakturor, certifikat, etiketter och rapportsidor där du redan vet var varje element hör hemma, är den precisionen exakt vad du vill ha

Minimumet som producerar en fil

Tre anrop står mellan en tom TPdf och en sparad PDF: skapa dokumentet, lägg till en sida, skriv ut den. Allt annat är innehåll du skiktar (layer) däremellan

uses
  Vcl.Graphics,   // for clBlack and TColor
  PDFium;         // TPdf lives here

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // empty in-memory document
    Pdf.AddPage(0, 595, 842);           // A4 portrait, in points
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialize to disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

En detalj fäller folk som har sett äldre kodsnuttar (snippets): du tilldelar inte Pdf.Active := True efter CreateDocument. Egenskapen Active rapporterar huruvida ett dokumenthandtag (document handle) existerar, och CreateDocument har redan skapat ett, så egenskapen är True i det ögonblick anropet returnerar. Att ställa in det igen är en no-op i bästa fall och missledande för nästa läsare i värsta fall. Active förtjänar sitt uppehälle på vägen ut: att tilldela False släpper det underliggande dokumentet före Free, vilket är den rena nedtagningsordningen (teardown order). Behandla CreateDocument och en filladdande öppning som ömsesidigt uteslutande (mutually exclusive). Biblioteket vägrar skapa ett nytt dokument på en TPdf som redan har ett öppet, så återanvändning innebär att stänga det nuvarande dokumentet först

Koordinater startar längst ner till vänster

Det andra argumentparet till AddText, och till varje placeringsanrop, är en punkt i PDF-användarrymden (user space). Origo sitter i sidans nedre vänstra hörn, X löper åt höger och Y löper uppåt. En enhet är en punkt, 1/72 tum, så en A4-sida är 595 x 842 enheter och US Letter är 612 x 792. Det där uppåtgående Y:et är den enskilt vanligaste källan till förvirringen "min text är utanför sidan", eftersom skärm- och bitmappskoordinater lägger origo i toppen med Y växande nedåt. På en 842-punkter hög sida sitter en rubrik nära toppen runt Y 780, inte Y 60. När en körning landar någonstans oväntat är sidhöjden minus ditt Y nästan alltid det tal du egentligen menade

AddPage tar en insättningsposition (insertion position) som sitt första argument, uttryckt 1-baserat, med 0 som en bekväm "början av dokument"-förkortning (shorthand). Skicka in 0 eller 1 för den första sidan så sätts sidan in längst fram; skicka in värdet som matchar det antal du lägger till (appending) i för att lägga till på slutet. Den nyligen tillagda sidan blir också den aktuella sidan, den som efterföljande ritanrop siktar på, så det finns inget separat "välj den här sidan"-steg efter att ha lagt till den. Om du lägger till flera sidor och senare behöver rita tillbaka på en tidigare sida, ställ in PageNumber för att flytta markören; medan du fyller i sidor i ordning allteftersom du skapar dem kan du låta den vara

Att skriva text, och typsnittsregeln som bits i tysthet

AddText-signaturen bär allt som en enskild körning (run) behöver: strängen, ett typsnittsnamn (font name), en storlek i punkter, X- och Y-ankaret (anchor), sedan valfri färg, en alfa-byte (alpha byte) för transparens, och en rotationsvinkel i grader

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Title in black, default opacity, no rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // A lighter byline 24 points below it
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // A faint diagonal draft stamp across the page
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Alfa-byten löper från $00 (osynlig) till $FF (opak/ogenomskinlig), vilket är det som gör draft-stämpeln till en vattenstämpel (watermark) snarare än ett solitt block: $30 är ungefär nitton procents opacitet, tillräckligt för att läsa igenom. Vinkeln roterar körningen moturs (counterclockwise) runt dess ankare, så 45 grader ger den klassiska hörn-till-hörn-stämpeln. Inget av detta behöver en separat vattenstämpelegenskap. En vattenstämpel är bara ett stort, halvtransparent, roterat AddText-anrop, och att rita den före eller efter brödtexten (the body) avgör om den sitter bakom eller ovanpå innehållet

Typsnitt förtjänar en noggrann mening, eftersom felfallet (failure mode) är tyst. När du skickar in ett typsnittsnamn frågar PDFium Component operativsystemet efter det typsnittets TrueType-data och bäddar in det i dokumentet, vilket är anledningen till att en fil som byggts på din maskin renderas identiskt på en som aldrig har haft typsnittet installerat. Haken är vad som händer när namnet inte löses upp (does not resolve): ett stavfel eller ett typsnitt (face) som helt enkelt inte är närvarande på byggmaskinen. Det blir inget undantag. Biblioteket faller tillbaka (falls back) till att skapa ett textobjekt som bara bär namnet som en etikett, utan att något bäddas in, och överlåter åt visaren (viewer) att ersätta med vad den anser ligga nära. Texten visas i dina tester, ser rimlig ut (looks plausible), och skiftar mått (metrics) eller glyfer i samma ögonblick som filen öppnas någonstans där det finns andra typsnitt installerade. Använd namn som du vet är närvarande på den genererande maskinen, behandla typsnittslistan som ett distributionsberoende (deployment dependency), och öppna ett prov (sample) i en visare på ett rent system innan du litar på utdatan

Vektorformer: bygg en bana och verkställ den sedan

Linjer, rektanglar och fyllda områden går genom en bana (path). Du öppnar en med CreatePath, vilket sätter startpunkten och hela utformningen (styling) på en gång: fyllningsläge (fill mode), fyllnings- och linjefärger (stroke colors) med deras egna alfa-bytes, linjebredd, linjeavslutningar (line caps) och fogar (joins). Sedan förlänger du den med LineTo, BezierTo och ClosePath, och slutligen verkställer (commits) AddPath den färdiga banan på sidan. Verkställande-steget (commit step) är lätt att glömma och producerar ingenting om du hoppar över det

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // A thin horizontal rule. The rectangle overload sets a box directly:
  // X, Y, Width, Height, then fill mode and colors.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Point overload: start at the first vertex, line to the rest, close.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nothing is drawn until this runs
end;

Två överlagringar (overloads) täcker de vanliga fallen. Formen med fyra koordinater tar X, Y, bredd och höjd och ger dig en axeljusterad (axis-aligned) rektangel i ett anrop, vilket är vad du sträcker dig efter för att rita ett streck, en cellram eller en fylld bakgrundspanel. Formen med två koordinater sätter bara en startpunkt, och du ritar resten av konturen själv med LineTo och BezierTo. Fyllningsläget styr hur överlappande regioner målas: fmWinding (skild-från-noll lindning) passar de flesta solida former, fmAlternate (jämn-udda) hanterar utskärningar och självkorsande konturer, och fmNone lämnar en bana med enbart linje (stroked-only) utan fyllning, vilket är vad avdelaren ovan använder

Tabeller är banor och text, sammansatta för hand

Eftersom det inte finns någon tabellprimitiv är en tabell en loop. Du bestämmer kolumnens X-förskjutningar (offsets) och radhöjden, skriver in varje cell med AddText och ritar strecken (rules) med rektangelbanor (rectangle paths). Aritmetiken är din, men den är enkel (plain), och när den väl är skriven generaliseras den till vilket rutnät (grid) du än behöver

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // column offsets
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Header row
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Rule under the header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Data rows, stepping Y downward each iteration
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

Lägg märke till att Y kliver nedåt med radhöjden för varje pass, återigen eftersom uppåt är positivt. Det är också här som frånvaron av textmätning (text measurement) visar sig: ingenting hindrar ett långt artikelnamn från att flöda över (overrunning) in i nästa kolumn, eftersom biblioteket inte vet hur bred din sträng renderades. För utdata i fast format där du kontrollerar datan, dimensionerar du kolumner generöst och går vidare. För genuint variabelt innehåll måste du antingen begränsa indatan eller mäta glyfbredderna själv innan du placerar dem, vilket är den punkt då ett dedikerat kompositionsbibliotek börjar löna sig

Bilder och flera sidor

Rasterinnehåll kommer in genom bildhjälparna. AddPicture tar en laddad TPicture och placerar den vid en punkt, med valfri bredd och höjd för att skala den; AddImage accepterar en filsökväg eller en TBitmap direkt, och AddJpegImage strömmar (streams) JPEG-bytes utan en rundresa genom en bitmapp. Liksom med allt annat är placeringskoordinaterna bildens nedre vänstra hörn i användarrymden, och bredden och höjden är on-page-storleken i punkter, inte källans pixeldimensioner

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; the new page becomes current
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer near the bottom edge
      // ... draw this page's body here ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Ett flersidigt dokument är ensides-mönstret i en loop. Varje AddPage lägger till (appends) en sida och gör den aktuell, så den brödtext och fotnot du ritar därefter landar på den sida du just lade till. Du omfördelar (reassign) inte PageNumber inuti den här loopen, eftersom tillägget av en sida redan har flyttat markören (cursor) dit; du behöver bara PageNumber när du går tillbaka till en sida utanför skapelseordningen (creation order). Anropa SaveAs en gång i slutet, efter att den sista sidan är fylld. Om du behöver en arkiveringsprofil snarare än en vanlig fil, exponerar samma dokumentobjekt SaveAsPdfA och de andra efterlevnadsvarianterna (conformance variants), så valet av utdatastandard (output standard) är ett annorlunda spara-anrop, inte en annan byggväg (build path)

Var detta passar in

Den ärliga inramningen är att PDFium Components författar-API (authoring API) är ett troget (faithful), tunt lager över PDFiums sidobjektsmodell: äkta dokumentskapande, äkta inbäddade typsnitt, äkta vektor- och rasterinnehåll, serialiserat till en standardföljande fil. Det är inte, och låtsas inte vara, en omflödande dokumentmotor. Den avgörande gränsen (dividing line) är textlayout. Om din utmatning (output) är mallad (templated) – fakturor, certifikat, etiketter, instrumentpaneler (dashboards) renderade till ett fast rutnät – är absolutkoordinat-modellen direkt och snabb, och koden förblir läsbar. Om din utmatning är långformig prosa som måste radbrytas och sidbrytas (paginate) på egen hand, kommer du att bygga om en layoutmotor ovanpå dessa anrop, och då är det fel verktyg för jobbet. Att veta vilken sida av den linjen du befinner dig på utgör det mesta av beslutet

De skapelse-metoder som beskrivs här är en del av PDFium Component för Delphi, vilken parar (pairs) denna författar-väg med de renderings- och textextraherings-funktioner som PDFium är mer känd för