Strona PDF nie przechowuje pikseli ani drzewa obiektów kształtów w taki sposób, w jaki robi to SVG. Przechowuje program. Każda linia, krzywa, wypełnienie i umieszczony obraz na stronie jest wynikiem wykonywania sekwencji operatorów w strumieniu zawartości, od góry do dołu, na bieżącym stanie graficznym. Zrozumienie tego jednego faktu sprawia, że większość zachowań formatu przestaje być zaskakująca: dlaczego wypełnienie potrzebuje osobnego operatora rysowania po zbudowaniu ścieżki, dlaczego kolory i grubości linii przenikają z jednego kształtu na drugi, chyba że zamkniesz je w nawiasach, dlaczego ten sam kod rysowania może wylądować w zupełnie różnych miejscach po pojedynczym przekształceniu współrzędnych. To przegląd tego modelu wykonania zdefiniowanego w normie ISO 32000: operatorów, które spotykasz po otwarciu strumienia zawartości, oraz reguł decydujących o tym, co pojawia się na stronie
Strumień zawartości to kod bajtowy z notacją postfiksową
Strumień zawartości to płaska sekwencja bajtów składająca się z operandów, po których następują operatory. Operandy występują na początku, operator, który z nich korzysta, na końcu, co stanowi odwrotność wywołania funkcji i jest identyczne z maszyną stosową: odłóż liczby, a następnie wydaj instrukcję. Nie ma tu zagnieżdżania, składni wyrażeń ani zmiennych. Obrys trójkąta to pięć takich linii:
100 100 m % moveto: start a new subpath at (100, 100)
200 200 l % lineto: add a segment to (200, 200)
300 100 l % lineto: add a segment to (300, 100)
h % closepath: connect back to the start
S % stroke: paint the path outline
Operatory są celowo zwięzłe. Rzeczywista strona zawiera ich tysiące, zazwyczaj skompresowanych przy użyciu FlateDecode. Ceną tej zwartości jest to, że strumień nie zawiera struktury, o którą można zapytać: przeglądarka nie może zapytać „gdzie na tej stronie jest nagłówek”, może tylko uruchomić program i zobaczyć, gdzie wyląduje atrament. To jest główny powód, dla którego ekstrakcja tekstu z dowolnych plików PDF jest trudna
Początek układu współrzędnych to lewy dolny róg, a oś Y rośnie w górę
Zanim jakakolwiek współrzędna nabierze sensu, musisz wiedzieć, gdzie jest (0, 0). PDF umieszcza początek układu współrzędnych w lewym dolnym rogu strony, gdzie oś X rośnie w prawo, a oś Y w górę, mierzony w punktach przy 72 punktach na cal (ISO 32000-2 §8.3.2). Na stronie formatu US Letter górna krawędź znajduje się na wysokości y = 792, a nie na y = 0. Każdy, kto przychodzi z dziedziny grafiki ekranowej, gdzie początek układu znajduje się w lewym górnym rogu, a oś Y rośnie w dół, za pierwszym razem robi to odwrotnie i rysuje pierwszą linię poza dolną krawędzią strony. Jednostka jest również niezależna od nośnika: 72 jednostki to jeden cal, bez względu na to, czy strona jest renderowana na ekranie telefonu, czy na naświetlarce
Większość bibliotek rysowania stron bezpośrednio dziedziczy tę konwencję. Przykładowo w HotPDF TextOut i wywołania ścieżek mierzą z lewego dolnego rogu w punktach, więc wartość bliska wysokości strony umieszcza zawartość na górze:
// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700); // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke; // emits the moveto/lineto/stroke operators
Ta sekwencja wywołań kompiluje się dokładnie do powyższych operatorów m, l i S. Biblioteka jest tylko osobą wpisującą dane do strumienia zawartości i niczym więcej, a wiedza o tym, co emituje, pozwala ci wnioskować o wyniku, gdy kształt wyląduje tam, gdzie się nie spodziewasz
Zbuduj ścieżkę, a następnie ją narysuj
PDF oddziela budowanie ścieżki od rysowania ścieżki, a ten podział nie jest czepialstwem. Najpierw opisujesz kształt operatorami budowania, które nie dodają niczego widocznego, a następnie wydajesz jeden operator rysowania, który decyduje, co zrobić ze zgromadzoną ścieżką. Ten sam trójkąt może być zarysem, jednolitym wypełnieniem lub obydwoma naraz, zależąc tylko od czasownika, na którym zakończysz
Operatorów budowania jest niewiele. m rozpoczyna nową podścieżkę w punkcie. l dodaje prosty segment. c dodaje sześcienną krzywą Beziera z sześciu operandów, dwóch punktów kontrolnych i punktu końcowego. re to skrót, który dodaje cały prostokąt z czwórki: x, y, szerokość, wysokość. h zamyka bieżącą podścieżkę i wraca do jej początku. Żaden z nich nie zostawia atramentu na stronie; one tylko gromadzą geometrię
200 250 m % start the subpath
300 350 400 450 500 250 c % cubic Bezier: two control points, then endpoint
150 200 re % a 150 x 200 rectangle, added as its own subpath
h % close
Pierwotny przykład używał przestarzałego obecnie wariantu y operatora krzywej; c z trzema jawnymi punktami to forma, którą spotkasz w praktyce i po którą należy sięgać. Po utworzeniu ścieżki jeden operator rysowania kończy sprawę. Słownictwo jest małe i warto je zapamiętać, ponieważ każdy kształt na każdej stronie kończy się jednym z nich:
Sobrysowuje zarys ścieżki, używając bieżącej szerokości linii i koloru obrysufwypełnia wnętrze przy użyciu bieżącego koloru wypełnienia i reguły „nonzero winding” (niezerowego nakładania)f*wypełnia za pomocą reguły parzystości-nieparzystości, co ma znaczenie w przypadku kształtów samoprzecinających się i kształtów z dziuramiBwypełnia, a następnie obrysowuje w jednej operacji;bnajpierw zamyka ścieżkęnnic nie maluje, w ten sposób ścieżka staje się regionem przycinania (clip region) bez pozostawiania widocznego śladu
Reguła krzywych (winding rule) to część, którą ludzie często błędnie interpretują. Reguła niezerowego nakładania (f, B) zlicza przecięcia ze znakiem promienia od punktu testowego i wypełnia wszędzie tam, gdzie wynik nie wynosi zero, więc otwór pozostaje pusty tylko wtedy, gdy jego podścieżka owija się w stronę przeciwną do zewnętrznej. Reguła parzystości-nieparzystości (f*, B*) przełącza się przy każdym przekroczeniu, niezależnie od kierunku. Jeśli kształt „pączka” okaże się wypełniony na stałe, to jego wewnętrzny okrąg jest zakreślony w ten sam sposób co zewnętrzny, i musisz to odwrócić albo zmienić regułę na parzystość-nieparzystość
Kolor jest trybem, a nie parametrem
Kolor w strumieniu zawartości utrzymuje się. Ustawiasz kolor i pozostaje on ustawiony do momentu ustawienia innego lub przywrócenia wcześniejszego stanu, dlatego też zmiana koloru nieujęta w nawiasy po cichu zabarwia wszystko, co zostanie narysowane po niej. PDF traktuje również kolor wypełnienia i kolor obrysu jako dwa niezależne ustawienia, przy czym małe litery operatorów oznaczają wypełnienie, a wielkie litery - obrys. Poszczególne przestrzenie kolorów urządzenia mają własne skróty:
0.5 g % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg % DeviceRGB fill
0.8 0.2 0.1 RG % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k % DeviceCMYK fill
Przestrzeń DeviceRGB jest odpowiednia do wyjścia ekranowego, przestrzeń DeviceCMYK jest tym, czego oczekuje produkcja drukarska, a przestrzeń DeviceGray jest najskromniejszym wyborem dla treści monochromatycznych. Przestrzenie urządzenia są wygodne, ale nieskalibrowane: ta sama wartość RGB może być różnie renderowana na dwóch monitorach, a jest to problem, który przestrzenie barwne oparte na profilach ICC i intencje wyjściowe PDF/A mają za zadanie rozwiązać. W przypadku prac o krytycznym znaczeniu pod względem kolorystyki należy wybrać skalibrowaną przestrzeń za pomocą cs i CS oraz ustawić komponenty za pomocą sc i scn, ale w przypadku zwykłych dokumentów skróty dotyczące urządzenia przejmują obciążenie. Biblioteka otacza to w wywołania oparte o typ. Przykładowo HotPDF pobiera jeden obiekt TColor i emituje pasujące operatory:
Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150); // x, y, width, height
Pdf.CurrentPage.Fill;
Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50); // x, y, radius
Pdf.CurrentPage.Fill;
Stan graficzny i stos q/Q
Wszystko, co nie jest samą ścieżką, znajduje się w stanie graficznym: macierz bieżącego przekształcenia, kolory wypełnienia i obrysu, grubość linii, wzór kreskowania (dash pattern), obszar przycinania (clip region), kanał alfa. Stan jest globalny i modyfikowalny, więc jedynym bezpiecznym sposobem na dokonanie lokalnej zmiany jest zapisanie całości, zmodyfikowanie jej, narysowanie i przywrócenie zmian. Tym właśnie zajmują się q i Q. Znak q przesuwa kopię bieżącego stanu na stos; Q ją zdejmuje, odrzucając każdą zmianę dokonaną od odpowiadającego q
q % save the entire graphics state
2 0 0 2 100 100 cm % concatenate a transform: scale 2x, translate to (100,100)
0.8 g % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q % restore: transform and color revert
Niezbilansowane q i Q to najczęstsza przyczyna, z powodu której strumień zawartości budowany ręcznie lub poszyty psuje się. Przypadkowe q bez pasującego Q po zakończeniu strony opuszcza głęboko stos; a dodatkowe Q doprowadza do jego niedomiaru. Niezależnie od przyczyny, przeglądarka może zachować stare przycięcie (clip) lub wymusić modyfikację, w wyniku czego zawartość znika lub ląduje w niewłaściwym miejscu. Gdy grafika znika z powodu, którego ścieżka nie potrafi wyjaśnić, w pierwszej kolejności zbadaj stos stanu
Macierz CTM przekształca każdą współrzędną
Macierz bieżącego przekształcenia (CTM) znajduje się między wartościami w Twoich operatorach a faktyczną stroną. Zanim cokolwiek zostanie narysowane, każda współrzędna mnożona jest przez macierz CTM, co powoduje, że zmiana macierzy modyfikuje miejsce i sposób wyświetlania wszystkich kolejnych rysunków bez zmiany jakiejkolwiek współrzędnej ścieżki. Operator cm łączy nową macierz z obecną i przyjmuje przy tym sześć operandów, rzutowanych na macierz afiniczną [a b c d e f]:
1 0 0 1 100 50 cm % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm % rotate 45 degrees (cos/sin in a, b, c, d)
Są dwie rzeczy, o które potykają się ludzie. Po pierwsze, cm układa, a nie podmienia, zatem modyfikacje kumulują się, a kolejność odgrywa rolę: skalowanie, po którym następuje przesunięcie, nie jest tym samym, co przesunięcie ze skalowaniem. Po drugie, skalowanie i rotacja skupiają się wokół współczesnego początku układu, a nie środka Twojego kształtu, zatem w celu obrotu elementu na swoim miejscu przenosisz go do początku, obracasz, po czym przesuwasz w dół, a to wszystko umieszczasz w operatorach q/Q. Dokładnie ta sama macierz określa lokalizację zdjęć, najnowszej z rzeczy polecanych do obejrzenia
Obrazy i zawartość wielokrotnego użytku to obiekty XObject
Obrazy rastrowe nie istnieją bezpośrednio w strumieniu zawartości. Zapisywane są one jako obiekty XObject obrazu, czyli obiekty zewnętrzne zaopatrzone we własny słownik wskazujący na na szerokość, wysokość, głębię bitową, przestrzeń kolorów oraz filtr ucisku, z kolei zawartość potoku tylko powołuje się na nie. Fotografia w wariancie JPEG określa się zatem we wskazany sposób:
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % the image data is a JPEG stream
>>
Obiekt XObject obrazu rysowany jest w jednostkowym kwadracie: zajmuje zawsze obszar od (0, 0) do (1, 1) w przestrzeni użytkownika. Nie przekazujesz mu pozycji ani rozmiaru. Zamiast tego ustawiasz macierz CTM tak, aby kwadrat jednostkowy mapował się na wybrany prostokąt, a następnie wywołujesz go za pomocą operatora Do. Dlatego właśnie umieszczanie obrazu jest zawsze przekształceniem, po którym następuje wywołanie, otoczone operacjami zapisu/przywracania (save/restore), tak aby zmiana skali nie przeniknęła do kolejnej operacji:
q
640 0 0 480 50 300 cm % map the unit square to a 640x480 box at (50, 300)
/Photo Do % paint the image XObject
Q
Ten sam mechanizm Do napędza obiekty XObject formularzy (form XObjects), które zawierają fragment grafiki wielokrotnego użytku, logo lub powtarzający się stempel, jako własny strumień zawartości z ramką ograniczającą (bounding box). Zdefiniuj go raz, wywołaj wielokrotnie za pomocą innej macierzy CTM, a bajty pojawią się w pliku tylko raz. Większość bibliotek ukrywa to za pojedynczym wywołaniem pozycjonowania: HotPDF rejestruje bitmapę za pomocą AddImage i umieszcza ją za pomocą ShowImage, pobierając jednoznaczne parametry x, y, szerokość i wysokość, zamiast wymagać ręcznego budowania macierzy:
var
Bmp: TBitmap;
ImgIndex: Integer;
begin
Bmp := TBitmap.Create;
try
Bmp.LoadFromFile('logo.bmp');
ImgIndex := Pdf.AddImage(Bmp, icFlate);
// x, y (bottom-left), width, height, rotation angle
Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
finally
Bmp.Free;
end;
end;
Pod tym jednym wierszem biblioteka zapisuje słownik obiektu XObject obrazu, ustawia macierz CTM w celu dobrania rozmiaru i pozycjonowania kwadratu jednostkowego oraz emituje operator Do. Model kryjący się pod spodem warto znać, ponieważ wyjaśnia on każdy dziwny rezultat: rozciągnięty obraz to macierz CTM z niedopasowanymi współczynnikami skali, identyczne logo na czterdziestu stronach to jeden obiekt XObject formularza wywołany czterdzieści razy, a obraz odwrócony do góry nogami to zamieniony znak w macierzy, a nie uszkodzony plik
Dokąd to prowadzi
Model graficzny staje się niewielki, gdy tylko poznasz jego zarys. Strumień zawartości to kod bajtowy z notacją postfiksową uruchamiany z modyfikowalnym stanem; współrzędne mają swój początek w lewym dolnym rogu i przechodzą przez macierz CTM; ścieżki są budowane po cichu i rysowane za pomocą jednego zamierzonego operatora; ustawienia kolorów i linii utrzymują się, dopóki nie zostaną zamknięte w nawiasach operatorów q/Q; obrazy i grafiki wielokrotnego użytku to obiekty XObject umieszczane poprzez przekształcenie kwadratu jednostkowego. Niemal każdy mylący wynik renderowania sprowadza się do jednej z tych pięciu reguł. Jeśli chcesz zobaczyć, w jaki sposób wspomniane operatory graficzne umiejscowione są w szerszym modelu obiektowym, słownikach stron i tabelach odnośników do nich, techniczny przegląd struktury pliku PDF pokrywa tę warstwę, a z kolei instrukcja z tworzenia prostego dokumentu PDF od podstaw pokazuje strukturę bajtów od początku do końca. Rysowanie tekstu należy do jego własnej rodziny operatorów i ma własne pułapki omówione w dołączonym artykule dotyczącym obsługi tekstu i czcionek w PDF
Przedstawione tutaj wywołania rysowania biblioteki Delphi, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage, oraz ShowImage stanowią część Komponentu HotPDF dla Delphi oraz C++Builder, emitującego przedstawione operatory z zakresu strumieni zawartości specjalnie dla Ciebie