Kurs Arduino – Bitmapowe fonty GFX
Wcześniej zajmowaliśmy się wyświetlaniem grafiki oraz biblioteką glcdfont.c. Nie jest to jedyna możliwość wyświetlania tekstów. Są inne, lepsze.
Zgodnie z zapowiedzią, w tym odcinku omówimy ulepszone…
Bitmapowe fonty GFX
Wiemy już, że biblioteka Adafruit GFX zawiera też katalog /Fonts z licznymi fontami, które są znacznie ładniejsze niż podstawowy font 5×7, w którym każdy znak określony jest przez matrycę (bitmapę) 5×8, czyli składa się tylko z 40 punktów.
Trzeba jednak podkreślić, że ładniejsze fonty można stworzyć tylko wtedy, gdy znak składa się z większej liczby pikseli, niż zawiera matryca 5×7 (czym więcej, tym lepiej).
Koniecznie trzeba też dodać, że w „prawdziwych komputerach” wykorzystujemy fonty wektorowe, których definicje nie są bitmapami, ale matematycznymi przepisami, równaniami określającymi kształt znaków. Wprawdzie znaki fontów wektorowych finalnie wyświetlane są na ekranie także jako bitmapy, jednak bitmapy te są wyświetlane na samym końcu procesu. Stan poszczególnych pikseli takiej finalnej bitmapy jest precyzyjnie wyliczany matematycznie z użyciem określonego wzoru – przepisu. Pozwala to dowolnie skalować wielkość znaków na ekranie. My w systemie Arduino wykorzystujemy proste fonty bitmapowe, gdzie dostępne jest tylko skalowanie bardzo uproszczone – możliwe jest jedynie powiększenie: 2-krotne, 3-krotne, itd. W poprzednim odcinku dokładniej omawialiśmy, jak taki powiększony znak jest rysowany.
Fotografia 7 pokazuje przykład skalowania litery ’a’ podstawowego fontu 5×7 ze szkicu A2402.ino). Gdy znak jest większy, jego kształt pozostaje tak samo brzydki, jak fontu o wielkości podstawowej. Ponadto kolejne litery „zjeżdżają w dół” po dwa piksele.
Właśnie dlatego, że dostępne jest jedynie skalowanie uproszczone (powiększanie z mnożnikiem całkowitym), aby uzyskać ładny wygląd znaków, trzeba zrezygnować ze skalowania i przygotować oddzielne definicje znaków o różnych wielkościach. I właśnie dlatego w katalogu /Fonts znajdziemy tak dużo plików, zawierających definicje fontów o różnym kroju i o różnej wielkości, których nazwa zaczyna się od Free…. Mamy trzy podstawowe kroje: Mono, podobny do popularnego fontu CourierNew, Sans – podobny do fontu Arial oraz Serif – podobny do TimesNewRoman. Określenie Mono pochodzi od monospaced i wskazuje, że szerokość każdego znaku jest jednakowa (tak też jest w omawianym wcześniej foncie podstawowym 5×7). Co ważne, w fontach Sens i Serif szerokość poszczególnych znaków nie jest jednakowa: przykładowo literka i jest w nich znacznie cieńsza niż literki w czy m. Takie zróżnicowanie szerokości znaków poprawia wygląd tekstu, ale też tworzy istotne kłopoty, które trzeba jakoś rozwiązać.
Nazwa Serif wskazuje, że chodzi o tak zwany font szeryfowy, w którym literki mają ozdobne zakończenia tworzących je kresek (szeryfy). Sans to skrót od Sans Serif – po francusku bez szeryfów, czyli pismo bezszeryfowe, z prostymi zakończeniami linii, bez ozdobników.
W każdym z tych trzech podstawowych krojów mamy odmiany: normalną, pogrubioną (Bold), pochyloną (Italic lub Oblique) oraz pogrubioną pochyloną (Bold Italic, Bold Oblique). Dla użytkownika zupełnie nieistotna jest teoretyczna różnica między Italic – pochylony, a Oblique – słabiej pochylony. Rysunek 8 pokazuje dostępne odmiany fontów GFX.
W przypadku fontów bitmapowych zawsze jest duży kłopot ze skalowaniem, dlatego każda odmiana jest oddzielnie definiowana dla różnych wielkości. W przypadku fontów Adafruit GFX mamy do wyboru cztery wielkości: 9, 12, 14 i 24 punkty.
Dodatkowo literki 7b w nazwie wskazują, że jest to „font 7-bitowy”, obejmujący co najwyżej 127 symboli, a ściślej tylko drukowalne symbole kodu ASCII (32…126), nieobejmujący kodów o numerach powyżej 127. To akurat nie jest dobra wiadomość z punktu widzenia problemu „polskich liter”.
Podstawowe zasady wykorzystania takich fontów GFX są bardzo proste.
Domyślnie biblioteka Adafruit GFX korzysta z podstawowego fontu 5×7. Aby wykorzystać ulepszony font GFX, przede wszystkim w szkicu trzeba dodać informację, z jakiego fontu lub kilku fontów będziemy korzystać. Teoretycznie w jednym programie możemy wykorzystać mnóstwo fontów, ale w przypadku Arduino Uno i podobnych z ATmega328P poważnym ograniczeniem jest to, że cała definicja każdego użytego fontu musi zostać zapisana w (niezbyt dużej 32kB) pamięci programu procesora. I to także wtedy, jeśli z tego fontu wykorzystamy tylko kilka znaków do stworzenia jakiegoś bardzo krótkiego napisu.
Wykorzystanie fontów GFX
Po pierwsze na początku szkicu trzeba dodać dyrektywę kompilatora, która dołączy wskazany font lub kilka fontów do procesu kompilacji. Potem w szkicu – programie przed wyświetleniem napisu trzeba niejako przełączyć się na potrzebny font.
Jak pokazuje szkic 3, w dyrektywie #include trzeba podać nie tylko nazwę pliku fontu, ale też ścieżkę: Fonts/.
Potem w treści szkicu można wielokrotnie odwoływać się do tego fontu, a dokładniej do miejsca w pamięci, gdzie zostanie umieszczony, podając w argumencie metody .setFont() tylko nazwę fontu bez rozszerzenia, ale poprzedzoną znakiem ampersand: &.
Szkic 3:
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH1106.h>
#include <Fonts/FreeSans9pt7b.h>
Adafruit_SH1106 wysw(-1);
void setup() { //jednorazowo:
wysw.begin(SH1106_SWITCHCAPVCC, 0x3C);
wysw.setFont(&FreeSans9pt7b); //font
wysw.setTextColor(1, 0);
wysw.display(); delay(1000); //logo
wysw.clearDisplay(); //czyść ekran
wysw.setCursor(0, 0); //kursor
wysw.println(„font Sans 9”);
wysw.display(); //wyświetl na ekranie
}
void loop() { } //pusta pętla
Niestety, po skompilowaniu i załadowaniu szkicu 3 (w Elportalu A2403.ino tylko w wersji dla SH1106), na ekranie zobaczymy żałosny obrazek jak na fotografii 9…
Drobny problem polega na tym, jak na ekranie umieszczane są znaki względem kursora. Zagadkę rozszyfrujesz, jeżeli wyświetlisz znacznie dłuższy napis – obcięte zostanie pierwszych kilkanaście znaków tego napisu. A to, co widzimy na fotografii 9, to dolne piksele właśnie tych kilkunastu znaków prawidłowo wyświetlanego napisu. W materiałach Adafruit wyjaśnia to rysunek 10.
Wcześniej wykorzystywaliśmy podstawowy font, którego jednakowe znaki – glify, mają rozmiar 5×7. W punkcie, gdzie aktualnie znajduje się kursor, umieszczany jest górny lewy róg znaku – glifu. Dokładnie tak samo jest przy wykorzystaniu „zwykłych obrazków”. Jest to dobra prosta zasada, ale fotografia 7 udowadnia, że powoduje ona kłopoty przy wyświetlaniu tekstu o różnej wielkości. Teraz okazuje się, że ulepszenie fontów GFX polega także na tym, że niezależnie od wielkości, znaki są niejako ustawiane na linii bazowej (baseline), czyli znajdują się nad linią bazową. Błąd w szkicu 3 polega na tym, że linią bazową jest najwyższy rządek pikseli o współrzędnej y równej zeru. Poprawimy to, obniżając linię bazową. Symbole, czy jak mówimy: glify, użytego fontu mają wysokość do 13 pikseli, więc linię bazową trzeba obniżyć co najmniej o 12 pikseli. P wyczyszczeniu ekranu instrukcja:
wysw.setCursor(0, 12); //kursor
ustawi kursor na początku 13 linii, a tak poprawiony szkic wyświetli prawidłowy obraz, jak widać na fotografii 11.
Możemy w programie wykorzystać kilka fontów. Przykładem jest szkic 4 (A2304.ino).
Szkic 4:
#include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SH1106.h> #include <Fonts/FreeSans9pt7b.h> #include <Fonts/FreeSerif9pt7b.h> #include <Fonts/FreeMono9pt7b.h> Adafruit_SH1106 wysw(-1); void setup() { wysw.begin(SH1106_SWITCHCAPVCC, 0x3C); wysw.display(); delay(1000); //logo wysw.clearDisplay(); //czyść ekran wysw.setTextColor(1, 0); // wysw.setTextSize(1); //domyślnie wysw.setFont(&FreeSans9pt7b); wysw.setCursor(0, 12); // kursor wysw.println("font Sans 9"); wysw.setFont(&FreeSerif9pt7b); wysw.println("font Serif 9"); wysw.setFont(&FreeMono9pt7b); wysw.println("font Mono 9"); wysw.display(); //wyświetl na ekranie } void loop() { } //pusta pętla
Wyświetli on napisy trzema fontami – fotografia 12.
Rozochoceni takimi sukcesami, próbujemy wykorzystać w programie także inne fonty z większymi znakami według szkicu 5 (A2405.ino).
Szkic 5:
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH1106.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSerif9pt7b.h>
#include <Fonts/FreeMono9pt7b.h>
#include <Fonts/FreeSans24pt7b.h>
#include <Fonts/FreeSerif24pt7b.h>
#include <Fonts/FreeMono24pt7b.h>
Adafruit_SH1106 wysw(-1);
(…) i tak dalej ….
Tak jak poprzednio, w szkicu tym przez sekundę także wyświetlamy obrazek powitalny z logo, a raczej z napisem Adafruit. A następnie w pętli loop() próbujemy przez 3 sekundy wyświetlać napis jak na fotografii 12, a potem po dwie sekundy kolejno trzy krótsze napisy trzema znacznie większymi fontami (24pkt).
Niestety, już podczas kompilacji czeka nas przykra niespodzianka!
Kompilator daje komunikat, pokazany na rysunku 13. Fonty wymagają więcej miejsca, niż ma pamięć programu (32kB)!
Program ze szkicu A2405.ino można zrealizować, ale etapami, wykorzystując w szkicu tylko jeden z fontów o wielkości 24pkt. Aby nie zapomnieć, że oprócz fontów, do pamięci programu zapisywany jest też bez naszej wiedzy obrazek – logo Adafruit, w dotychczasowych szkicach wyświetlamy go na początku przez 1 sekundę.
Jak już wiemy, biblioteka Adafruit (a ściślej procedura w pliku sprzętowego sterownika modułu wyświetlacza) domyślnie ładuje logo Adafruit do pamięci FLASH procesora. Niestety, kompilator nie jest na tyle inteligenty, by sprawdzić, czy ten obrazek jest wyświetlany i jeżeli nie, nie obciążać nim procesora. Możemy to sprawdzić, komentując w szkicu 4 dwie linie odpowiadające za wyświetlanie logo, jak pokazuje szkic 6.
Szkic 6:
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH1106.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSerif9pt7b.h>
#include <Fonts/FreeMono9pt7b.h>
Adafruit_SH1106 wysw(-1);
void setup() {
wysw.begin(SH1106_SWITCHCAPVCC, 0x3C);
//wysw.display(); delay(1000); //logo
//wysw.clearDisplay(); //czyść ekran
wysw.setTextColor(1, 0);
// wysw.setTextSize(1);
wysw.setFont(&FreeSans9pt7b);
wysw.setCursor(0, 12); // kursor
wysw.println(„font Sans 9”);
wysw.setFont(&FreeSerif9pt7b);
wysw.println(„font Serif 9”);
wysw.setFont(&FreeMono9pt7b);
wysw.println(„font Mono 9”);
wysw.display(); //wyświetl na ekranie
}
void loop() { } //pusta pętla
Objętość obu wersji programu będzie różnić się o nieznaczące 88 bajtów, jak pokazuje rysunek 14.
Czym większy rozmiar fontu, tym ładniejsze są znaki, ale też ich definicje zajmą w pamięci programu więcej miejsca. Rysunek 15 wskazuje, że definicja dużego fontu o rozmiarze 24pkt jest około cztery razy większa niż definicja małego fontu 9pkt.
Wprawdzie do pamięci mikroprocesora wstawianych jest mniej informacji, niż wynosi objętość opisującego font pliku .h, jednak chodzi o kilka do kilkudziesięciu kilobajtów.
Dla porównania: logo Adafruit to jednokolorowa bitmapa, która dla całego ekranu miałaby 128*64 punktów, co opiszą 8192 bity – jeden kilobajt. Natomiast pliki zawierające informacje o fontach mają objętość dużo większą. Dlatego jeżeli potrzebnych jest kilka stałych krótkich napisów o różnych krojach i wielkości, korzystniej byłoby tworzyć je nie jako napisy z użyciem fontu, tylko wyświetlać jako wcześniej przygotowane obrazki – bitmapy. Ale jeżeli treść napisów ma się zmieniać zależnie od przebiegu programu – trzeba wykorzystać fonty.
Być może wystarczy jeden font GFX. Warto wiedzić, że także ulepszone fonty GFX można skalować za pomocą metody .setTextSize(); tak samo jak font podstawowy 5×7. Przykład znajdziesz na fotografii 16. Jest to wynik działania szkicu A2406.ino, który jest dostępny w Elportalu (także tylko w wersji dla SH1106).
Skalowanie na fotografii 16 wygląda na prawidłowe, jednak biorąc pod uwagę treść komentarza w pliku bibliotecznym Adafruti_GFX.cpp, opisującym metodę drawChar(), nie należy się dziwić, gdyby przy skalowaniu pojawiły się jakieś problemy.
Na koniec wspomnę, że w Internecie można znaleźć sporo narzędzi do tworzenia fontów „od zera” (from scratch), co jednak nie jest wcale sprawą łatwą, nie tyle ze względu na aspekty techniczne, tylko estetyczne i artystyczne. Zdecydowanie łatwiej wykorzystać już istniejące fonty. W sieci można znaleźć potrzebne konwertery, które potrafią zamieniać współczesne fonty wektorowe (głównie typu TTF – True Type Fonts) na bitmapowe. Niektóre potrafią od razu zapisać wynik takiej konwersji jako gotowy plik w formacie GFX. Taki plik czcionki z rozszerzeniem .h można wykorzystać równie prosto, jak te dostarczone w bibliotece Adafruit GFX. Do tego szczegółu wrócimy przy omawianiu polonizacji fontów.
Oczywiście nie wyczerpaliśmy tematu wyświetlania tekstu i wykorzystania fontów. Gorąco zachęcam, żebyś w ramach ćwiczeń modyfikował szkice i tworzył nowe. W ten sposób nie tylko utrwalisz sobie umiejętności. Zapewne natkniesz się na przeszkody, a próby ich rozwiązywania to bardzo cenna część procesu uczenia się. Dlatego zachęcam do samodzielnych działań, w tym do łączenia wyświetlania podstawowych elementów graficznych, bitmap oraz tekstu. A w następnym odcinku (UR027) zaczynamy omawiać problem „polskich liter”.
Piotr Górecki