PDFium heeft een reputatie als viewer-engine, de renderer achter het PDF-tabblad van Chrome. Het eerste dat dus moet worden opgehelderd, is dat de PDFium-component ook een document kan opbouwen dat nog nooit eerder heeft bestaan. De kant van het schrijven (authoring side) omhult de page-object API van PDFium: u maakt een leeg document, voegt pagina's toe met expliciete afmetingen, en plaatst (drop) tekst, vectorpaden en afbeeldingen op elke pagina op coördinaten die u zelf kiest. U hoeft geen paginabeschrijvingstaal (page description language) te leren en er is geen printerstuurprogramma (print driver) in the loop. U roept methoden aan, de bibliotheek stelt PDF-objecten samen en SaveAs serialiseert het resultaat
Wat u níét krijgt, is een lay-out-engine (layout engine). Dit is belangrijk genoeg om vooraf te vermelden, omdat het elk onderstaand voorbeeld vormt. De PDFium-component plaatst content waar u zegt dat het moet staan, in absolute coördinaten, en nergens anders. Het zal geen alinea afbreken (wrap), tekst niet over een pagina-einde heen laten vloeien, of een tabel berekenen uit rijen en kolommen. Dat is uw taak. Als u was gekomen met de verwachting iets te vinden dat proza laat reflowen op de manier waarop een tekstverwerker dat doet, pas dan uw verwachtingen nu aan: dit is een nauwkeurige, low-level plaatsings-API, die dichter bij tekenen op een canvas staat dan bij het zetten (typesetting) van een document. Voor gegenereerde facturen, certificaten, labels en rapportpagina's waarbij u al weet waar elk element thuishoort, is die precisie precies wat u zoekt
Het minimum dat een bestand produceert
Er staan drie aanroepen tussen een lege TPdf en een opgeslagen PDF: het document maken, een pagina toevoegen en wegschrijven (write it out). Al het andere is content die u ertussen in lagen opbouwt
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;
Eén detail brengt mensen in verwarring die oudere codefragmenten hebben gezien: u wijst geen Pdf.Active := True toe ná CreateDocument. De eigenschap (property) Active rapporteert of er een document-handle bestaat, en CreateDocument heeft er al een gemaakt. De eigenschap is dus 'True' op het moment dat die aanroep terugkeert. Het nogmaals instellen is op zijn best een no-op en op zijn slechtst misleidend voor de volgende lezer. Active verdient zijn sporen (earns its keep) bij het afsluiten: het toewijzen van 'False' geeft het onderliggende document vrij vóór Free, wat de schone teardown-volgorde is. Behandel CreateDocument en een bestandslandende 'open' als elkaar uitsluitend (mutually exclusive). De bibliotheek weigert een nieuw document te maken op een TPdf die er al een open heeft staan. Hergebruik (reuse) betekent dus dat u het huidige document eerst moet sluiten
Coördinaten beginnen linksonder
Het tweede argumentenpaar voor AddText, en voor elke plaatsingsaanroep (placement call), is een punt in de PDF user space. De oorsprong bevindt zich in de linkerbenedenhoek van de pagina, X loopt naar rechts en Y loopt omhoog. Eén eenheid is één punt, 1/72 van een inch, dus een A4-pagina is 595 bij 842 eenheden en US Letter is 612 bij 792. Die opwaartse Y is veruit de meest voorkomende bron van "mijn tekst valt buiten de pagina"-verwarring, aangezien scherm- en bitmapcoördinaten de oorsprong bovenaan plaatsen waarbij Y naar beneden groeit. Op een pagina van 842 punten hoog, bevindt een kop (heading) in de buurt van de bovenkant zich rond Y 780, niet Y 60. Wanneer een reeks (run) ergens onverwachts belandt, is de paginahoogte minus uw Y vrijwel altijd het getal dat u eigenlijk bedoelde
AddPage neemt een invoegpositie als eerste argument, uitgedrukt als 1-gebaseerd (one-based), met 0 als een handige kortere weg (shorthand) voor "begin van het document". Geef 0 of 1 door voor de eerste pagina en de pagina wordt vooraan ingevoegd; geef de waarde door die overeenkomt met de telling (count) waaraan u toevoegt om deze aan het einde toe te voegen. De nieuw toegevoegde pagina wordt tevens de huidige pagina, de pagina waarop daaropvolgende tekenaanroepen zich richten. Er is dus geen afzonderlijke "selecteer deze pagina"-stap na het toevoegen. Als u meerdere pagina's toevoegt en later terug moet tekenen op een eerdere pagina, stelt u PageNumber in om de cursor te verplaatsen. Zolang u pagina's op volgorde vult naarmate u ze aanmaakt, kunt u dit ongemoeid laten
Tekst schrijven, en de lettertyperegel die stilzwijgend bijt
De handtekening (signature) van AddText bevat alles wat een enkele run nodig heeft: de string, een lettertypenaam, een grootte in punten, het X- en Y-anker, en vervolgens optioneel een kleur, een alpha-byte voor transparantie en een rotatiehoek in graden
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;
De alpha-byte loopt van $00 (onzichtbaar) tot $FF (ondoorzichtig). Dit is wat de concept-stempel (draft stamp) een watermerk maakt in plaats van een massief blok: $30 is ongeveer negentien procent ondoorzichtigheid (opacity), wat voldoende is om er doorheen te kunnen lezen. De hoek roteert de run tegen de klok in rond zijn anker, dus 45 graden geeft de klassieke 'van-hoek-tot-hoek'-stempel. Voor niets hiervan is een aparte watermerkfunctie nodig. Een watermerk is gewoon een grote, semi-transparante, geroteerde AddText-aanroep. Het moment van tekenen (ervóór of na de body) bepaalt of het achter of bovenop de content ligt
Lettertypen verdienen een zorgvuldige zin, omdat de storingsmodus (failure mode) stilzwijgend is. Wanneer u een lettertypenaam doorgeeft, vraagt de PDFium-component het besturingssysteem om de TrueType-gegevens van dat lettertype en sluit deze in (embeds) in het document. Dat is de reden waarom een bestand dat op uw machine is gebouwd, identiek rendert op een machine waarop het lettertype nog nooit geïnstalleerd is geweest. Het addertje onder het gras (the catch) is wat er gebeurt wanneer de naam niet opgelost kan worden: een typefout, of een lettertype dat simpelweg niet aanwezig is op de build machine. Er vindt geen exceptie plaats. De bibliotheek valt terug op het maken van een tekstobject dat de naam uitsluitend als label draagt, zonder dat er iets is ingesloten, en laat het aan de viewer over om iets te substitueren wat in de buurt komt. De tekst verschijnt in uw tests, ziet er plausibel uit, en verschuift metrics of tekens (glyphs) op het moment dat het bestand ergens wordt geopend waar andere lettertypen zijn geïnstalleerd. Gebruik namen waarvan u weet dat ze aanwezig zijn op de genererende machine, behandel de lettertypelijst als een deployment dependency en open een sample in een viewer op een schoon systeem voordat u de output vertrouwt
Vormen in vectoren (Vector shapes): bouw een pad, leg het vervolgens vast
Lijnen, rechthoeken en gevulde gebieden verlopen via een pad. U opent er een met CreatePath, dat in één klap het startpunt en alle styling instelt, evenals de vulmodus (fill mode), vul- en omlijningskleuren (fill and stroke colors) met hun eigen alpha-bytes, omlijningsbreedte, linuiteinden (line caps) en joins. Vervolgens breidt u het uit met LineTo, BezierTo en ClosePath, waarna AddPath tot slot het voltooide pad vastlegt (commits) op de pagina. Deze vastleggingsstap (commit step) is makkelijk te vergeten en produceert niets als u deze overslaat
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;
Twee overloads (overbelastingen) dekken de veelvoorkomende gevallen. De vorm met vier coördinaten neemt X, Y, breedte en hoogte en geeft u in één call een as-uitgelijnde (axis-aligned) rechthoek. Hier grijpt u naar om een lijn, een celrand of een gevuld achtergrondpaneel te tekenen. De vorm met twee coördinaten stelt uitsluitend een startpunt in, waarna u de rest van de omtrek zelf traceert met LineTo en BezierTo. De vulmodus (fill mode) regelt hoe overlappende gebieden worden geverfd (painted): fmWinding (nonzero winding) is geschikt voor de meeste massieve (solid) vormen, fmAlternate (even-odd) verwerkt uitsnijdingen en elkaar kruisende contouren, en fmNone laat een uitsluitend omlijnd (stroked-only) pad zonder vulling achter, wat de scheidslijn (divider) hierboven gebruikt
Tabellen zijn paden en tekst, handmatig in elkaar gezet
Omdat er geen tabelprimitief (table primitive) bestaat, is een tabel een loop (lus). U bepaalt de X-offsets van de kolommen en de rijhoogte, schrijft elke cel met AddText en tekent de lijnen (rules) met rechthoekige paden. Het rekenwerk is voor u, maar het is ongecompliceerd (plain), en eenmaal geschreven kan het worden gegeneraliseerd naar elk willekeurig raster dat u nodig hebt
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;
Let op de Y die bij elke doorgang (pass) naar beneden stapt met de rijhoogte, wederom omdat omhoog positief is. Dit is tevens de plek waar de afwezigheid van tekstmeting (text measurement) zichtbaar wordt: niets weerhoudt een lange itemnaam ervan om de volgende kolom te overschrijden, omdat de bibliotheek niet weet hoe breed uw string is gerenderd. Voor fixed-format output waarbij u controle over de data hebt, bemeten (size) we de kolommen ruim en gaan we verder. Voor daadwerkelijk variabele inhoud, moet u óf de input beperken óf u meet zelf de tekenbreedten (glyph widths) voordat u ze plaatst. Dit is het moment waarop een gespecialiseerde compositiebibliotheek (composition library) zichzelf begint terug te verdienen
Afbeeldingen en meerdere pagina's
Rastercontent komt binnen via de image helpers (afbeeldingshelpers). AddPicture neemt een geladen TPicture en plaatst deze op een bepaald punt, met een optionele breedte en hoogte om het te schalen; AddImage accepteert direct een bestandspad of een TBitmap, en AddJpegImage streamt JPEG-bytes zonder een round trip (omweg) via een bitmap. Net als bij al het andere zijn de plaatsingscoördinaten de linkerbenedenhoek van de afbeelding in user space, en de breedte en hoogte zijn het formaat op de pagina in punten (points), niet de pixelafmetingen van de bron
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;
Een document met meerdere pagina's is het patroon van één pagina in een loop (lus). Elke AddPage voegt een pagina toe (appends) en maakt deze huidig (current), zodat de body en voettekst (footer) die u hierna tekent op de pagina belanden die u zojuist hebt toegevoegd. U wijst PageNumber niet opnieuw toe binnen deze loop, omdat het toevoegen van een pagina de cursor al daarheen heeft verplaatst. U hebt PageNumber pas nodig als u teruggaat naar een pagina buiten de aanmaakvolgorde om (creation order). Roep SaveAs één keer aan het eind aan, nadat de laatste pagina is gevuld. Als u een archiefprofiel (archival profile) nodig hebt in plaats van een gewoon bestand, stelt hetzelfde documentobject SaveAsPdfA en de andere conformiteitsvarianten beschikbaar. De keuze van de uitvoerstandaard is dus een andere opslag-aanroep, geen ander bouwpad
Waar dit thuishoort
De eerlijke kadrering (framing) is dat de authoring API van de PDFium-component een getrouwe, dunne laag is over het page-object model van PDFium: echte documentcreatie, echte ingesloten (embedded) lettertypen, echte vector- en rastercontent, geserialiseerd naar een bestand dat aan de standaarden voldoet. Het is géén reflowende document-engine, en pretendeert dat ook niet te zijn. De scheidslijn is de lay-out van de tekst (text layout). Als uw output getemplateerd (templated) is (facturen, certificaten, labels, dashboards gerenderd in een vast raster), is het absolute-coördinatenmodel direct en snel, en blijft de code leesbaar. Als uw uitvoer long-form proza (lange vorm proza) is dat zelfstandig (on its own) moet afbreken (wrap) en pagineren, zult u bovenop deze calls een lay-out-engine moeten herbouwen. Dat is de verkeerde tool voor de klus. Weten aan welke kant van die lijn u staat, is het grootste deel van de beslissing
De hier beschreven methoden voor creatie maken deel uit van de PDFium-component voor Delphi, die dit schrijftraject (authoring path) koppelt aan de rendering- en tekstextractiefuncties waar PDFium beter om bekend staat