Teknisk artikkel

Opprette PDF-er fra bunnen av med PDFium-komponent i Delphi

PDFium har et rykte som en visningsmotor, gjengiveren bak Chromes PDF-fane, så det første å oppklare er at PDFium-komponenten også kan bygge et dokument som aldri har eksistert før. Forfattersiden pakker inn PDFiums side-objekt API: du lager et tomt dokument, legger til sider med eksplisitte dimensjoner, og slipper tekst, vektorbaner og bilder på hver side på koordinater du velger. Det er ingen sidebeskrivelsesspråk å lære og ingen utskriftsdriver i løkken. Du kaller metoder, biblioteket setter sammen PDF-objekter, og SaveAs serialiserer resultatet

Det du ikke får er en layoutmotor. Dette betyr nok til å si det på forhånd, fordi det former hvert eksempel nedenfor. PDFium-komponenten plasserer innhold der du forteller det å gjøre det, i absolutte koordinater, og ingen andre steder. Den vil ikke bryte et avsnitt, flyte tekst over et sideskift eller beregne en tabell fra rader og kolonner. Det er din jobb. Hvis du kom med forventninger om noe som flyter om prosa slik et tekstbehandlingsprogram gjør det, kalibrer nå: dette er et presist API for plassering på lavt nivå, nærmere det å tegne på et lerret enn å sette et dokument. For genererte fakturaer, sertifikater, etiketter og rapportsider der du allerede vet hvor hvert element hører hjemme, er den presisjonen akkurat det du ønsker

Minimumet som produserer en fil

Tre kall står mellom en tom TPdf og en lagret PDF: opprett dokumentet, legg til en side, skriv det ut. Alt annet er innhold du legger inn imellom

Diagram over den firetrinns PDF-opprettingsflyten i PDFium Component for Delphi, fra CreateDocument gjennom AddPage og innholdskall til SaveAs
CreateDocument starter et tomt i-minne-dokument, hver AddPage blir gjeldende side, og SaveAs serialiserer de monterte PDF-objektene til disk
uses
  Vcl.Graphics,   // for clBlack og TColor
  PDFium;         // TPdf bor her

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // tomt minne-dokument
    Pdf.AddPage(0, 595, 842);           // A4-portrett, i punkter
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialiser til disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Én detalj feller folk som har sett eldre kodesnutter: du tildeler ikke Pdf.Active := True etter CreateDocument. Egenskapen Active rapporterer om et dokumenthåndtak eksisterer, og CreateDocument har allerede opprettet ett, så egenskapen er sann i det øyeblikket det kallet returnerer. Å sette den på nytt er en nulloperasjon i beste fall og villedende for neste leser i verste fall. Active gjør jobben sin på vei ut: å tilordne False frigjør det underliggende dokumentet før Free, som er den rene nedrivningsrekkefølgen. Behandle CreateDocument og en fillastende åpning som gjensidig utelukkende. Biblioteket nekter å opprette et nytt dokument på en TPdf som allerede har ett åpent, så gjenbruk betyr å lukke gjeldende dokument først

Koordinater starter nederst til venstre

Det andre argumentparet til AddText, og til hvert plasseringskall, er et punkt i PDF-brukerrommet. Opprinnelsen sitter nederst i venstre hjørne av siden, X går mot høyre, og Y går opp. Én enhet er ett punkt, 1/72 av en tomme, så en A4-side er 595 ganger 842 enheter og US Letter er 612 ganger 792. Denne oppovergående Y-en er den desidert vanligste kilden til forvirring av typen "teksten min er utenfor siden", fordi skjerm- og bitkartkoordinater plasserer opprinnelsen øverst med Y som vokser nedover. På en side som er 842 punkter høy, sitter en overskrift nær toppen rundt Y 780, ikke Y 60. Når en kjøring lander et uventet sted, er sidehøyden minus Y-en din nesten alltid det tallet du egentlig mente

PDFium Component-diagram som setter PDF-brukerrom, hvis origo ligger nede til venstre med Y voksende oppover, opp mot skjermkoordinater hvis Y vokser nedover fra øverst til venstre
En overskrift nær toppen av en A4-side trenger Y rundt 780 i PDF-brukerrom, mens skjermvaner ville skrevet Y 60 og landet teksten nær bunnen

AddPage tar en innsettingsposisjon som sitt første argument, uttrykt én-basert, med 0 som en praktisk stenografi for "start av dokument". Send 0 eller 1 for den første siden, og siden settes inn foran; send verdien som samsvarer med opptellingen du legger til for å legge til på slutten. Den nylig tillagte siden blir også den gjeldende siden, den som etterfølgende tegnekall retter seg mot, så det er ikke noe eget "velg denne siden"-trinn etter å ha lagt den til. Hvis du legger til flere sider og senere trenger å tegne tilbake på en tidligere en, sett PageNumber for å flytte markøren; mens du fyller sider i rekkefølge etter hvert som du oppretter dem, kan du la den være i fred

Skrive tekst, og skrifttyperegelen som biter i det stille

AddText-signaturen bærer alt en enkelt kjøring trenger: strengen, et skrifttypenavn, en størrelse i punkter, X- og Y-ankeret, deretter valgfri farge, en alfa-byte for åpenhet (transparency), og en rotasjonsvinkel i grader

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Tittel i svart, standard opasitet, ingen rotasjon
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // En lysere byline 24 punkter under den
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // Et svakt diagonalt utkaststempelet over siden
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Alfa-byten går fra $00 (usynlig) til $FF (ugjennomsiktig), noe som gjør utkaststempelet til et vannmerke i stedet for en solid blokk: $30 er omtrent nitten prosent opasitet, nok til å lese gjennom. Vinkelen roterer kjøringen mot klokken rundt ankeret sitt, så 45 grader gir det klassiske hjørne-til-hjørne-stempelet. Ingenting av dette trenger en egen vannmerkefunksjon. Et vannmerke er bare et stort, halvgjennomsiktig, rotert AddText-kall, og å tegne det før eller etter brødteksten avgjør om det sitter bak eller oppå innholdet

Skrifttyper fortjener en nøye setning, fordi feilmodusen er stille. Når du sender et skrifttypenavn, ber PDFium-komponenten operativsystemet om den skrifttypens TrueType-data og bygger den inn i dokumentet, som er grunnen til at en fil bygget på din maskin gjengis identisk på en som aldri har hatt skrifttypen installert. Haken er hva som skjer når navnet ikke løses opp: en skrivefeil, eller et snitt som rett og slett ikke er til stede på byggemaskinen. Det er ikke noe unntak. Biblioteket faller tilbake til å opprette et tekstobjekt som bærer navnet kun som en etikett, med ingenting innebygd, og overlater til visningsprogrammet å erstatte med hva enn det anser som nært. Teksten vises i testene dine, ser plausibel ut, og forskyver metrikk eller glyfer i det øyeblikket filen åpnes et sted med forskjellige skrifttyper installert. Bruk navn du vet er til stede på den genererende maskinen, behandle skrifttypelisten som en distribusjonsavhengighet, og åpne en prøve i et visningsprogram på et rent system før du stoler på utdataene

Vektorformer: bygg en bane, og forplikt den

Linjer, rektangler og fylte områder går gjennom en bane. Du åpner en med CreatePath, som setter startpunktet og all stilsetting på én gang, fyllmodus, fyll- og strekfarger med sine egne alfa-byter, strekbredde, linjeender og -sammenføyninger. Så forlenger du den med LineTo, BezierTo og ClosePath, og til slutt forplikter (commits) AddPath den ferdige banen inn på siden. Forpliktelsestrinnet er lett å glemme og produserer ingenting hvis du hopper over det

Diagram over vektorsti-livssyklusen i PDFium Component, der CreatePath setter startpunktet og stilingen, LineTo og BezierTo sporer omrisset, og AddPath committer tegningen
CreatePath fastsetter startpunktet og hver stil på forhånd, men ingenting vises på siden før AddPath forplikter den ferdige pathen
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // En tynn vannrett linje. Rektangeloverlastningen setter en boks direkte:
  // X, Y, Width, Height, deretter fyllmodus og farger.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Punktoverlastning: start ved det første hjørnet, trekk linje til resten, lukk.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // ingenting tegnes før dette kjører
end;

To overbelastninger dekker de vanlige tilfellene. Firkordinatformen tar X, Y, bredde og høyde og gir deg et aksejustert rektangel i ett kall, som er det du strekker deg etter for å tegne en regel, en cellekant eller et fylt bakgrunnspanel. Tokoordinatformen setter bare et startpunkt, og du sporer resten av omrisset selv med LineTo og BezierTo. Fyllmodus styrer hvordan overlappende områder males: fmWinding (vinding forskjellig fra null) passer til de fleste solide former, fmAlternate (partall-oddetall) håndterer utskjæringer og selvkryssende omriss, og fmNone etterlater en kun-streket bane uten fyll, som er det skillelinjen ovenfor bruker

Tabeller er baner og tekst, satt sammen for hånd

Fordi det ikke er noen tabell-primitiv, er en tabell en løkke. Du bestemmer kolonnenes X-forskyvninger og radhøyden, skriver hver celle med AddText, og tegner reglene med rektangelbaner. Aritmetikken er din, men den er grei, og når den først er skrevet, generaliserer den til hvilket som helst rutenett du trenger

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // kolonneforskyvninger
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Overskriftsrad
  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);

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

  // Datarader, trinnvis nedover i Y for hver iterasjon
  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;

Legg merke til at Y trapper nedover med radhøyden hvert pass, igjen fordi opp er positivt. Det er også her fraværet av tekstmåling viser seg: ingenting stopper et langt varenavn fra å flyte over i neste kolonne, fordi biblioteket ikke vet hvor bred strengen din ble gjengitt. For utdata med fast format der du kontrollerer dataene, dimensjonerer du kolonner raust og går videre. For genuint variabelt innhold, enten begrenser du inndata eller måler glyfbredder selv før du plasserer dem, som er punktet der et dedikert komposisjonsbibliotek begynner å betale for seg

Bilder og flere sider

Rasterinnhold kommer inn gjennom bildehjelperne. AddPicture tar en innlastet TPicture og plasserer den i et punkt, med en valgfri bredde og høyde for å skalere den; AddImage aksepterer en filsti eller en TBitmap direkte, og AddJpegImage strømmer JPEG-bytes uten en rundtur gjennom et bitkart. Som med alt annet er plasseringskoordinatene nederste venstre hjørne av bildet i brukerrommet, og bredden og høyden er størrelsen på siden i punkter, ikke pikseldimensjonene til kilden

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);     // legg til; den nye siden blir gjeldende
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // bunntekst nær nederkanten
      // ... tegn denne sidens brødtekst her ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Et flersidig dokument er enkeltsidemønsteret i en løkke. Hver AddPage legger til en side og gjør den gjeldende, så brødteksten og bunnteksten du tegner deretter lander på siden du nettopp la til. Du tildeler ikke PageNumber på nytt inni denne løkken, fordi å legge til en side allerede flyttet markøren dit; du trenger bare PageNumber når du går tilbake til en side utenfor opprettelsesrekkefølgen. Kall SaveAs én gang på slutten, etter at den siste siden er fylt. Hvis du trenger en arkivprofil i stedet for en vanlig fil, eksponerer det samme dokumentobjektet SaveAsPdfA og de andre samsvarsvariantene, så valget av utdatastandard er et annet lagringskall, ikke en annen byggevei

Hvor dette passer inn

Den ærlige innrammingen er at PDFium-komponentens forfatter-API er et trofast, tynt lag over PDFiums side-objekt-modell: ekte dokumentopprettelse, ekte innebygde skrifttyper, ekte vektor- og rasterinnhold, serialisert til en standardsamsvarende fil. Den er ikke, og utgir seg ikke for å være, en omflytende dokumentmotor. Skillelinjen er tekstlayout. Hvis utdataene dine er malbaserte, fakturaer, sertifikater, etiketter, dashbord gjengitt til et fast rutenett, er den absolutte koordinatmodellen direkte og rask, og koden forblir lesbar. Hvis utdataene dine er langform prosa som må brytes og pagineres på egen hånd, vil du bygge opp en layoutmotor på toppen av disse kallene, og det er feil verktøy for jobben. Å vite hvilken side av den linjen du befinner deg på er mesteparten av avgjørelsen

Opprettelsesmetodene beskrevet her er en del av PDFium-komponenten for Delphi, som kobler denne forfatterveien sammen med gjengivelses- og tekstutvinningsfunksjonene PDFium er bedre kjent for