0994 - Czcionkami Pisane cz.1

Celem mojego artykułu jest próba odpowiedzi na pytanie, jakie zadał pan Jarosław Witkowski w post scriptum do swojego artykułu "CRT w trybie graficznym" (PCKurier 20/93, str.153). Chodziło o informację na temat formatu borlandowskich zbiorów czcionek wektorowych *.CHR i ich ewentualnej modyfikacji. Za pytaniem tym kryje się oczywiście odwieczny problem polskich liter. Liczba artykułów na jego temat, opublikowanych w PCkurierze, bije wszelkie rekordy.

Zanim jednak przejdę do opisu zbiorów *.CHR, przedstawię - nawiązując do artykułu pana Witkowskiego - bardziej uniwersalny sposób otrzymywania polskich liter dla czcionki typu DefaultFont. Pozwoli on przede wszystkim na znacznie szybsze wyświetlanie łańcuchów z polskimi czcionkami, gdyż nie będzie - w przeciwieństwie do swego poprzednika - ingerował w proces wyświetlania poprzez przejecie kontroli nad procedurą OutText. Drugą jego zaletą będzie możliwość uzyskania polskich liter dla dowolnej wielkości czcionki, a ostatnią prostota, z jaką otrzymamy żądany efekt. Niestety, nic nie jest doskonałe. Metoda otrzymania w ten sposób polskich liter wymaga zdefiniowania matrycy polskich znaków 8x8 punktów w dowolnym standardzie kodowania. Jej działanie polega bowiem na podmienieniu matryc odpowiednich znaków z przedziału #128 .. #256. Musze w tym miejscu dodać, iż czytelnicy mający kartę Hercules powinni zdefiniować wszystkie czcionki z tego przedziału. Ma to tę dobrą stronę, że uzyskają w ten sposób w trybie graficznym możliwość wyświetlenia procedurą OutText wszystkich znaków ASCII. By ułatwić życie tej części czytelników, przesłałem do redakcji PCKuriera zbiór z definicjami czcionek z tego przedziału, w skład których wchodzą polskie litery w standardzie Mazovii. Oczywiście można poprzestać tylko na definicji polskich znaków: otrzymamy wówczas na ekranie tylko polskie litery, a pozostałe znaki będą interpretowane jako znaki odstępu.

Jak realizowana jest zamiana czcionek? Aby odpowiedzieć na to pytanie, konieczne jest przedstawienie sposobu, w jaki procedura OutText wyświetla znaki typu DefaultFont. Każdy, kto używał tej procedury, wie, że w przypadku tego typu czcionek nie odwołuje się ona do dysku w celu pobrania odpowiednich definicji. Pomimo to czcionki te, a właściwie ich część (od 'A' do 'z') znajdują się w zbiorze na dysku. Jaki to zbiór ? Oczywiście jedynym zbiorem, w którym można umieścić te znaki, jest zbiór sterownika graficznego *.BGI (choć np. nie ma już ich w zbiorach VGA256, SVGA256, SVGA16). Jeśli ktoś wyświetli sobie te zbiory w postaci binarnej, zobaczy, że wśród "kaszki" na ekranie pojawią się matryce czcionek z podanego powyżej przedziału. Warto w tym miejscu dodać, że w tym samym zbiorze znajdują się również matryce wzorców służących do wypełniania obszarów.

Przedstawię teraz sposób wyświetlania czcionek podstawowych przez procedurę OutText. Znaki z przedziału od 'A' do 'z' są pobierane z pamięci zajmowanej przez sterownik graficzny. Znaki z przedziału #0 ... #64 pobierane są z pamięci ROM karty graficznej spod z góry zadanego adresu $F000:FA6E ([2] tom 2, str.29). Pozostałe znaki są pobierane spod adresu określonego przez przerwanie $1F (jest to tzw. przerwanie adresowe). Przerwanie to jest zarezerwowane dla sterownika graficznego, w celu wskazania obszaru pamięci, zawierającego dodatkowe matryce znaków (#128...#255) dla trybów graficznych 320x200 i 640x200. Jeśli dana karta nie ma tablicy matryc tych znaków, BIOS karty graficznej wpisuje na wektor przerwania $1F wartość $000:000 ([1] str.321).

Na koniec warto zauważyć, że pod adresem $F000:FA6E znajdują się znaki obejmujące pierwsze 127 znaków tabeli ASCII, dziwne więc wydaje się pobieranie tylko 64 początkowych znaków, by pozostałe pobrać z innego miejsca pamięci. Nie wiem, jaki był powód, że autorzy procedury zrealizowali ją właśnie w ten sposób.

Myślę, że po przeczytaniu powyższego tekstu każdy już wie, w jaki sposób dokonamy zamiany. Sprowadza się ona do prostej zmiany wartości wektora przerwania $1F na wartość określającą adres naszej tablicy matryc znaków. Można tego dokonać trzema sposobami: modyfikując wektor przerwania za pomocą funkcji systemowej $25, używając do tego celu procedury udostępnianej przez BIOS karty graficznej ([1] str. 364, funkcja $11, pod funkcja $20, przerwanie $10) lub też modyfikując bezpośrednio wektor przerwania. Ponieważ ostatni sposób jest sposobem najszybszym i w wypadku przerwania adresowego jak najbardziej bezpiecznym, użyjemy właśnie jego. Zrobimy to w taki sposób, że zadeklarujemy sobie zmienną absolutną typu pointer, umieszczając ją pod adresem $0000:$001F*4.

 Var Int1F: Pointer Absolute $0000:$001F * 4

Pomnożenie numeru przerwania przez 4 daje nam jego adres względny - przesunięcie w tabeli wektorów przerwań. Każdy adres jest bowiem reprezentowany przez 4 bajty pamięci, tak zresztą, jak i standardowy typ wskaźnikowy języka Pascal. Teraz przez proste podstawienie pod tą zmienną adresu naszej tablicy matryc znaków zmodyfikujemy wektor przerwania $1F. Wszystkich, którzy w tej chwili chcą odłożyć artykuł, by wypróbować mój sposób, ostrzegam przed takim pospiechem. Sam padłem dwa lata temu jego ofiarą: wbrew wszelkim oczekiwaniom nie otrzymałem zakładanego wyniku. Spowodowało to, że zarzuciłem ten pomysł na jakiś czas, by powrócić do niego pod wpływem niezadowolenia z tego typu rozwiązań, jakie są w poz. [3]. Co było powodem mojego niepowodzenia? Jak już wspomniałem, w wypadku braku definicji czcionek #128 ... #255 BIOS karty wpisuje na wektor przerwania zera. Okazuje się, że procedura OutText, która powinna być przygotowana na taką ewentualność (stąd brak znaków z owego przedziału na karcie Hercules), nie sprawdza całego adresu, lecz tylko jego OffSet. Jeśli jest on równy zero, wtedy przyjmuje się, że w tablicy matryc znaków nie ma. Procedura GetMem języka Pascal zwraca adres danej zmiennej zawsze w takiej postaci, by - o ile to możliwe - OffSet adresu był równy zero. Te właśnie właściwości obu procedur dały w połączeniu ze sobą taki niefortunny efekt. Jak zaradzić takiej sytuacji? Należy odpowiednio "przerobić" adres naszej tablicy. Dokonujemy tego odejmując od segmentu adresu liczbę 1 i dodając do OffSet-u adresu liczbę 16. Wtedy nie zależnie od tego, czy OffSet był równy zero czy nie, otrzymujemy jego wartość niezerową.

Mając te wiadomości można już skonstruować moduł biblioteczny, instalujący polskie litery. Przygotowując taki moduł starałem się, by był on jak najbardziej uniwersalny. Każdy może jednak wybrać z niego tylko te procedury, które najbardziej przypadną mu do gustu, lub też napisać własne bez korzystania z mojego modułu. Ja sam używam tylko jednej z procedur i uzasadnienie mojego wyboru przedstawię podczas opisu tej właśnie procedury.

Konstruując moduł wziąłem pod uwagę wszystkie (jak sądzę) sposoby przechowywania matryc znaków i pod tym kątem przygotowałem procedury. Testowałem je tylko na kartach VGA i Hercules oraz EGA i CGA, we wszystkich trybach tych kart.

Moduł pozwala na pobieranie czcionek ze zbioru dyskowego lub z pamięci. Mogą to być czcionki z przedziału #128 ... #255 lub tylko 18 polskich liter w dowolnej kolejności i dowolnym systemie kodowania. Możliwość pobierania czcionek ze zbioru pociąga za sobą konieczność rezerwowania dla nich miejsca w pamięci. Ponieważ moduł pozwala na wielokrotną zmianę definicji czcionek, konieczne staje się panowanie nad tym, by pamięć nam się nie "zapchała" przy pobieraniu z dysku kolejnych zbiorów. Poza tym można skorzystać z czcionek umieszczonych w pamięci, np. przez zadeklarowanie ich tablicy w segmencie danych, i wtedy nie ma sensu rezerwowanie pamięci, skoro czcionki już w niej rezydują. Dlatego przy zmianie czcionek ze zbioru na te w pamięci należy zwolnić pamięć tych pierwszych. Wszystkie takie przypadki są uwzględnione w konstrukcji procedur. Zmienna prywatna modułu o nazwie "Size" informuje procedury, czy pamięć jest zarezerwowana (Size > 0) czy nie (Size = 0) oraz jaki jest rozmiar zarezerwowanej pamięci (ta ostatnia informacja jest raczej zbyteczna, gdyż zawsze rezerwujemy tę samą liczbę bajtów: 128 znaków po 8 bajtów na znak daje 2048 B pamięci czyli 2 KB).

Zmiana wartości wektora przerwania pociąga za sobą konieczność zapamiętania jego wartości standardowej. Do tego celu służy zmienna prywatna modułu OldInt1F typu Pointer, która pobiera w części inicjującej moduł wartość wektora $1F.

Moduł nazywa się "GrFonts" i ma 5 procedur o dość długich nazwach określających zadania realizowane przez te procedury.

Procedura InstallMFonts instaluje czcionki umieszczone już w jakimś miejscu pamięci. Jeśli przedtem zarezerwowano pamięć dla czcionek pobranych z dysku, procedura zwalnia ją i dopiero wtedy zmienia wartość wektora. Instalowane jest 128 matryc znaków, których adres przekazywany jest przez zmienną amorficzną (czyli bez określonego typu) V.

Procedura InstallPolFonts - podobnie jak jej poprzedniczka - instaluje czcionki umieszczone gdzieś w pamięci, z tym że chodzi tylko o 18 znaków polskich. W tym wypadku konieczne jest jednak zarezerwowanie pamięci. Wynika to ze sposobu pobierania czcionek przez procedurę OutText. Oblicza ona przesuniecie matrycy czcionki względem adresu określonego przez przerwanie $1F według wzoru:

 Przesunięcie := (NumerZnaku - 128) * 8 + Ofs(Int1F^);

Nakłada to na nas konieczność stworzenia takiego obszaru, by przesunięcie matrycy znaku było obliczone właściwie. Oczywiście można ustalić, że polskie litery zaczynają się od znaku #128 do #146 (nowy standard!), ale nawet wtedy istnieje niebezpieczeństwo otrzymania na ekranie "kaszki" - jeśli w naszym łańcuchu znaków będzie znak powyżej wartości #146. Stąd własnie konieczność sprawdzania dla tej procedury, czy rezerwowano pamięć dla czcionek. Jeśli taka sytuacja nie wystąpiła, to pamięć jest rezerwowana. W przeciwnym wypadku poprzednia zawartość pamięci jest czyszczona. Następnie, jeśli pierwotny wektor przerwania wskazywał na definicje czcionek, są one przenoszone na właśnie wyczyszczone miejsce procedurą Move. Ostatnią operacją jest nadpisanie znaków określonych przez tablicę Pol matrycami znaków polskich. Warto zauważyć, że procedura ta pozwala otrzymać polskie litery na karcie Hercules bez potrzeby definiowania wszystkich znaków powyżej #127. Jak wspomniałem wcześniej, pozostałe znaki będą wtedy miały postać znaku odstępu. Obszar pamięci z polskimi znakami podaje się do procedury przez zmienną amorficzną V. Parametr Pol służy do określenia, które znaki będą zamienione na polskie litery. Kolejność występowania znaków w łańcuchu powinna być taka sama jak kolejność występowania ich definicji w tablicy matryc.

Procedura InstallFFonts pozwala pobrać czcionki ze zbioru na dysku do pamięci i zainstalować je. Zbiór musi zawierać matryce znaków z przedziału #128 ... #255. Nazwę zbioru podaje się w "głowie" procedury.

Procedura InstallPolFFonts pobiera ze zbioru na dysku do pamięci 18 matryc polskich znaków, a następnie instaluje je zgodnie ze specyfikacją podaną przez parametr Pol. Nazwę zbioru podaje się przez parametr FName. Taka organizacja procedury nie jest zbyt szczęśliwa, wymaga bowiem od programującego wiedzy na temat standardu i kolejności znaków umieszczonych w zbiorze. Najlepszym rozwiązaniem byłoby włączenie tej informacji do samego zbioru, np. na jego początek, i pobranie jej przy odczycie (do czego zachęcam).

Procedura DeInstallFonts powoduje zwolnienie pamięci zarezerwowanej na matryce znaków i przywrócenie pierwotnego wektora przerwania. Najlepiej umieścić ją na końcu programu.

Ostatnią procedurą, którą omówię, jest InstallHardFonts zadeklarowana w module jako pierwsza. (Ten sposób instalacji czcionek stosowany jest przeze mnie.) Przede wszystkim nie pobiera ona czcionek ze zbioru, poza tym nie zajmuje miejsca w segmencie danych przeznaczonym na zmienne programu poprzez umieszczenie tam czcionek. By zainstalować w ten sposób czcionki, wystarczy jedna instrukcja przypisania (obecność dwóch linii z instrukcjami w procedurze wzięła się z konieczności współpracy z innymi procedurami). Procedura instaluje wszystkie znaki od #128 do #256, zajmując 2048 bajtów pamięci, ale każdy, kto uważnie śledził tekst, wie, że wszystkie pozostałe procedury robią to samo i że nie ma od tego odwrotu. Gdzie znajdują się znaki? Umieszczone są one w segmencie kodu programu. Ich adres to adres procedury FontsDef, która tak naprawdę procedurą nie jest. Nie zawiera ona bowiem rozkazów dla procesora, lecz matryce znaków, i dlatego nie radziłbym jej uruchamiać. W tej chwili część czytelników pewnie zmarszczy czoło. Coś tu chyba nie gra? Procedura to Procedura, a nie jakiś obszar danych!. To, co różni procedurę od danych, to tylko narzucona procesorowi interpretacja bajtów pamięci. Jeśli wywołujemy procedurę (przez jej nazwę), to tak naprawdę procesor pobiera jej adres i zaczyna interpretować bajty spod tego adresu jako rozkazy. Natomiast w przypadku zmiennej procesor dokonuje na niej jakiejś operacji, traktując ją jako normalną liczbę. Od nas więc zależy, czy polecimy procesorowi, by dany obszar pamięci traktował jako listę rozkazów, czy też listę danych. Dlatego możliwe są tego rodzaju "sztuczki" z pamięcią.

Na końcu chciałbym powiedzieć, że ten sposób przechowywania danych jest stosowany w jednym z programów przykładowych pakietu Turbo Pascala o nazwie BGILINK.PAS (właściwie w modułach, których używa, o nazwach : FONTS.PAS i DRIVERS.PAS) i że do tego celu opracowano między innymi program BINOBJ.EXE. Tego programu też należy użyć, by otrzymać ze zbioru z czcionkami zbiór widziany przez kompilator Turbo Pascala dzięki dyrektywie {$L nazwa[.obj]}. Należy wywołać go z następującymi parametrami :

 BINOBJ nazwa1[.bin] nazwa2[.bin] NazwaProcedury

Nawiasy kwadratowe określają części opcjonalne nazw zbioru. Jeśli nie podamy ich w postaci jawnej, to zostaną im nadane postacie podane w nawiasach. Na przykład niech nasz zbiór z czcionkami nazywa się GRAPH.FNT, wtedy uruchamiamy program BINOBJ z następującymi parametrami :

 BINOBJ graph.fnt fonts fontdef

Zawartość tak otrzymanego zbioru zostanie potraktowana jako "ciało" procedury i dołączona do niej poprzez rozpoznanie nazwy pod warunkiem, że za jej deklaracją występuje słowo kluczowe external. W naszym przypadku jest to procedura FontsDef.

Literatura :

  1. Bułhak L, Goczyński R, Tuszyński M., "DOS 5.0 od środka" HELP, Warszawa 1992.
  2. Z. Jasiak, "PC/XT.AT. Najważniejsze dane i tabele. Tom 1/2" M & M Agencja Wydawnicza.
  3. J.K. Kowalski, "Turbo Pascal. Moduły użytkowe", PLJ, Warszawa 1992.
  4. Paweł Skolimowski, "Własne Turbo Vision", PCkurier 17/93.

Wydkuk 1.

 Program FontDemo; Uses Graph, GrFonts;
 Const PathBGI = ' ';  { ściezka do sterownika graficznego }
       Mazovia = ' ';   { zestaw polskich znaków w standardzie Mazovii }

 Var St, Tr : Integer;

 Begin
   St := HercMono;
   Tr := 0;
   InitGraph(St, Tr, PathBGI);
   InstallFFonts(GRAPH.FNT);
   OutText(Mazovia);
   ReadLn;
   CloseGraph;
 End.

Wydkuk 2.

 Unit GrFonts; Interface

 Type PolChar = Array[1..18] Of Char;
      Trans = Array[#128..#255,1..8] Of Byte;

 Procedure InstallHardFonts;
 Procedure InstallMFonts(Var V);
 Procedure InstallPolFonts(Var V; Pol: PolChar);
 Procedure InstallFFonts(FName: String);
 Procedure InstallPolFFonts(FName: String; Pol: PolChar);
 Procedure DeInstallFonts;

 Implementation

 Const Size : Word = 0;

 Var Int1F : Pointer Absolute $0000:$001F * 4;
     OldInt1F : Pointer;

 Procedure FontsDef; External; {$L GRFONTS}

 Procedure GetFntMem(Var P: Pointer);
 Begin
   Size := 128 * 8;
   GetMem(P, Size);
 End;

 Procedure FreeFntMem(P: Pointer);
 Begin
   FreeMem(P, Size);
   Size := 0;
 End;

 Procedure InstallFonts(Var V);
 Begin
   Int1F := Ptr(Seg(V)-1, Ofs(V)+16);
 End;

 Procedure InstallHardFonts;
 Begin
   If Size > 0 Then FreeFntMem(Int1F);
   Int1F := Ptr(Seg(FontsDef)-1, Ofs(FontsDef)+16);
 End;

 Procedure InstallMFonts(Var V);
 Begin
   If Size > 0 Then FreeFntMem(Int1F);
   InstallFonts(V);
 End;

 Procedure InstallPolFonts(Var V; Pol: PolChar);
 Var Fnt: Array[1..18,1..8] Of Byte Absolute V;
     F: Byte;
 Begin
   If Size = 0 Then GetFntMem(Int1F);
   FillChar(Int1F^, Size, 0);
   If OldInt1F <> Nil Then Move(OldInt1F^, Int1F^, Size);
   For F := 1 To 18 Do Move(Fnt[F], Trans(Int1F^)[Pol[F]], 8);
 End;

 {$I-}
 Procedure InstallFFonts(FName: String);
 Var FFont : File;
     FSize : Word;
     P : Pointer;
 Begin
   Assign(FFont, FName);
   ReSet(FFont,1);
   If IOResult <> 0 Then Exit;
   FSize := FileSize(FFont);
   If IOResult <> 0 Then Exit;
   If Size = 0 Then GetFntMem(P);
   If FSize > Size Then FSize := Size;
   BlockRead(FFont, P^, FSize);
   If IOResult <> 0 Then
   Begin
     FreeFntMem(P);
     Exit;
   End;
   Close(FFont);
   InstallFonts(P^);
   If IOResult <> 0 Then Exit;
 End;

 Procedure InstallPolFFonts(FName: String; Pol: PolChar);
 Var FFont : File;
     FSize : Word;
     Fnt: Array[1..18, 1..8] Of Byte;
 Begin
   Assign(FFont, FName);
   ReSet(FFont,1);
   If IOResult <> 0 Then Exit;
   FSize := FileSize(FFont);
   If IOResult <> 0 Then Exit;
   If FSize > SizeOf(Fnt) Then FSize := SizeOf(Fnt);
   BlockRead(FFont, Fnt, FSize);
   If IOResult <> 0 Then Exit;
   Close(FFont);
   InstallPolFonts(Fnt, Pol);
   If IOResult <> 0 Then Exit;
 End;
 {$I+}

 Procedure DeInstallFonts;
 Begin
   If Size > 0 Then
   Begin
     FreeMem(Int1F, Size);
     Size := 0;
   End;
   Int1F := OldInt1F;
 End;

 Begin
   OldInt1F := Int1F;
End.

Paweł Skolimowski -
PC Kurier 09/94 - 28 Kwietnia 1994 -