Artykuł techniczny

Rysowanie na płótnie HotPDF w Delphi

Linia stopki skopiowana z przykładu strumienia zawartości PDF brzmi 72 72 m 540 72 l S: idź do (72, 72), poprowadź linię do (540, 72), obrysuj. W surowym PDF ta linia siedzi cal nad dolną krawędzią strony US Letter. Podaj te same cztery liczby do API rysującego, które mierzy od lewego górnego rogu, i linia ląduje cal pod górą — prosto przez nagłówek listu. Sama linia jest w porządku. Zmieniła się tylko rama odniesienia, a przejście między obiema ramami na stronie 792 punktów to jedno odejmowanie: 792 minus Y

Gdzie jest Y = 0 na stronie HotPDF?

Wywołania rysujące HotPDF kładą origin w lewym górnym rogu strony, z X rosnącym w prawo i Y rosnącym w dół — ta sama rama, której używa VCL-owe TCanvas. Przestrzeń użytkownika PDF, zdefiniowana w ISO 32000-1 §8.3.2.3, startuje w lewym dolnym rogu z Y rosnącym w górę, a HotPDF konwertuje każde Y, które podasz, w chwili zapisu strumienia zawartości. Więc Y := 72 na THotPDF.CurrentPage znaczy cal pod górną krawędzią — niezależnie od rozmiaru strony

Rozmiar strony ma znaczenie na drugim końcu. Na stronie US Letter, 612 na 792 punktów, Y := 720 to cal nad dnem. Nowa strona HotPDF to A4, chyba że powiesz inaczej, a A4 ma 842 punkty wysokości, więc to samo Y := 720 zostawia pod sobą 122 punkty, z grubsza 1.7 cala. Stopki pozycjonowane przez odejmowanie od 792 dryfują na A4 w górę dokładnie z tego powodu. Czytaj wysokość z CurrentPage.Height zamiast wbijać ją na sztywno albo ustaw CurrentPage.Size wprost tuż po BeginDoc

Porównanie VCL TCanvas i API rysowania stron HotPDF: oba mierzą od lewego górnego rogu z Y rosnącym w dół, podczas gdy HotPDF zapisuje ten sam punkt blisko góry strony US Letter jako y = 720 w rosnącej od dołu przestrzeni użytkownika PDF
HotPDF rysuje w tej samej lewo-górnej ramie co TCanvas, więc punkt cal pod górą strony 612 na 792 to Y = 72 w obu, a HotPDF zapisuje go do pliku jako y = 720 przestrzeni użytkownika PDF

Jednostką jest punkt, 1/72 cala, bo THotPDF.Resolution domyślnie ma 72. Podniesienie Resolution sprawia, że współrzędne i rozmiary liczą się w 1/Resolution cala. Dla roboty wektorowej zostaw 72: grubości linii, długości kresek i współrzędne czytają się wtedy w tej samej jednostce, którą plik PDF trzyma, i wartość widziana w strumieniu zawartości zgadza się z wartością w źródle

Tekst podąża za tym samym modelem. Domyślnie Y podane do TextOut to górna krawędź tekstu, więc etykieta i pudełko wokół niej są planowane na jednej siatce. Kilka wywołań pracuje na pliku PDF zamiast na powierzchni rysowania i używa rosnących od dołu współrzędnych PDF: ustawiacze pudełek stron, jak SetCropBox(Left, Bottom, Right, Top), i rodzina StampLoadedPageText / StampLoadedPageImage, która stempluje już wczytany dokument. Mieszanie ich z wywołaniami ścieżek to źródło większości odwróconych układów — trzymaj je w osobnych procedurach

Budowanie ścieżki i malowanie jej

Ścieżka jest najpierw budowana, potem malowana. MoveTo ustawia punkt startowy, nie zaznaczając niczego, każde LineTo dodaje prosty odcinek, i nic nie dociera do strony, dopóki wywołanie malujące, jak Stroke, nie użyje bieżącego koloru obrysu i grubości linii. W terminach PDF Stroke emituje operator S, a każdy operator malujący kończy bieżącą ścieżkę, więc następne MoveTo startuje na czysto

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := Letter;    // 612 x 792 pt; domyślnie A4

    // Grubość i kolor to stan strony: ustaw raz, używane przez każde późniejsze Stroke
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // Linia cal pod górą, marginesy 72 pt z obu stron
    Pdf.CurrentPage.MoveTo(72, 72);
    Pdf.CurrentPage.LineTo(540, 72);
    Pdf.CurrentPage.Stroke;

    // Linia stopki z otwarcia: cal nad dnem
    Pdf.CurrentPage.MoveTo(72, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.LineTo(540, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.Stroke;

    // Trzy połączone odcinki w jednej ścieżce, grubsze i niebieskie
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 152);
    Pdf.CurrentPage.LineTo(172, 102);
    Pdf.CurrentPage.LineTo(272, 172);
    Pdf.CurrentPage.LineTo(372, 112);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Grubość linii to stan, nie argument. SetLineWidth zapisuje operator w i każde późniejsze obrysowanie na stronie używa tej grubości, aż ją zmienisz — dlatego polilinia wychodzi 3 punkty, a obie linie zostają przy 1.5. Jeśli nigdy jej nie ustawisz, obowiązuje domyślna wartość PDF 1.0 (ISO 32000-1 §8.4.3.2). Brakujące kształty mają prozaicniejszą przyczynę: geometria położona bez wywołania malującego nigdy się nie renderuje

Współrzędne i grubości są Single, więc 72.25 i włosowa linia 0.5 punkta przetrwają do pliku. Zero to przypadek szczególny. Specyfikacja definiuje grubość 0 jako najcieńszą linię, jaką potrafi narysować urządzenie wyjściowe: jeden piksel na ekranie przy każdym zoomie i 1/600 cala na drukarce 600 dpi, przez co ta sama linia wygląda mocno w przeglądarce i niemal znika na papierze. Wybierz prawdziwą grubość, jak 0.25 albo 0.5, dla linii włosowych, a linia zachowa swoją wagę na każdym urządzeniu

Model ścieżki HotPDF: MoveTo, LineTo, Rectangle i Circle budują niewidoczną bieżącą ścieżkę w pamięci, i tylko malujący commit Stroke, Fill albo FillAndStroke maluje ją, używając trwałego stanu grafiki — koloru obrysu, koloru wypełnienia i grubości linii — po czym czyści bufor
Geometria narasta po cichu w bieżącej ścieżce, dopóki operator malujący nie zatwierdzi jej z zapamiętanym kolorem obrysu, kolorem wypełnienia i grubością linii — zapomniane wywołanie malujące zostawia kształt niewidocznym

Dlaczego zamknięty trójkąt ma wcięcie w jednym rogu?

Trójkąt narysowany trzema LineTo wracającymi do punktu startowego wygląda na zamknięty, ale w PDF to wciąż otwarta podścieżka. ISO 32000-1 §8.5.3.2 stosuje łączenie linii tylko tam, gdzie odcinki spotykają się wewnątrz podścieżki; dwa wolne końce dostają zamiast tego nasadki. Przy domyślnej nasadce butt i piórze 6 punktów róg startowy pokazuje kwadratowe ugryzienie tam, gdzie dwa pozostałe rogi mają czyste sfazowania. ClosePath (operator h) dokłada ostatni odcinek i każe rendererowi połączyć go z pierwszym

Pdf.CurrentPage.SetLineWidth(6);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

// Wygląda zamknięty, nie jest: nasadki butt spotykają się w (72, 500) i zostawiają wcięcie
Pdf.CurrentPage.MoveTo(72, 500);
Pdf.CurrentPage.LineTo(132, 420);
Pdf.CurrentPage.LineTo(192, 500);
Pdf.CurrentPage.LineTo(72, 500);
Pdf.CurrentPage.Stroke;

// Zamknięta podścieżka: wszystkie trzy rogi dostają to samo łączenie miter
Pdf.CurrentPage.MoveTo(232, 500);
Pdf.CurrentPage.LineTo(292, 420);
Pdf.CurrentPage.LineTo(352, 500);
Pdf.CurrentPage.ClosePath;
Pdf.CurrentPage.Stroke;              // albo ClosePathStroke w jednym wywołaniu

// Fill nie potrzebuje ClosePath: wypełnianie zamyka każdą podścieżkę implicite
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.MoveTo(392, 500);
Pdf.CurrentPage.LineTo(452, 420);
Pdf.CurrentPage.LineTo(512, 500);
Pdf.CurrentPage.Fill;

Zamykaj każdy wielobok, który zamierzasz obrysować, przez ClosePath albo ClosePathStroke, nawet gdy ostatni punkt już pokrywa się z pierwszym. Przy 1 punkcie wcięcie jest niewidoczne na ekranie; przy 6 punktach wychodzi przy zwykłym zoomie, a w drukowanej legendzie wykresu czyta się jako glitch renderowania. Dla kształtów wypełnianych pytanie nie powstaje, a EoFill jest na wypadek, gdy ścieżka przecina samą siebie, a wolisz regułę even-odd zamiast nonzero winding

Prostokąty, okręgi i nawyk TCanvas

Zamknięte prymitywy oszczędzają księgowość MoveTo. Rectangle(X, Y, Width, Height) bierze lewy górny róg i rozmiar, Circle(X, Y, Radius) bierze środek i promień, a każdy z nich jest malowany przez Fill, Stroke albo FillAndStroke. Kolor wypełnienia i kolor obrysu są niezależne: SetRGBFillColor i SetRGBStrokeColor biorą każde po TColor, więc stałe Delphi i RGB(...) działają wprost, a SetRGBColor ustawia oba naraz

Kształt argumentów Rectangle to pułapka. TCanvas.Rectangle bierze (Left, Top, Right, Bottom), i lata kodu malującego VCL trenują palce, by podać drugi róg. Podaj Rectangle(72, 202, 232, 292) do HotPDF, a dostaniesz pudełko 232 punkty szerokie i 292 wysokie zamiast 160 na 90. Gdy liczby, które masz, naprawdę są dwoma rogami, wołaj MFRectangle(X, Y, X1, Y1), które bierze współrzędne rogów i wylicza rozmiar za ciebie; Rectangle zostaw kodowi myślącemu rozmiarami

Plik PDF nie ma operatora okręgu. Circle zapisuje cztery sześcienne segmenty Beziera ze zwyczajowym współczynnikiem punktów kontrolnych 0.5523 — zobaczysz to, dekompresując strumień zawartości strony: jedno m, po którym idą cztery operatory c. Ellipse(X, Y, Width, Height) robi to samo wewnątrz pudełka ograniczającego, którego lewym górnym rogiem jest (X, Y)

HotPDF wystawia też THotPDF.Canvas — standardowy TCanvas dla bieżącej strony, a program CanvasDraw z demo Graphic zapełnia stronę Ledger zwykłymi wywołaniami Canvas.Rectangle, Ellipse i RoundRect. Ta droga ma sens przy portowaniu istniejącego kodu malującego VCL. Nowy kod rysujący lepiej wypada na CurrentPage, bo każde wywołanie mapuje się na jeden operator PDF w punktach, a wyjście jest przewidywalne do liczby

Callout z czterech commitów

Callout używa wszystkiego powyżej naraz: panelu wypełnionego i obrysowanego, linii wskaźnika, kotwiącej kropki i dwóch linii tekstu ułożonych wewnątrz panelu na tych samych lewo-górnych współrzędnych. FillAndStroke (operator B) maluje wnętrze i obramowanie jednej ścieżki w jednym wywołaniu, więc prostokąt jest budowany raz, a nie dwa

Anatomia calloutu złożonego z prymitywów HotPDF: prostokąt zatwierdzony przez FillAndStroke, obrysowana linia wskaźnika z MoveTo i LineTo, wypełniona kropka kotwiąca i belki etykiet TextOut dzielące tę samą lewo-górną siatkę co kształty
Cztery commity budują callout — FillAndStroke maluje panel i obramowanie razem, wskaźnik i kropka reużywają stanu obrysu i wypełnienia, a każdy offset etykiety to goła arytmetyka od rogu pudełka (90, 122)
procedure DrawCallout(Page: THPDFPage);
begin
  // 1. Panel: blada wypełniacz i ciemniejsze obramowanie, jedna ścieżka, jeden commit
  Page.SetRGBFillColor(RGB(255, 244, 214));
  Page.SetRGBStrokeColor(RGB(180, 130, 40));
  Page.SetLineWidth(1);
  Page.Rectangle(90, 122, 240, 70);      // lewy górny (90, 122), 240 x 70
  Page.FillAndStroke;

  // 2. Wskaźnik od lewej krawędzi panelu w dół do celu
  Page.SetLineWidth(1.5);
  Page.MoveTo(90, 177);
  Page.LineTo(66, 246);
  Page.Stroke;

  // 3. Kropka kotwicąca na końcu wskaźnika, kolorem obramowania
  Page.SetRGBFillColor(RGB(180, 130, 40));
  Page.Circle(64, 250, 3);
  Page.Fill;

  // 4. Etykiety: każde Y to góra tekstu, mierzona na tej samej siatce
  Page.SetFont('Arial', [fsBold], 10);
  Page.TextOut(102, 134, 0, 'Check this total');
  Page.SetFont('Arial', [], 9);
  Page.TextOut(102, 152, 0, 'Rounded to two decimals per line,');
  Page.TextOut(102, 164, 0, 'then summed');
end;

// Wołane między BeginDoc a EndDoc:
//   DrawCallout(Pdf.CurrentPage);

Każdy blok ustawia własny kolor wypełnienia, kolor obrysu i grubość tuż przed kształtem, który ich potrzebuje, nawet gdy poprzedni blok zostawił w miejscu tę samą wartość. Kosztuje to kilka nadmiarowych operatorów w pliku. W zamian każdy blok może się przenieść, zniknąć albo stać własną procedurą, bez dziedziczenia bezdomnego czerwonego wypełnienia po tym, co biegło przed nim — i dla kodu rysującego, który zmienia się razem z raportami, ten handel opłaca się za każdym razem

Kolory zasługują na tę samą dyscyplinę. Zasiewanie wypełnień przez Random($FFFFFF) czyni stronę demo żywą, a generowany raport niemożliwym do porównania między uruchomieniami, bo dwa buildy z identycznych danych dają różne bajty. Prowadź kolor z danych albo ze stałej tablicy palety, jak robi demo CanvasDraw swoją ośmiopolową tablicą CanvasColors, a to samo wejście da ten sam plik

Kreski, krzywe i przezroczystość

SetDash to stan strony jak grubość linii. SetDash([3, 3], 0) sprawia, że każde późniejsze obrysowanie ma trzy punkty kreski i trzy przerwy, drugi argument przesuwa, gdzie wzorzec startuje, a NoDash wraca do pełnego pióra. Tablica kresek to array of Byte, więc długości to pełne punkty od 1 do 255, zapisywane do strumienia zawartości dokładnie tak, jak podane. Kreska ustawiona dla jednej grupy linii siatki zostaje na każdą linię po niej, więc owiń kreskowany fragment w GStateSave i GStateRestore: one emitują q i Q, a przywrócenie zapisanego stanu przynosi z powrotem poprzednią kreskę, grubość i kolory w jednym kroku

Krzywe przedłużają tę samą ścieżkę. CurveToC(X1, Y1, X2, Y2, X3, Y3) dodaje sześcienny Bezier od bieżącego punktu do (X3, Y3) z dwoma punktami kontrolnymi, a CurveToV i CurveToY pokrywają przypadki, gdy pierwszy albo drugi punkt kontrolny pokrywa się z końcem. Jedna ścieżka może mieszać odcinki LineTo i CurveToC przed jednym Stroke albo Fill — tak rysuje się zaokrąglone rogi i wygładzone linie wykresów

Przezroczystość mieszka w nazwanym stanie grafiki, bo PDF trzyma alfę w słowniku ExtGState, a nie w kolorze. THotPDF.RegisterExtGState(FillAlpha, StrokeAlpha) przyjmuje wartości od 0 do 1 i zwraca nazwę; CurrentPage.SetGraphicsState(Name) stosuje ją do wszystkiego malowanego potem. Rejestruj każde krycie raz na dokument i reużyj nazwy, więc plik niesie jeden słownik na krycie, choćby używało go sto kształtów. Alfa wymaga PDF 1.4, a HotPDF podnosi wersję wyjścia do 1.4 w razie potrzeby; przy ustawionym StrictVersionLock zostawia wersję w spokoju i zwraca pustą nazwę. Gdy PDFACompliance wybiera profil PDF/A-1, alfa poniżej 1 podnosi wyjątek, bo ISO 19005-1 §6.4 zakazuje tam przezroczystości. Profile PDF/A-2 i PDF/A-3 ją przyjmują

var
  Half: AnsiString;
begin
  // Rejestruj raz na dokument, reużyj zwróconej nazwy na dowolnej stronie
  Half := Pdf.RegisterExtGState(0.5, 1.0);   // wypełnienie 50%, obrys kryjący

  Pdf.CurrentPage.GStateSave;                // q
  Pdf.CurrentPage.SetDash([3, 3], 0);
  Pdf.CurrentPage.SetLineWidth(0.5);
  Pdf.CurrentPage.SetRGBStrokeColor(clGray);
  Pdf.CurrentPage.MoveTo(72, 400);
  Pdf.CurrentPage.LineTo(540, 400);
  Pdf.CurrentPage.Stroke;

  Pdf.CurrentPage.SetGraphicsState(Half);
  Pdf.CurrentPage.SetRGBFillColor(clYellow);
  Pdf.CurrentPage.Rectangle(72, 380, 200, 16);  // podświetlenie nad wierszem tekstu
  Pdf.CurrentPage.Fill;
  Pdf.CurrentPage.GStateRestore;             // Q: pełne pióro, pełne krycie z powrotem
end;

Wywołania wektorowe pasują do geometrii, którą liczy twój program: linie tabel, osie i słupki wykresów, callouty, logo złożone z tuzina ścieżek. Zdjęcie albo screenshot należy do AddImage i ShowImage; obrysowywanie pikseli ścieżkami tylko powiększa plik. Sprawdzając wyjście, zzoomuj do 1600% na rogach grubych obrysów i na każdej linii kreskowanej — wcięcia z niedomkniętych ścieżek i kreski przeciekłe poza swój fragment są przy tym powiększeniu oczywiste, a przy 100% łatwo je przegapić

Każde wywołanie z tego artykułu mieszka na obiekcie strony HotPDF Delphi Component, który biegnie w projektach VCL Delphi i C++Builder i zapisuje wynik jako gołe operatory ścieżek PDF