Artykuł techniczny

Tworzenie plików PDF od zera w Delphi z PDFium Component

PDFium ma opinię silnika przeglądarki, renderera stojącego za kartą PDF w Chrome, więc pierwszą rzeczą do wyjaśnienia jest to, że PDFium Component potrafi też zbudować dokument, który wcześniej nie istniał. Strona autorska opakowuje API obiektów strony PDFium: robisz pusty dokument, dodajesz strony o jawnych wymiarach i upuszczasz tekst, ścieżki wektorowe oraz obrazy na każdą stronę we współrzędnych, które sam wybierasz. Nie ma tu języka opisu strony do nauczenia się ani sterownika drukarki w pętli. Wywołujesz metody, biblioteka składa obiekty PDF, a SaveAs serializuje wynik

Czego nie dostajesz, to silnika układu. Ma to na tyle duże znaczenie, że warto powiedzieć to od razu, bo kształtuje każdy przykład poniżej. PDFium Component umieszcza treść tam, gdzie mu każesz, we współrzędnych bezwzględnych, i nigdzie indziej. Nie zawinie akapitu, nie przeleje tekstu przez podział strony ani nie policzy tabeli z wierszy i kolumn. To twoja robota. Jeśli przyszedłeś tu, oczekując czegoś, co przelewa prozę tak jak edytor tekstu, skalibruj się teraz: to precyzyjne, niskopoziomowe API rozmieszczania, bliższe rysowaniu po płótnie niż składaniu dokumentu. Dla generowanych faktur, certyfikatów, etykiet i stron raportów, gdzie już wiesz, gdzie należy każdy element, ta precyzja jest dokładnie tym, czego chcesz

Minimum, które produkuje plik

Trzy wywołania dzielą pusty TPdf od zapisanego PDF-a: utwórz dokument, dodaj stronę, wypisz go. Cała reszta to treść, którą nawarstwiasz pomiędzy nimi

Diagram czterokrokowego przepływu tworzenia PDF w PDFium Component dla Delphi, od CreateDocument przez AddPage i wywołania treści aż po SaveAs
CreateDocument zaczyna pusty dokument w pamięci, każde AddPage staje się bieżącą stroną, a SaveAs serializuje złożone obiekty PDF na dysk
uses
  Vcl.Graphics,   // dla clBlack i TColor
  PDFium;         // tutaj mieszka TPdf

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // pusty dokument w pamięci
    Pdf.AddPage(0, 595, 842);           // A4 pionowo, w punktach
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serializuj na dysk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Jeden szczegół potyka ludzi, którzy widzieli starsze fragmenty kodu: nie przypisujesz Pdf.Active := True po CreateDocument. Właściwość Active raportuje, czy istnieje uchwyt dokumentu, a CreateDocument już go utworzyło, więc właściwość jest prawdziwa w chwili powrotu z tego wywołania. Ustawianie jej ponownie jest w najlepszym razie bezczynne, a w najgorszym mylące dla następnego czytelnika. Active zarabia na siebie przy wyjściu: przypisanie fałszu zwalnia dokument leżący pod spodem przed Free, i to jest czysta kolejność rozbiórki. Traktuj CreateDocument i otwarcie pliku jako wzajemnie wykluczające się. Biblioteka odmawia utworzenia nowego dokumentu na TPdf, który już ma otwarty dokument, więc ponowne użycie oznacza najpierw zamknięcie bieżącego

Współrzędne zaczynają się w lewym dolnym rogu

Druga para argumentów AddText, i każdego wywołania rozmieszczającego, to punkt w przestrzeni użytkownika PDF. Początek układu siedzi w lewym dolnym rogu strony, X biegnie w prawo, a Y biegnie w górę. Jedna jednostka to jeden punkt, 1/72 cala, więc strona A4 ma 595 na 842 jednostki, a US Letter 612 na 792. To rosnące w górę Y jest najczęstszym pojedynczym źródłem zamieszania w stylu „mój tekst wyszedł poza stronę”, bo współrzędne ekranu i bitmapy umieszczają początek na górze, z Y rosnącym w dół. Na stronie o wysokości 842 punktów nagłówek przy górnej krawędzi siedzi około Y 780, a nie Y 60. Gdy fragment ląduje w nieoczekiwanym miejscu, wysokość strony minus twoje Y to prawie zawsze liczba, o którą naprawdę ci chodziło

Diagram PDFium Component zestawiający przestrzeń użytkownika PDF, której początek siedzi w lewym dolnym rogu z Y rosnącym w górę, ze współrzędnymi ekranu, w których Y rośnie w dół od lewego górnego rogu
Nagłówek przy górnej krawędzi strony A4 potrzebuje Y około 780 w przestrzeni użytkownika PDF, podczas gdy nawyki ekranowe kazałyby wpisać Y 60 i wylądować z tekstem przy dole

AddPage przyjmuje jako pierwszy argument pozycję wstawienia, wyrażoną z liczeniem od jedynki, gdzie 0 jest wygodnym skrótem oznaczającym „początek dokumentu”. Podaj 0 albo 1 dla pierwszej strony, a strona zostanie wstawiona z przodu; podaj wartość odpowiadającą liczbie stron, do której dokładasz, żeby dodać na końcu. Nowo dodana strona staje się też stroną bieżącą, tą, w którą celują kolejne wywołania rysujące, więc nie ma osobnego kroku „wybierz tę stronę” po jej dodaniu. Jeśli dodasz kilka stron, a później musisz narysować coś z powrotem na wcześniejszej, ustaw PageNumber, żeby przesunąć kursor; dopóki wypełniasz strony po kolei w miarę ich tworzenia, możesz go nie ruszać

Pisanie tekstu i reguła czcionek, która gryzie po cichu

Sygnatura AddText niesie wszystko, czego potrzebuje pojedynczy fragment: ciąg, nazwę czcionki, rozmiar w punktach, kotwicę X i Y, a potem opcjonalny kolor, bajt alfa dla przezroczystości i kąt obrotu w stopniach

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Tytuł na czarno, domyślna krycie, bez obrotu
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // Jaśniejszy podpis autora 24 punkty poniżej
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // Blada ukośna pieczęć wersji roboczej w poprzek strony
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Bajt alfa biegnie od $00 (niewidoczny) do $FF (nieprzezroczysty), i to właśnie czyni pieczęć wersji roboczej znakiem wodnym, a nie litym blokiem: $30 to mniej więcej dziewiętnaście procent krycia, tyle, by dało się przez nie czytać. Kąt obraca fragment przeciwnie do ruchu wskazówek zegara wokół jego kotwicy, więc 45 stopni daje klasyczną pieczęć od rogu do rogu. Nic z tego nie wymaga osobnej funkcji znaku wodnego. Znak wodny to po prostu duże, półprzezroczyste, obrócone wywołanie AddText, a narysowanie go przed treścią albo po niej decyduje, czy siedzi za nią, czy na wierzchu

Czcionki zasługują na uważne zdanie, bo tryb awarii jest cichy. Gdy podajesz nazwę czcionki, PDFium Component prosi system operacyjny o dane TrueType tej czcionki i osadza je w dokumencie, i dlatego plik zbudowany na twojej maszynie renderuje się identycznie na takiej, która nigdy nie miała zainstalowanej tej czcionki. Haczyk tkwi w tym, co się dzieje, gdy nazwa się nie rozwiązuje: literówka albo krój, którego po prostu nie ma na maszynie budującej. Nie ma żadnego wyjątku. Biblioteka cofa się do utworzenia obiektu tekstowego, który niesie nazwę wyłącznie jako etykietę, bez niczego osadzonego, i zostawia przeglądarce podstawienie tego, co uzna za zbliżone. Tekst pojawia się w twoich testach, wygląda wiarygodnie i przesuwa metryki albo glify w chwili, gdy plik otworzy się gdzieś z innym zestawem czcionek. Używaj nazw, o których wiesz, że są obecne na maszynie generującej, traktuj listę czcionek jako zależność wdrożeniową i otwórz próbkę w przeglądarce na czystym systemie, zanim zaufasz wyjściu

Kształty wektorowe: zbuduj ścieżkę, potem ją zatwierdź

Linie, prostokąty i wypełnione obszary idą przez ścieżkę. Otwierasz ją przez CreatePath, które ustawia naraz punkt początkowy i całą stylistykę: tryb wypełnienia, kolory wypełnienia i obrysu z własnymi bajtami alfa, szerokość obrysu, zakończenia i łączenia linii. Potem rozszerzasz ją przez LineTo, BezierTo i ClosePath, a na końcu AddPath zatwierdza gotową ścieżkę na stronie. Krok zatwierdzenia łatwo pominąć, a jego pominięcie nie produkuje niczego

Diagram cyklu życia ścieżki wektorowej w PDFium Component, gdzie CreatePath ustawia punkt początkowy i stylistykę, LineTo i BezierTo kreślą obrys, a AddPath zatwierdza rysunek
CreatePath ustala z góry punkt początkowy i każdy styl, ale na stronie nie pojawia się nic, dopóki AddPath nie zatwierdzi gotowej ścieżki
procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // Cienka linia pozioma. Przeciążenie prostokątne ustawia ramkę wprost:
  // X, Y, szerokość, wysokość, potem tryb wypełnienia i kolory.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Przeciążenie punktowe: zacznij w pierwszym wierzchołku, poprowadź linie, zamknij.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nic nie jest rysowane, dopóki to nie zadziała
end;

Dwa przeciążenia pokrywają typowe przypadki. Postać czterowspółrzędna przyjmuje X, Y, szerokość i wysokość i daje ci jednym wywołaniem prostokąt wyrównany do osi, po który sięgasz, by narysować linię, obramowanie komórki albo wypełniony panel tła. Postać dwuwspółrzędna ustawia wyłącznie punkt początkowy, a resztę obrysu kreślisz sam przez LineTo i BezierTo. Tryb wypełnienia steruje tym, jak malowane są nakładające się obszary: fmWinding (niezerowy indeks nawinięcia) pasuje do większości litych kształtów, fmAlternate (parzysty-nieparzysty) radzi sobie z wycięciami i obrysami przecinającymi samych siebie, a fmNone zostawia ścieżkę wyłącznie obrysowaną, bez wypełnienia, i tego właśnie używa powyższa linia rozdzielająca

Tabele to ścieżki i tekst, składane ręcznie

Ponieważ nie ma prymitywu tabeli, tabela jest pętlą. Decydujesz o przesunięciach X kolumn i o wysokości wiersza, wypisujesz każdą komórkę przez AddText i rysujesz linie prostokątnymi ścieżkami. Arytmetyka należy do ciebie, ale jest prosta, a raz napisana uogólnia się na dowolną potrzebną siatkę

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // przesunięcia kolumn
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Wiersz nagłówka
  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 pod nagłówkiem
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Wiersze danych, z Y schodzącym w dół w każdej iteracji
  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;

Zwróć uwagę na Y schodzące w dół o wysokość wiersza przy każdym przebiegu, znów dlatego, że w górę jest dodatnie. Tu też widać brak pomiaru tekstu: nic nie powstrzymuje długiej nazwy pozycji przed wjechaniem w następną kolumnę, bo biblioteka nie wie, jak szeroko wyrenderował się twój ciąg. Dla wyjścia o stałym formacie, gdzie kontrolujesz dane, wymiarujesz kolumny z zapasem i jedziesz dalej. Dla treści naprawdę zmiennej albo ograniczasz wejścia, albo sam mierzysz szerokości glifów przed ich rozmieszczeniem, a to moment, w którym dedykowana biblioteka składu zaczyna zarabiać na siebie

Obrazy i wiele stron

Treść rastrowa wchodzi przez pomocnicze funkcje obrazów. AddPicture przyjmuje wczytany TPicture i umieszcza go w punkcie, z opcjonalną szerokością i wysokością do przeskalowania; AddImage przyjmuje ścieżkę pliku albo TBitmap bezpośrednio, a AddJpegImage strumieniuje bajty JPEG bez objazdu przez bitmapę. Jak we wszystkim innym, współrzędne rozmieszczenia to lewy dolny róg obrazu w przestrzeni użytkownika, a szerokość i wysokość to rozmiar na stronie w punktach, a nie wymiary źródła w pikselach

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);     // dołącz; nowa strona staje się bieżącą
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // stopka przy dolnej krawędzi
      // ... narysuj tutaj treść tej strony ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Dokument wielostronicowy to wzorzec jednostronicowy w pętli. Każde AddPage dołącza stronę i czyni ją bieżącą, więc treść i stopka, które rysujesz następnie, lądują na właśnie dodanej stronie. Nie przypisujesz ponownie PageNumber wewnątrz tej pętli, bo dodanie strony już przesunęło tam kursor; PageNumber potrzebujesz tylko wtedy, gdy wracasz do strony poza kolejnością tworzenia. Wywołaj SaveAs raz na końcu, po wypełnieniu ostatniej strony. Jeśli potrzebujesz profilu archiwalnego zamiast zwykłego pliku, ten sam obiekt dokumentu udostępnia SaveAsPdfA i pozostałe warianty zgodności, więc wybór standardu wyjścia to inne wywołanie zapisu, a nie inna ścieżka budowania

Gdzie to pasuje

Uczciwe ujęcie jest takie, że autorskie API PDFium Component to wierna, cienka warstwa nad modelem obiektów strony PDFium: prawdziwe tworzenie dokumentu, prawdziwe osadzone czcionki, prawdziwa treść wektorowa i rastrowa, serializowane do pliku zgodnego ze standardami. Nie jest to i nie udaje silnika dokumentów z przelewaniem tekstu. Linią podziału jest układ tekstu. Jeśli twoje wyjście jest szablonowe: faktury, certyfikaty, etykiety, panele renderowane do stałej siatki, model współrzędnych bezwzględnych jest bezpośredni i szybki, a kod pozostaje czytelny. Jeśli twoje wyjście to długa proza, która musi sama się zawijać i paginować, będziesz odbudowywać silnik układu na tych wywołaniach, a to niewłaściwe narzędzie do tej roboty. Wiedza o tym, po której stronie tej linii jesteś, to większość decyzji

Opisane tutaj metody tworzenia są częścią PDFium Component dla Delphi, który łączy tę ścieżkę autorską z renderowaniem i wydobywaniem tekstu, z których PDFium jest lepiej znany