Większość kodu w Delphi operującego na PDF traktuje ten format jako kontener dla dwóch rzeczy: fragmentów tekstu i kilku umieszczonych map bitowych. Ten pogląd jest słuszny tylko do pewnego stopnia i pozostawia niewykorzystaną najbardziej zaawansowaną część formatu. Strona PDF to niezależne od rozdzielczości płótno 2D oparte na tym samym modelu obrazowania co PostScript. Pozwala na rysowanie linii, krzywych, obszarów wypełnionych, gradientów oraz powtarzających się wzorów, a wszystko to w postaci wektorów, które zachowują ostrość przy dowolnym powiększeniu i są drukowane w pełnej rozdzielczości urządzenia. Jeśli rysujesz logo, wykres, znak wodny lub obramowanie certyfikatu, ścieżka wektorowa jest prawie zawsze właściwym prymitywem, a do tego mniejszym i wyraźniejszym niż obraz rastrowy, po który zamiast tego sięga wiele programów.
W tym artykule przeanalizowano model wektorowy w postaci zdefiniowanej przez standard ISO 32000-1 i przedstawiono odpowiadające mu wywołania w PDFlibPas. Celem jest urzeczywistnienie specyfikacji, ponieważ API ściśle ją odzwierciedla, a zrozumienie jednego elementu uczy obsługi drugiego.
Strona to maszyna do obsługi ścieżek
Standard ISO 32000-1 §8.5 opisuje grafikę w dwóch fazach, które nigdy na siebie nie nachodzą. Najpierw konstruujesz ścieżkę, co jest czystą geometrią niedającą żadnego widocznego rezultatu. Następnie malujesz tę ścieżkę w ramach jednej operacji, która obrysowuje jej kontur, wypełnia jej wnętrze lub wykonuje obie te czynności. Podczas budowania ścieżki na stronie nic się nie pojawia. Ścieżka to abstrakcyjna sekwencja punktów i segmentów utrzymywana w stanie grafiki, dopóki operator rysowania jej nie skonsumuje, kiedy to jest renderowana i usuwana.
Ścieżka składa się z jednej lub więcej podścieżek. Podścieżka zaczyna się w punkcie i rośnie poprzez dołączanie segmentów: linii prostych, sześciennych krzywych Beziera, a na niektórych platformach całych prostokątów dodawanych jako ich własne zamknięte podścieżki. W PDFlibPas otwierasz ścieżkę za pomocą StartPath, co ustala punkt początkowy, a następnie rozszerzasz ją przy użyciu AddLineToPath i AddCurveToPath. Każde wywołanie przesuwa ukryty punkt bieżący, dzięki czemu kolejny segment zaczyna się w miejscu, w którym zakończył się poprzedni. Funkcja ClosePath rysuje końcowy prosty segment z powrotem do początku podścieżki, co ma znaczenie przy obrysowywaniu (stroking), ponieważ tworzy rzeczywiste łączenie linii w zamykającym wierzchołku zamiast dwóch luźnych zakończeń linii.
// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);
PDF.StartPath(150, 100); // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath; // straight segment back to (150, 100)
PDF.DrawPath(2); // 2 = fill and stroke; path is consumed
Krzywe korzystają z metody AddCurveToPath, która przyjmuje dwa punkty kontrolne Beziera oraz punkt końcowy: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Krzywa biegnie od punktu bieżącego do (EndX, EndY), będąc po drodze przyciąganą w stronę dwóch punktów kontrolnych. Łuki kołowe są dostępne poprzez AddArcToPath(CenterX, CenterY, TotalAngle), gdzie promień jest wyznaczany na podstawie odległości między punktem bieżącym a środkiem, a silnik generuje łuk jako łańcuch segmentów Beziera. Prostokąty mają skrót AddBoxToPath(Left, Top, Width, Height), który dołącza kompletny zamknięty prostokąt jako osobną podścieżkę bez wcześniejszego wywołania StartPath.
Dwie reguły wypełniania i dlaczego dają różne efekty
Gdy wypełniasz ścieżkę, która przecina samą siebie lub zawiera pętlę wewnętrzną, renderer potrzebuje reguły decydującej o tym, które obszary leżą wewnątrz kształtu, a które są otworami. Standard ISO 32000-1 §8.5.3.3 definiuje dwie reguły, które mogą różnie pomalować tę samą geometrię. Reguła nieparzystego uzwojenia (nonzero winding) zlicza przecięcia ze znakiem dla promienia rzuconego z punktu testowego do nieskończoności, dodając jeden dla każdego segmentu przecinającego promień od lewej do prawej i odejmując jeden dla przecinającego w drugą stronę; punkt leży wewnątrz, gdy suma jest różna od zera. Reguła parzysto-nieparzysta (even-odd) ignoruje kierunek i po prostu zlicza przecięcia, uznając, że punkt leży wewnątrz, gdy liczba ta jest nieparzysta.
Klasycznym przypadkiem, w którym reguły te się rozchodzą, jest kształt z otworem, taki jak pączek z dziurką czy podkładka. Narysuj granicę zewnętrzną i granicę wewnętrzną w jej środku. Zgodnie z regułą parzysto-nieparzystą wewnętrzna pętla zawsze wycina otwór, ponieważ każdy punkt między dwiema granicami jest przecinany raz, a każdy punkt wewnątrz pętli wewnętrznej dwukrotnie. Zgodnie z regułą nieparzystego uzwojenia otwór pojawi się tylko wtedy, gdy pętla wewnętrzna biegnie w przeciwnym kierunku do pętli zewnętrznej; jeśli biegną w tę samą stronę, uzwojenia wzmacniają się zamiast znosić, a obszar wewnętrzny zostanie całkowicie wypełniony. Pięcioramienna gwiazda narysowana jako pojedyncza przecinająca się linia wykazuje ten sam podział: reguła parzysto-nieparzysta pozostawia środkowy pięciokąt pusty, podczas gdy reguła nieparzystego uzwojenia go wypełnia.
PDFlibPas wybiera regułę na podstawie wywołania metody malowania, a nie za pomocą flagi. Funkcja DrawPath wypełnia zgodnie z regułą nieparzystego uzwojenia; DrawPathEvenOdd stosuje regułę parzysto-nieparzystą. Obie przyjmują ten sam tryb całkowity (integer mode): 0 tylko obrysowuje kontur, 1 tylko wypełnia, a 2 wypełnia i obrysowuje. Reguła parzysto-nieparzysta jest prostszym narzędziem do wycinania otworów właśnie dlatego, że nie wymaga zarządzania kierunkiem przebiegu podścieżek.
// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120); // outer
PDF.AddBoxToPath(140, 130, 120, 60); // inner
PDF.DrawPath(1); // 1 = fill, nonzero winding
// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120); // outer
PDF.AddBoxToPath(140, 330, 120, 60); // inner cut-out
PDF.DrawPathEvenOdd(1); // 1 = fill, even-odd
Gradienty osiowe zmieniają kolor wzdłuż linii
Jednolite wypełnienie kolorem ma jedną wartość w całym obszarze. Gradient zmienia kolor w sposób ciągły, a najprostszym jego rodzajem jest gradient osiowy (liniowy). Standard ISO 32000-1 §8.7.4.5 definiuje go jako cieniowanie osiowe typu 2 (Type 2 axial shading): podajesz dwa punkty określające oś, kolor początkowy w pierwszym punkcie oraz kolor końcowy w drugim, a renderer interpoluje kolor wzdłuż tej osi. Każdy punkt w wypełnionym obszarze przyjmuje kolor swojej prostopadłej projekcji na oś, co sprawia, że gradient układa się w pasy pod kątem prostym do linii łączącej oba punkty.
W PDFlibPas gradient to nazwany zasób dokumentu, który tworzysz raz, a następnie wybierasz jako aktywny materiał malarski. NewRGBAxialShader rejestruje go. Sygnatura to NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): dwa punkty końcowe osi, trójki RGB na każdym końcu jako wartości w zakresie od 0 do 1 oraz flaga Extend. Gdy flaga Extend ma wartość 1, kolory końcowe rozciągają się jako jednolite wypełnienie poza punkty końcowe osi, co jest zazwyczaj pożądane, aby narożniki obszaru poza osią nie pozostały niepomalowane; wartość 0 pozostawia je nietknięte. Gdy shader już istnieje, powiązujesz go za pomocą SetFillShader dla obszarów wypełnionych, SetLineShader dla obrysów lub SetTextShader dla tekstu. Powiązanie pozostaje aktywne dla kolejnych wywołań rysowania, więc następna malowana ścieżka przyjmie gradient zamiast jednolitego koloru.
// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
0, 100, 0.10, 0.25, 0.55, // start point and start RGB
0, 260, 1.00, 1.00, 1.00, // end point and end RGB
1); // 1 = extend ends as solid color
// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1); // 1 = fill, now filled by the shader
Oś jest tutaj pionowa, od y=100 do y=260 przy stałej wartości x, więc pasy koloru układają się poziomo, a prostokąt płynnie przechodzi od niebieskiego u podstawy do białego na górze. Ponieważ shader jest identyfikowany po nazwie, jedna definicja może wypełnić dowolną liczbę kształtów na stronie, a powrót do jednolitego koloru to po prostu kolejne wywołanie SetFillColor przed następną ścieżką.
Wzory kafelkowe powtarzają komórkę
Tam, gdzie gradient płynnie zmienia pojedynczy kolor, wzór kafelkowy (tiling pattern) powtarza mały element graficzny na całym obszarze. Standard ISO 32000-1 §8.7.3.1 definiuje go jako komórkę wzoru (pattern cell) — niezależny fragment zawartości, który renderer powiela na stałej siatce w celu pokrycia kafelkami malowanego obszaru. W ten sposób buduje się kreskowanie dla wypełnień inżynieryjnych, powtarzający się motyw marki za nagłówkiem lub teksturowane tło, które pozostaje wektorowo ostre i nie waży prawie nic bez względu na to, jak duży jest obszar, ponieważ komórka jest zapisywana tylko raz i wywoływana w każdym miejscu.
PDFlibPas buduje komórkę wzoru na podstawie przechwyconej zawartości strony. Przechwytujesz stronę lub obszar za pomocą CapturePage, zamieniasz przechwycony obraz w nazwany wzór za pomocą NewTilingPatternFromCapturedPage(PatternName, CaptureID), a następnie wybierasz ten wzór jako bieżące wypełnienie za pomocą SetFillTilingPattern(PatternName). Od tego momentu każda wypełniana ścieżka jest malowana powtarzającą się komórką zamiast jednolitego koloru — dokładnie tak, jak działa wypełnienie shaderem, ale ze skafelkowaną komórką jako źródłem farby. Ta sekwencja jest bardziej złożona niż jedno wywołanie, więc jeśli krok przechwytywania jest nowy, potraktuj wzór jako operację dwuetapową: najpierw wygeneruj przechwyconą komórkę, a następnie powiąż ją jako wypełnienie według nazwy przed narysowaniem obszaru, który ma zostać pokryty wzorem.
Łączenie prymitywów w całość
Te elementy łączą się bezpośrednio. Wypełniona plama Beziera to ścieżka z krzywych namalowana za pomocą DrawPath. Ten sam kontur namalowany za pomocą DrawPathEvenOdd po dodaniu wewnętrznej pętli pokazuje otwór, który wypełnienie regułą uzwojenia zamknęłoby. Prostokąt wypełniony gradientem to prostokąt powiązany z shaderem. Poniższy przykład rysuje wszystkie trzy elementy w kolejności, dzięki czemu różnica między dwiema regułami wypełniania jest widoczna na jednej stronie, a następnie umieszcza pod nimi panel z gradientem.
// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480); // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480); // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1); // 1 = fill
// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300); // new subpath: the hole
PDF.AddArcToPath(200, 300, 360); // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1); // hole is punched out
// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
60, 100, 0.95, 0.55, 0.10,
60, 200, 0.20, 0.10, 0.40,
1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);
Warto pamiętać o dwóch szczegółach. Wywołanie malowania decyduje o regule wypełniania, więc wybór między DrawPath a DrawPathEvenOdd to wybór między regułą nieparzystego uzwojenia a parzysto-nieparzystą. W przypadku kształtów z otworami reguła parzysto-nieparzysta zwalnia z konieczności analizowania kierunku podścieżek. Ponadto stan grafiki jest próbkowany w momencie malowania: ustaw kolory, szerokość linii i powiązanie shadera przed wywołaniem malowania, ponieważ to ten stan odczytuje silnik. Najpierw zbuduj geometrię, skonfiguruj stan, namaluj na końcu, a model wektorowy będzie zachowywał się przewidywalnie za każdym razem.
Stąd naturalnym kolejnym krokiem jest odczytywanie wektorów i tekstu z istniejącego dokumentu, co opisano w naszym artykule na temat ekstrakcji tekstu, obrazów i czcionek, a także renderowanie tego samego modelu rysunkowego do kontekstu urządzenia Windows w celu podglądu na ekranie i drukowania, co omówiono w przewodniku po drukowaniu i podglądzie. Wywołania ścieżek, shaderów i wzorów opisane tutaj są dostarczane jako część biblioteki PDF dla Delphi wraz z interfejsami API do obsługi tekstu, obrazów, formularzy i podpisów, które omówiono w innych częściach tego bloga.