Cairo jest najbardziej znaną biblioteką do tworzenia grafiki wektorowej. Co więcej, nie sprawia absolutnie żadnych problemów w komponowaniu grafiki wektorowej i rastrowej. Wyniki działania programów korzystających z libcairo można prezentować bezpośrednio na ekranie, w okienku w ramach aplikacji, można też zapisywać bezpośrednio do formatów wspierających wektorowy zapis grafiki, takich jak svg, Postscript czy PDF. W końcu – można wynik pracy programu zrasteryzować albo do pamięci, albo wprost do pliku obrazka – png.
Te potężne narzędzie oprócz interfejsu w C ma również interfejs dla Pythona – pycairo.
We wpisie omówię jak działa libcairo i do czego można tę bibliotekę wykorzystać.
1. Raster vs Wektor
To tytułem wprowadzenia, wielu z Was pewnie wie na czym polegają różnice, więc pewnie paluszek od razu powędruje do scrollowania. A jednak co nieco postanowiłem o tym napisać.
Obrazy rastrowe to w skrócie tablice pikseli. Do tego znamy szerokość i wysokość, więc możemy te tablice rozpisać w dwóch wymiarach. Piksel to oczywiście kwadrat o danym kolorze, opisanym dzisiaj najczęściej przez 24 bity, po 8 na każdą z wartości R, G, B. Jak to ujął recenzent na mojej obronie, obraz rastrowy to po prostu macierz. Koniec, kropka.
Raster:
Obrazek 11×8 pikseli, przypisując bieli 1 a czerni 0, dałoby się zapisać jako:
Szerokość: 11
Wysokość: 8
Piksele: 1101111101111101110111110000000111001000100100000000000010000000100101111101011100100111
Wektor:
Zapiszmy to następująco:
Obrazek 300×200
Szerokość: 300
Wysokość: 200
Wierzchołki (x,y): (50,25),(250,25),(50, 175), (250, 175)
Kolor konturu: czerwony
Kolor wypełnienia: niebieski
Grubość konturu: 4
Innymi słowy, obraz wektorowy jest zbiorem informacji, które określają współrzędne, kształty, sposoby kreślenia i wypełnienia obiektów. Obraz wektorowy, w odróżnieniu od rastrowego, jest zapisem parametrycznym. Główną korzyścią z takiego zapisu jest niezależność obrazu od rzeczywistych rozmiarów. Skalowanie w górę i w dół rozmiarów obrazu czy poszczególnych obiektów nie niesie za sobą żadnej utraty informacji związanej z interpolacją, jak to ma miejsce w przypadku obrazów rastrowych, czyli bitmap.
Dodatkowo, w przypadku niezbyt skomplikowanych obrazów wektorowych, ich rozmiar w pamięci będzie na ogół znacznie mniejszy niż obrazów rastrowych, ponieważ zapisowi podlegają jedynie parametry, a nie poszczególne piksele. W przypadku klatki wideo Full HD (1920×1080) bitmapa 24 bitowa będzie miała rozmiar przekraczający 6MB.
I tak w przypadku czarno-białego obrazka zamieszczonego wyżej (space invader) o wymiarach 739×542 mamy, dla kolejnych formatów rozmiary:
- spaceinvader.svg (wektorowy) – 6,5kB
- spaceinvader.bmp (rastrowy) – 1,6MB (!)
- spaceinvader.pdf (wektorowy) – 1,5kB (!!)
- spaceinvader.png (rastrowy z kompresją) – 5,1kB
Na korzyść PNG przemawia w tym przypadku wyłącznie mała ilość kolorów (3) i duże obszary o tym samym kolorze. Kompresja PNG świetnie sobie z tym radzi. Spory jednak rozmiar SVG spowodowany jest tym, że SVG jest zapisem tekstowym, w XML-u. Ponieważ PDF jest zapisany binarnie, jego rozmiar jest wielokrotnie mniejszy (najmniejszy).
2. Zastosowania libcairo
Na stronie Cairo przeczytamy, że biblioteka jest używana do rysowania grafiki 2D w projektach takich jak Firefox 3.0 (renderowanie HTML do grafiki), Inkscape (open source’owy odpowiednik Adobe Illustrator czy Corel Draw), GTK+ (framework GUI), Gnuplot.
Często wykorzystywana jest funkcjonalność rysowania do plików SVG i PDF.
Ja osobiście używałem libcairo do renderowania prezentacji a’la powerpoint w jednym z prowadzonych przeze mnie projektów. Zdarzyło mi się również korzystać z dość kompaktowego silnika do renderowania html – litehtml – który całe renderowanie stron (grafiki, tekstu) opierał na libcairo. W jednym z projektów z obszaru Digital Signage użyłem libcairo do wizualizacji i animacji „analogowego”, wskazówkowego zegara na podstawie czasu systemowego. Jeden z moich kolegów używał libcairo do wizualizacji i animacji wirtualnego wskazówkowego miernika prądu.
Libcairo może być z powodzeniem używane do generowania w pełni wektorowych wydruków w PDF – zawierających tekst, grafikę, czy elementy takie jak kody kreskowe i kody QR.
3. Rysowanie
Będąc jeszcze w szkole średniej zetknąłem się z językiem programowania Logo. Była to aplikacja składająca się z okna reprezentującego „płótno” malarskie i z okna, w którym wprowadzało się komendy dla „żółwia”. W wielu implementacjach faktycznie na oknie do rysowania pojawia się żółw, któremu wydaje się proste komendy, gdzie ma się przesunąć i czy ma za sobą pozostawić ślad w postaci linii. Można tam było formułować funkcje (lub jak kto woli procedury), więc używając rekurencji rysowałem sobie w Logo proste fraktale, z reguły przypominające płatki śniegu czy struktury przypominające kalafiora.
Zatem – cairo to trochę takie Logo, tyle, że dla zaawansowanych.
Najpierw białe tło:
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); /* clear to white */ cairo_paint(cr);
cr jest tutaj „kontekstem” libcairo, na którym wywołujemy funkcje rysujące. Skąd się bierze ten kontekst – o tym później. cairo_set_source_rgb ustala tutaj to, czym będziemy malować, czyli w tym przypadku kolor biały. Wartości RGB zawierają się w przedziale 0-1 i są ułamkowe. To zapewne zwróci Waszą uwagę, że większość parametrów w libcairo to są floaty, ma to swój głęboki sens, o czym w dalszej części wpisu.
cairo_new_path(cr); cairo_move_to(cr, 100.0, 150.0); cairo_line_to(cr, 200.0, 150.0); cairo_line_to(cr, 150.0, 63.397459622); cairo_close_path(cr); cairo_set_source_rgb(cr, 0, 0, 1.0); /* blue stroke */ cairo_set_line_width(cr, 1.5); cairo_stroke(cr);
cairo_new_path usuwa z kontekstu cr starą ścieżkę. Ścieżka to podstawowy element określający kontur i kształt. Wywołując rozmaite funkcje rysujące tworzymy ścieżkę, na której potem możemy wykonywać operacje.
cairo_move_to przesuwa wirtualny „pisak” na wskazane współrzędne. cairo_line_to jest już funkcją tworzącą ścieżkę, czyli z punktu (100,150) prowadzimy linię do (200, 150).
Trzeba tu zaznaczyć, że jeszcze nie odbywa się właściwe rysowanie. Sama ścieżka jest na razie „niewidoczna”, dopiero po jej zakończeniu decydujemy, jak ją zwizualizować.
W końcu, wywołując kolejne line_to docieramy do trzeciego wierzchołka – jest to trójkąt równoboczny. Figurę zamykamy wywołując cairo_close_path, co powoduje powrót do punktu (100, 150). Teraz czas na właściwe rysowanie.
cairo_set_source_rgb(cr, 0, 0, 1.0) to wybór koloru, tutaj jest to niebieski.
cairo_set_line_width(cr, 1.5) to wybór grubości linii, wyrażony jak widać ułamkowo.
cairo_stroke(cr) to rysowanie konturu, gdyby wywołać zamiast tego cairo_fill(cr) wówczas zamiast konturu pokolorowane zostałoby wnętrze.
Teraz pora na okrąg.
cairo_new_path(cr); cairo_arc(cr, 100, 300.0, 80, 0, 2* M_PI); cairo_set_source_rgb(cr, 1.0, 0, 0); /* red stroke */ cairo_set_line_width(cr, 10.0); /* thick line */ cairo_stroke_preserve(cr); /* preserve path (circle) for later filling */ cairo_set_source_rgb(cr, 0, 1.0, 0); /* green fill */ cairo_fill(cr);
cairo_arc służy do tworzenia ścieżek w postaci łuków. Szczególnym przypadkiem łuku jest oczywiście okrąg. Jeśli chodzi o figury geometryczne, to cairo oferuje jedynie możliwość rysowania prostokątów i łuków-okręgów. Inne kształty użytkownik musi komponować z odcinków i krzywych.
cairo_stroke_preserve różni się od wspomnianego wyżej cairo_stroke tym, że po odrysowaniu konturu nie kasuje aktualnej ścieżki. Zamierzam bowiem, oprócz odrysowania konturu wypełnić także wnętrze okręgu.
cairo_fill wypełnia okrąg na zielono.
Cairo ma również dość rozbudowany interfejs do renderowania tekstu, umożliwiający wykorzystanie m.in. biblioteki FreeType. Cairo jednak nie zapewnia żadnych funkcji do łamania tekstu i jego rozkładu na ekranie, więc wyliczanie pozycji poszczególnych znaków, łamania linii, justowanie jest już zadaniem programisty, chyba, że skorzysta z biblioteki wyższego poziomu, np. Pango, służącej do łamania i rozkładu tekstu.
cairo_new_path(cr); cairo_font_face_t* ff = cairo_toy_font_face_create ("Sunshiney", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_face(cr, ff); cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); cairo_set_font_size (cr, 48.0); cairo_move_to(cr, 250, 100); cairo_show_text (cr, "Hello"); cairo_font_face_destroy(ff);
cairo_toy_font_face_create jest „szybkim” interfejsem do wczytywania czcionek, przeznaczonym raczej do celów testowych. Wczytaną czcionkę (Sunshiney z biblioteki Google Fonts) ustawiam jako aktualną używając cairo_set_font_face(cr, ff). Tekst będzie czarny, poprzez cairo_set_font_size ustawiam rozmiar na 48 punktów. cairo_show_text jest również „szybką” funkcją do renderowania tekstu, nie mamy tu wpływu absolutnie na sposób ułożenia tekstu, chociaż zaobserwowałem, że to API przynajmniej stosuje kerning, co ma niezwykle korzystny wpływ na wygląd tekstu.
Nie samą grafiką wektorową grafik żyje, czasem trzeba wpleść jakiś obrazek rastrowy, na przykład zdjęcie. Cairo umożliwia łączenie grafiki rastrowej z wektorową. Można bezpośrednio wklejać obrazki, można ich używać jako „tekstur” do wypełniania kształtów czy rysowania konturów. Do kompozycji postanowiłem wkleić znane zdjęcie Leny Sjööblom:
Zdjęcie ma 256×256 pikseli, więc przykryłoby większość sceny, zatem może by je zmniejszyć?
cairo_new_path(cr); cairo_save(cr); cairo_surface_t* lena = cairo_image_surface_create_from_png ("lena.png"); cairo_translate(cr, 250, 200); cairo_scale(cr, 0.5, 0.5); cairo_rotate(cr, 30.0 * 2.0 * M_PI / 360.0); cairo_set_source_surface(cr, lena, 0, 0); cairo_paint(cr); cairo_surface_destroy(lena); cairo_restore(cr);
Warto najpierw zwrócić uwagę na klamrę w postaci poleceń cairo_save(cr) i cairo_restore(cr). Jest to dość przydatna funkcja. Cairo bowiem zapamiętuje kolejne operacje, takie jak ustawienie koloru, grubości pędzla itp. cairo_save ustawia taki punkt powrotu, tak, że po wielu operacjach na kontekście możemy przywrócić jego stan sprzed ostatniego cairo_save() wołając cairo_restore(). Co najciekawsze, te wywołania można zagnieżdżać, dlatego określiłem to jako klamrę, bo para cairo_save/restore zachowuje się jak para klamer w programie, tylko dotyczy ustawień kontekstu cairo_t. Ta para funkcji jest najberdziej przydatna wtedy, gdy nasz program podzielony jest na funkcje operujące na jednym kontekście.
cairo_image_surface_create_from_png tworzy powierzchnię (surface_t), innymi słowy płótno malarskie, które w tym przypadku jest rozmiaru wczytanego obrazka lena.png i zawiera ten obrazek. Płótno może zawierać obraz rastrowy, ale przede wszystkim może zawierać obraz wektorowy. W cairo rysuje się zawsze do płótn, można też rysować z jednej surface do innej surface, jak w tym przykładzie.
Zestaw wywołań translate, scale, rotate umożliwia manipulowanie układem odniesienia, dzięki czemu możemy przemieścić punkt (0,0) z lewego górnego rogu płótna, do dowolnego punktu (translate), następnie zmienić skalę na wirtualnych osiach układu (scale), w końcu obrócić osie (rotate). W grafice wektorowej współrzędne mają dość umowny charakter, podczas gdy w grafice rastrowej mają charakter absolutny, tzn. pod współrzędną całkowitą (x,y) zawsze znajduje się jeden piksel.
Z tego powodu współrzędne w cairo wyrażone są w postaci liczb rzeczywistych, jako zmiennoprzecinkowy float. Cairo w zasadzie abstrahuje od konkretnych współrzędnych jako jednostek odległości, sam tutorial na stronie Cairo operuje na współrzędnych z przedziału 0 – 1. Taki obrazek można potem zeskalować do dowolnego wymiaru za pomocą cairo_scale.
cairo_set_source_surface wybiera płótno, z którego będziemy rysować, czyli obrazek z Leną. cairo_paint maluje cały obszar obrazkiem z Leną, uwzględniając aktualną pozycję środka ukłądu współrzędnych, kierunek osi i skalę. Obszar malowany można ograniczyć ustawiając region przycinania (clip region), który może być dowolną zamkniętą ścieżką. Tutaj po prostu kopiujemy zmniejszoną, przesuniętą i obróconą Lenę.
Płótna (surface_t)
Zanim powstanie kontekst cairo_t* cr najpierw musimy określić na czym będziemy rysować. Trzeba za tem powołać płótno (surface_t). Płótno może być wektorowe lub rastrowe. Mamy m.in dostępne płótna svg i pdf, czyli możemy rysować wprost do wektorowych formatów. Mamy formaty rastrowe w postaci obrazków lub okna aplikacji.
Najpierw przedstawię jak wygląda funkcja rysująca, której użyłem do narysowania kompozycji powyżej:
void draw(cairo_surface_t * surf) { cairo_t* cr = cairo_create(surf); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); /* clear to white */ cairo_paint(cr); cairo_new_path(cr); (...) cairo_surface_destroy(lena); cairo_restore(cr); cairo_destroy(cr); }
Widać, że kontekst rysunku (cr) tworzymy na dostarczonym wcześniej płótnie. Rysując wcale nie musimy wiedzieć, do czego rysujemy, czy pod spodem mamy ekran komputera, plik pdf czy obszar w pamięci.
Ja przetestowałem akurat trzy rodzaje płótn, które w dokumentacji czasem nazwane są backendami. Są to image (png), pdf i svg:
int main() { cairo_surface_t* surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, 400, 400); draw(surf); cairo_surface_write_to_png (surf, "drawings/drawing.png"); cairo_surface_destroy(surf); surf = cairo_svg_surface_create ("drawings/drawing.svg", 400, 400); draw(surf); cairo_surface_destroy(surf); surf = cairo_pdf_surface_create ("drawings/drawing.pdf", 400, 400); draw(surf); cairo_surface_destroy(surf); }
Na każdym z płótn wywołana jest ta sama funkcja draw(). A oto efekty:
Oczywiście plik svg możemy sobie potem dowolnie edytować używając aplikacji do edycji grafiki wektorowej, np. Inkscape, Corel, Adobe.