Articol tehnic

Crearea PDF-urilor de la zero cu PDFium în Delphi

PDFium are reputația de motor de vizualizare, randorul din spatele filei PDF din Chrome, așa că primul lucru de lămurit este că PDFium Component poate și să construiască un document care nu a existat până atunci. Latura de creație împachetează API-ul de obiecte de pagină al lui PDFium: faceți un document gol, adăugați pagini cu dimensiuni explicite și așezați text, trasee vectoriale și imagini pe fiecare pagină, la coordonate alese de dumneavoastră. Nu există niciun limbaj de descriere a paginii de învățat și niciun driver de imprimantă în lanț. Apelați metode, biblioteca asamblează obiecte PDF, iar SaveAs serializează rezultatul

Ce nu primiți este un motor de aranjare. Asta contează destul cât să fie spus din start, pentru că modelează fiecare exemplu de mai jos. PDFium Component pune conținutul acolo unde îi spuneți, în coordonate absolute, și nicăieri altundeva. Nu va încadra un paragraf, nu va face textul să curgă peste o ruptură de pagină și nu va calcula un tabel din rânduri și coloane. Acelea sunt treaba dumneavoastră. Dacă ați venit așteptând ceva care reformatează proza așa cum face un procesor de text, calibrați-vă acum: acesta este un API de plasare precis, de nivel jos, mai apropiat de desenul pe o pânză decât de tehnoredactarea unui document. Pentru facturi generate, certificate, etichete și pagini de raport în care știți deja unde stă fiecare element, exact acea precizie o vreți

Minimul care produce un fișier

Trei apeluri stau între un TPdf gol și un PDF salvat: creați documentul, adăugați o pagină, scrieți-l pe disc. Tot restul este conținut pe care îl stratificați între ele

Diagramă a fluxului de creare a unui PDF în patru pași cu PDFium Component pentru Delphi, de la CreateDocument, prin AddPage și apelurile de conținut, până la SaveAs
CreateDocument începe un document gol în memorie, fiecare AddPage devine pagina curentă, iar SaveAs serializează pe disc obiectele PDF asamblate
uses
  Vcl.Graphics,   // pentru clBlack și TColor
  PDFium;         // aici trăiește TPdf

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // document gol în memorie
    Pdf.AddPage(0, 595, 842);           // A4 portret, în puncte
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serializează pe disc
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Un detaliu îi încurcă pe cei care au văzut fragmente mai vechi: nu atribuiți Pdf.Active := True după CreateDocument. Proprietatea Active raportează dacă există un handle de document, iar CreateDocument a creat deja unul, așa că proprietatea este True din clipa în care apelul returnează. Setarea ei din nou este, în cel mai bun caz, o operație fără efect și, în cel mai rău, derutantă pentru următorul cititor. Active își merită locul la ieșire: atribuirea lui False eliberează documentul de dedesubt înainte de Free, adică ordinea curată de demontare. Tratați CreateDocument și deschiderea unui fișier ca fiind reciproc exclusive. Biblioteca refuză să creeze un document nou pe un TPdf care are deja unul deschis, așa că reutilizarea înseamnă închiderea mai întâi a documentului curent

Coordonatele pornesc din stânga-jos

A doua pereche de argumente a lui AddText, ca la orice apel de plasare, este un punct din spațiul utilizator PDF. Originea stă în colțul din stânga-jos al paginii, X merge spre dreapta, iar Y merge în sus. O unitate este un punct, 1/72 dintr-un țol, așa că o pagină A4 are 595 pe 842 de unități, iar US Letter are 612 pe 792. Acel Y ascendent este singura cea mai frecventă sursă a confuziei „textul meu a ieșit din pagină”, pentru că la coordonatele de ecran și de imagine bitmap originea stă sus, iar Y crește în jos. Pe o pagină înaltă de 842 de puncte, un titlu aproape de partea de sus stă pe la Y 780, nu la Y 60. Când o secvență aterizează undeva neașteptat, înălțimea paginii minus Y-ul dumneavoastră este aproape întotdeauna numărul la care vă gândeați de fapt

Diagramă PDFium Component care pune în contrast spațiul utilizator PDF, cu originea în stânga-jos și Y crescând în sus, cu coordonatele de ecran, la care Y crește în jos din stânga-sus
Un titlu aflat aproape de partea de sus a unei pagini A4 are nevoie de Y în jur de 780 în spațiul utilizator PDF, în timp ce obiceiurile de ecran ar scrie Y 60 și ar duce textul lângă marginea de jos

AddPage primește o poziție de inserare ca prim argument, exprimată începând de la 1, cu 0 drept prescurtare comodă pentru „începutul documentului”. Transmiteți 0 sau 1 pentru prima pagină și pagina este inserată în față; transmiteți valoarea egală cu numărul de pagini la care adăugați pentru a insera la final. Pagina nou adăugată devine și pagina curentă, cea vizată de apelurile de desenare ulterioare, așa că nu există un pas separat de „selectează această pagină” după adăugare. Dacă adăugați mai multe pagini și mai târziu trebuie să desenați înapoi pe una anterioară, setați PageNumber pentru a muta cursorul; cât timp umpleți paginile în ordinea în care le creați, îl puteți lăsa în pace

Scrierea textului și regula de font care mușcă în tăcere

Semnătura lui AddText poartă tot ce îi trebuie unei singure secvențe: șirul, un nume de font, un corp în puncte, ancorele X și Y, apoi opțional culoarea, un octet alfa pentru transparență și un unghi de rotire în grade

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Titlul cu negru, opacitate implicită, fără rotire
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // O semnătură mai deschisă, cu 24 de puncte mai jos
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // O ștampilă de ciornă slabă, în diagonală peste pagină
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Octetul alfa merge de la $00 (invizibil) la $FF (opac), iar asta face din ștampila de ciornă un filigran, nu un bloc plin: $30 înseamnă cam nouăsprezece procente opacitate, destul cât să se citească prin el. Unghiul rotește secvența în sens antiorar în jurul ancorei ei, așa că 45 de grade dau ștampila clasică din colț în colț. Nimic din toate acestea nu are nevoie de o funcție separată de filigran. Un filigran este doar un apel AddText mare, semitransparent și rotit, iar desenarea lui înainte sau după corpul textului decide dacă stă în spatele sau deasupra conținutului

Fonturile merită o frază atentă, pentru că modul de eșec este tăcut. Când transmiteți un nume de font, PDFium Component cere sistemului de operare datele TrueType ale acelui font și le încorporează în document, motiv pentru care un fișier construit pe mașina dumneavoastră se randează identic pe una care nu a avut niciodată fontul instalat. Capcana este ce se întâmplă când numele nu se rezolvă: o greșeală de tastare sau o față care pur și simplu nu este prezentă pe mașina de compilare. Nu apare nicio excepție. Biblioteca revine la crearea unui obiect de text care poartă numele doar ca etichetă, fără nimic încorporat, și lasă vizualizatorul să substituie ce consideră el apropiat. Textul apare în testele dumneavoastră, pare plauzibil și își schimbă metricile sau glifele în clipa în care fișierul se deschide undeva cu alte fonturi instalate. Folosiți nume despre care știți că sunt prezente pe mașina generatoare, tratați lista de fonturi ca pe o dependență de implementare și deschideți o mostră într-un vizualizator pe un sistem curat înainte să vă încredeți în rezultat

Forme vectoriale: construiți un traseu, apoi confirmați-l

Liniile, dreptunghiurile și regiunile umplute trec printr-un traseu. Îl deschideți cu CreatePath, care fixează dintr-odată punctul de start și tot stilul: modul de umplere, culorile de umplere și de contur cu octeții lor alfa, grosimea conturului, capetele și îmbinările de linie. Apoi îl extindeți cu LineTo, BezierTo și ClosePath, iar în final AddPath confirmă traseul finit pe pagină. Pasul de confirmare este ușor de uitat și nu produce nimic dacă îl săriți

Diagramă a ciclului de viață al unui traseu vectorial în PDFium Component, în care CreatePath fixează punctul de start și stilul, LineTo și BezierTo trasează conturul, iar AddPath confirmă desenul
CreatePath fixează de la început punctul de start și fiecare stil, dar nimic nu apare pe pagină până când AddPath nu confirmă traseul finit
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // O linie orizontală subțire. Supraîncărcarea cu dreptunghi fixează direct o casetă:
  // X, Y, lățime, înălțime, apoi modul de umplere și culorile.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Supraîncărcarea cu punct: pornește din primul vârf, trage linii spre restul, închide.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nimic nu este desenat până nu rulează asta
end;

Două supraîncărcări acoperă cazurile obișnuite. Forma cu patru coordonate primește X, Y, lățime și înălțime și vă dă un dreptunghi aliniat la axe într-un singur apel, adică exact ce folosiți pentru a desena o linie de separare, o bordură de celulă sau un panou de fundal umplut. Forma cu două coordonate fixează doar un punct de start, iar restul conturului îl trasați singur cu LineTo și BezierTo. Modul de umplere controlează cum sunt pictate regiunile suprapuse: fmWinding (înfășurare nenulă) se potrivește majorității formelor pline, fmAlternate (par-impar) tratează decupajele și contururile autointersectate, iar fmNone lasă un traseu doar conturat, fără umplere, adică ce folosește linia de separare de mai sus

Tabelele sunt trasee și text, asamblate manual

Pentru că nu există o primitivă de tabel, un tabel este o buclă. Decideți deplasările X ale coloanelor și înălțimea rândului, scrieți fiecare celulă cu AddText și desenați liniile cu trasee dreptunghiulare. Aritmetica vă aparține, dar este simplă, iar odată scrisă se generalizează la orice grilă vă trebuie

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // deplasările coloanelor
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Rândul de antet
  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);

  // Linia de sub antet
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Rânduri de date, cu Y coborât la fiecare iterație
  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;

Observați cum Y coboară cu înălțimea rândului la fiecare trecere, din nou pentru că sus înseamnă pozitiv. Aici se vede și absența măsurării textului: nimic nu împiedică un nume lung de articol să dea peste coloana următoare, pentru că biblioteca nu știe cât de lat s-a randat șirul dumneavoastră. Pentru rezultate cu format fix, în care controlați datele, dimensionați coloanele generos și mergeți mai departe. Pentru conținut cu adevărat variabil, fie constrângeți intrările, fie măsurați singur lățimile glifelor înainte de a le plasa, iar acesta este punctul din care o bibliotecă dedicată de compoziție începe să își merite banii

Imagini și pagini multiple

Conținutul raster intră prin funcțiile ajutătoare de imagine. AddPicture primește un TPicture încărcat și îl plasează într-un punct, cu o lățime și o înălțime opționale pentru scalare; AddImage acceptă direct o cale de fișier sau un TBitmap, iar AddJpegImage transmite octeți JPEG fără un drum dus-întors printr-o imagine bitmap. Ca peste tot, coordonatele de plasare sunt colțul din stânga-jos al imaginii în spațiul utilizator, iar lățimea și înălțimea sunt dimensiunea pe pagină în puncte, nu dimensiunile în pixeli ale sursei

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);     // adaugă la final; pagina nouă devine curentă
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // subsol lângă marginea de jos
      // ... desenează aici corpul acestei pagini ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Un document cu mai multe pagini este tiparul de o pagină pus într-o buclă. Fiecare AddPage adaugă o pagină și o face curentă, așa că corpul și subsolul pe care le desenați în continuare aterizează pe pagina tocmai adăugată. Nu reatribuiți PageNumber în această buclă, pentru că adăugarea unei pagini a mutat deja cursorul acolo; aveți nevoie de PageNumber doar când vă întoarceți la o pagină în afara ordinii de creare. Apelați SaveAs o singură dată, la final, după ce ultima pagină este umplută. Dacă vă trebuie un profil de arhivare, nu un fișier obișnuit, același obiect document expune SaveAsPdfA și celelalte variante de conformitate, așa că alegerea standardului de ieșire este un alt apel de salvare, nu un alt drum de construcție

Unde se potrivește toate acestea

Formularea onestă este că API-ul de creație al PDFium Component este un strat fidel și subțire peste modelul de obiecte de pagină al lui PDFium: creare reală de documente, fonturi încorporate reale, conținut vectorial și raster real, serializate într-un fișier conform standardelor. Nu este și nu pretinde să fie un motor de documente cu reformatare. Linia de demarcație este aranjarea textului. Dacă rezultatul dumneavoastră vine din șabloane, facturi, certificate, etichete, tablouri de bord randate pe o grilă fixă, modelul cu coordonate absolute este direct și rapid, iar codul rămâne lizibil. Dacă rezultatul este proză de lungă respirație care trebuie să se încadreze și să se pagineze singură, veți reconstrui un motor de aranjare peste aceste apeluri, iar acela este instrumentul greșit pentru acea treabă. Să știți de care parte a acelei linii vă aflați înseamnă cea mai mare parte a deciziei

Metodele de creare descrise aici fac parte din PDFium Component pentru Delphi, care împerechează acest drum de creație cu funcțiile de randare și de extragere a textului pentru care PDFium este mai cunoscut