Algoritmy rovinné grafiky
Obrázek vytvářený na obrazovce se skládá z různých
elementárních objektů: bodů, úseček, kruhů, elips atd.
Zobrazení těchto objektů je různě složité. Bodu odpovídá
jeden bit obrazové paměti, zde není žádný problém.
U složitějších objektů (čára, kruh) je situace poněkud
složitější. Z čistě matematického hlediska jsou tyto objekty
složeny z nekonečného počtu bodů. Obrazovka samozřejmě
nemůže zobrazit tak velký počet nekonečně malých bodů a tak
musí být provedena určitá aproximace, kdy se vykreslí pixely
ležící nejblíže skutečnému umístění křivky. Algoritmy pro
rovinou grafiku mají za úkol, co nejrychleji
a nejjednodušeji, určit polohu aproximovaných pixelů.
Z těchto algoritmů je nejjednodušší algoritmus pro kreslení
úseček. Rozebereme ho zde tedy poněkud podrobněji.
Bresenhamův algoritmus pro kresbu úsečky
Z matematiky si ještě možná pamatujete, že rovnici
přímky lze zapsat ve tvaru:
y = mx + b, ( 1 )
kde m je směrnice a b posun na ose y. Je-li úsečka určena
počátečním a koncovým bodem (souřadnicmi bodů [x1,y1] a
[x2,y2]), můžeme směrnici a posunutí vyjádřit následujícími
vztahy:
y2-y1 ëy
m = ------- = ----
x2-x1 ëx
( 2 )
x2y1-x1y2 x2y1-x1y2
b = ----------- = -----------.
x2-x1 ëx
Algoritmy pro zobrazení úsečky vycházejí z rovnic 1 a 2.
Na nějakém intervalu ëx se souřadnice y změní o hodnotu ëy
podle následujícího vztahu:
ëy = m ëx.
J.E.Bresenham vymyslel algoritmus, který při generování
úsečky vystačí s celočíselnou aritmetikou. Na obrázku 10 je
nakreslena část obrazovky, na které mají být zobrazeny
pixely úsečky. Po nakreslení levého koncového bodu úsečky
máme rozhodnout, zda následující bod bude vykreslen na
pozici se souřadnicí y stejnou jako předchozí bod nebo o
jedna větší. Z těchto dvou možností umístění následujícího
pixelu vybereme tu, která leží blíže skutečné poloze bodu
úsečky.
Popíšeme si zde způsob, jak pomocí Bresenhamova
algoritmu vykreslit úsečku s kladnou směrnicí menší než 1.
Ostatní směry jsou pouze obměnou dále uvdedného postupu. V
případě, že úsečka má kladnou směrnici menší než jedna je
řídící osou osa x. Znamená to, že postupně pro všechny
souřadnice x měněné o jedničku, určíme jim odpovídající
souřadnice y. Předpokokládejme, že pixel o souřadnicích
[xi,yi] již byl nakreslen a my máme rozhodnou o poloze
následujícího. Na výběr máme ze dvou možností [xi+1,yi] a
[xi+1,yi+1]. Vzdálenost těchto dvou bodů od skutečného bodu
úsečky je na obraázku 10 vyznačena jako d1 a d2. Pro
skutečnou souřadnci y platí:
y = m (xi + 1) + b.
Pak platí:
d1 = y - yi = m (xi + 1) + b - yi
d2 = yi + 1 - y = yi + 1 - m (xi + 1) - b
Rozdíl těchto dvou vzdáleností je:
ëd = d1 - d2 = 2m (xi + 1) - 2yi + 2b - 1 ( 3 )
Podle proměné ëd můžeme určit, který ze dvou pixelů leží
blíže skutečnému umístění usečky. Kladná hodnota ëd, znamená
že d1 > d2 a tudíž bližší je pixel o souřadnicích
[xi+1,yi+1]. Naopak záporná hodnota ëd (d1 < d2) vybere
pixel o souřadnicích [xi+1,yi]. Není tedy důležitá hodnota
ëd, ale její znaménko. Bohužel ëd není díky m celočíslená.
Celou rovnici 3 však můžeme vynásobit ëx a dostáváme:
pi = ëd ëx = 2 ëy xi - 2 ëx yi + 2 ëy + ëx (2b - 1), ( 4 )
kde 2 ëy + ëx (2b - 1) je konstanta, která bude při
následujících úpravách vyloučena. Hodnotu p, podle jejíhož
znaménka určujeme polohu následujících pixelů nazveme
predikcí. Predikci následujcího bodu pi+1 můžeme zapsat
jako:
pi+1 = 2 ëy xi+1 - 2 ëx yi+1 + konstanta ( 5 )
Rovnice 4 a 5 můžeme odečíst, vyjádříme tak pi+1 pomocí pi:
pi+1 = pi + 2 ëy - 2 ëx (yi+1 - yi), ( 6 )
za předpokladu, že xi+1 = xi + 1.
Následující vztahy vyjadřují závislost hodnoty pi+1 na
hodnotě pi:
/---------------------------------------------\
| pi < 0 => pi+1 = pi + 2 ëy |
|---------------------------------------------|
| pi ň 0 => pi+1 = pi + 2 ëy - 2 ëx |
\---------------------------------------------/
První hodnota predikce p1 se získá dosazením počátečního
bodu úsečky do rovnice 4. Dostaneme p1 = 2 ëy - ëx.
Predikce tvoří základní kritérium pro výběr pixelů
tvořících rastrový obraz úsečky. Hodnotu pi pro každý pixel
postupně aktualizujeme podle jednoduchých vztahů v tabulce.
Znaménko predikce určuje polohu následujícího pixelu. Pokud
je predikce záporná, y souřadnice následujícího pixelu se
nemění. Pokud je kladná, zvětší se o jedna.
V případě, kdy je směrnice větší než jedna zaměníme
souřadnice x a y a algoritmus zůstane stejný. Pokud je
směrnice úsečky záporná, jedna ze souřadnic se zmenšuje a
zbytek postupu je shodný.
Příklad na závěr
Některé zde uvedené poznatky shrneme do ukázkového
programu. Je jím jednoduchá grafická knihovna, která dovede
pouze kreslit čáry různými barvami a různým způsobem je
kombinovat s již nakreslenými objekty. Knihovna je ve tvaru
unity pro Turbo Pascal, což je pro většinu z vás asi
nejdostupnější a nejsrozumitelnější programovací jazyk. Pro
dosažení vysoké rychlosti jsou její klíčové části napsáné
v assembleru.
Unit SGraph;
interface
Const
{ Konstanty pro typ kreslení }
NormalPut = 0; { Přepisování }
ANDPut = 8; { Logické AND }
ORPut = 16; { Logické OR }
XORPut = 24; { Logické XOR }
{ Hlavičky exportovaných procedur }
procedure Line( X1, Y1, X2, Y2: word); { Kreslení čáry }
procedure InitGraph; { Inicializace grafiky }
procedure CloseGraph; { Obnovení původního videomódu }
procedure SetColor( Color: byte); { Nastavení barvy pro kreslení čar }
procedure SetWriteMode( Mode: byte); { Volba druhu kombinace kreslených a
již zobrazených dat }
implementation
Const
ActColor: byte = 15; { Proměná sloužící k uložení aktuální barvy
Standardně je nastavena bílá barva }
WriteMode: byte = 0; { Zapisovací režim 0 = přepisování
8 = AND
16 = OR
24 = XOR }
BytesPerRow = 80; { Počet byte v jedné řádce (640/8 = 80) }
IsGraphicsMode: boolean
= false; { Indikace zda jsme v grafice }
PixelsPerByte = 3; { Počet bodů v jednom bite udaný
v bitových posuvech }
ModMask = 7; { Bitová maska pro získání MOD 8 (zbytku
po dělení osmi }
BMask = 128; { Bitová maska pro kreslení bodu }
VRAMSegment = $a000; { Segment počátku obrazové paměti }
GraphicsAR = $3ce; { Port Graphics 1 and 2 Address Register }
ModeRegister = 5; { Index registru zapisovacího módu }
DataRotate = 3; { Index registru pro rotaci a kombinaci dat }
BitMask = 8; { Index registru bitové masky }
var
OldVMode: byte; { Proměná sloužící pro uchování čísla
zobrazovacího režimu }
delta: word; { Pomocné proměné pro uchování 2dx nebo 2dy}
procedure Line( X1, Y1, X2, Y2: word); assembler;
{ Kreslí čáru z X1, Y1 do X2, Y2 }
const
neg_dx = 2; { Znaménko od delta x }
neg_dy = 4; { Znaménko od delta y }
asm
push BP { Uchování registrů BP }
push DS { a DS }
mov AX, X1 { Načtení souřadnic do registrů procesoru }
mov BX, Y1
mov CX, X2
mov DX, Y2
xor BP, BP { Smazání registru BP }
mov DI, CX { Spočítání hodnoty deltax }
sub DI, AX
jg @p_dx { Je deltax kladné ? }
or BP, neg_dx { ne => ulož znaménko do BP }
neg DI { absolutní hodnota DI tj. deltax }
@p_dx:
mov SI, DX { Určení hodnoty deltay }
sub SI, BX
jg @p_dy { Je deltay kladné ? }
or BP, neg_dy { ne => ulož znaménko do BP }
neg SI { absolutní hodnota delaty }
@p_dy:
cmp DI, SI { Porovnání deltax a deltay }
jb @vertical_dir { deltax > deltay <=> absolutní hodnota směrnice > 1
=> vertikální směr
deltax <= deltay <=> absolutní hodnota směrnice <= 1
=> horizontální směr }
@horizontal_dir:
test BP, neg_dx { Porovnání X1 a X2 }
jz @hor_init { Je směr zprava doleva ? }
xchg AX, CX { ne => vyměn souřadnice počátku a konce čáry }
xchg BX, DX
xor BP, neg_dy { uprav znaménko deltay }
@hor_init: { Výpočet adresy prvního bodu }
mov CX, AX { Do CX X1 }
xchg AX, BX { BX = X1, AX = X2 }
mov DX, BytesPerRow { DX = Počet byte na jedné řádce (80) }
mul DX { AX:DX = Y1 * 80 }
shr BX, PixelsPerByte { BX = BX div 8 }
add BX, AX { V BX je offset prvního bodu čáry }
mov AX, DS { Do ES přesuneme DS }
mov ES, AX
mov AX, VRAMSegment { Segment obrazové paměti do DS }
mov DS, AX
mov DX, GraphicsAR { Port adresového registru grafického kontroleru }
mov AX, ModeRegister + $0200 { Nastaví zapisovací mód 2 }
out DX, AX
mov AL, DataRotate { Nastaví způsob kombinace dat z CPU }
mov AH, ES: WriteMode { s latch-registry }
out DX, AX
and CX, ModMask { CX = X1 and 7 }
mov AH, BMask { AH = 80h, tj. nastaven je levý pixel }
shr AH, CL { Rotace AH. Po rotaci je v AH bitová maska bodu }
mov DX, BP { Uchová znaménka deltax a deltay v DX }
mov CX, DI { CX = deltax }
inc CX { CX = deltax - 1, tj. počet bodů na čáře }
{ Výpočet predikce do BP: }
shl SI, 1 { SI = 2*deltay }
mov BP, SI { BP = 2*deltay }
sub BP, DI { BP = 2*deltax - deltay }
shl DI, 1 { DI = 2*deltax }
mov ES: delta, DI { Uložení hodnoty 2*deltax }
mov DI, BX { Do DI offset adresy 1. bodu }
test DX, neg_dy { Určení směru }
pushf { Uchování flagů }
mov DX, GraphicsAR { Nastaví index graf. kontroleru na Bit mask reg. }
mov AL, BitMask
out DX, AL
inc DX { DX = datový port graf. kontroleru }
popf { Obnoví flagy s výsledkem testu směru }
jnz @hor_neg_dy_init { Rozskok, podle znaménka směrnice }
@hor_pos_dy_init: { Kladná směrnice, y souřadnice je zvyšována }
mov BL, ES: ActColor { BL = Barva čáry }
mov BH, BL { BH = Barva čáry }
mov AL, AH { AL = Bitová maska prvního bodu }
@hor_pos_dy:
cmp BP, 0 { Test predikce }
jng @hor_pos_dy_L1
out DX, AL { predikce >= 0 }
mov BL, BH { BL = Barva bodu }
xchg [DI], BL { Nakresli bod }
xor AL, AL { Vynuluje střádanou masku }
sub BP, ES: delta { Upraví predikaci P = P - 2*deltax }
add DI, BytesPerRow { Zvětšení y souřadnice }
@hor_pos_dy_L1:
add BP, SI { Úprava predikce P = P + 2*deltay }
ror AH, 1 { Zvětšení x souřadnice }
jc @hor_pos_dy_L2 { Přesáhli jsme jeden byte ? }
or AL, AH { Uprav masku byte }
loop @hor_pos_dy { Další bod }
jmp @hor_pos_dy_lastbyte { Došli jsme do koncového bodu }
@hor_pos_dy_L2:
out DX, AL { Nastaví novou bitovou masku }
mov BL, BH
xchg [DI], BL { Nakreslí body jednoho byte }
mov AL, AH { Inicializuje bitovou masku }
inc DI { Zvětší x souřadnici }
loop @hor_pos_dy { Byl to poslední bod ? }
jmp @l_done { => konec kreslení čáry }
@hor_pos_dy_lastbyte:
xor AL, AH { Poslední bit je neplatný, odstranit }
out DX, AL { Nastavení bitové masky }
mov BL, BH { BL = Barva čáry }
xchg [DI], BL { Nakreslení bodu }
jmp @l_done { Ukončení kreslení čáry }
@hor_neg_dy_init: { Záporná směrnice, y souřadnice je zmenšována }
mov BL, ES: ActColor { BL = Barva čáry }
mov BH, BL { BH = Barva čáry }
mov AL, AH { AL = Bitová maska prvního bodu }
@hor_neg_dy:
cmp BP, 0 { Test predikce }
jng @hor_neg_dy_L1
out DX, AL { predikce >= 0 }
mov BL, BH { BL = Barva bodu }
xchg [DI], BL { Nakresli bod }
xor AL, AL { Vynuluje střádanou masku }
sub BP, ES: delta { Upraví predikaci P = P - 2*deltax }
sub DI, BytesPerRow { Zmenšení y souřadnice }
@hor_neg_dy_L1:
add BP, SI { Úprava predikce P = P + 2*deltay }
ror AH, 1 { Zvětšení x souřadnice }
jc @hor_neg_dy_L2 { Přesáhli jsme jeden byte ? }
or AL, AH { Uprav masku byte }
loop @hor_neg_dy { Další bod }
jmp @hor_neg_dy_lastbyte { Došli jsme do koncového bodu }
@hor_neg_dy_L2:
out DX, AL { Nastaví novou bitovou masku }
mov BL, BH
xchg [DI], BL { Nakreslí body jednoho byte }
mov AL, AH { Inicializuje bitovou masku }
inc DI { Zvětší x souřadnici }
loop @hor_neg_dy { Byl to poslední bod ? }
jmp @l_done { => konec kreslení čáry }
@hor_neg_dy_lastbyte:
xor AL, AH { Poslední bit je neplatný, odstranit }
out DX, AL { Nastavení bitové masky }
mov BL, BH { BL = Barva čáry }
xchg [DI], BL { Nakreslení bodu }
jmp @l_done { Ukončení kreslení čáry }
@vertical_dir: { Výpočet adresy 1. bodu }
test BP, neg_dy { Kreslíme shora dolů ? }
jz @vert_init
xchg AX, CX { Prohození souřadnic }
xchg BX, DX
xor BP, neg_dx { Úprava znaménka }
@vert_init:
mov CX, AX { CX = X1 }
xchg AX, BX { AX = Y1, BX = X1 }
mov DX, BytesPerRow { DX = Počet byte na řádku (80) }
mul DX { AX:DX = Y1 * 80 }
shr BX, PixelsPerByte { BX = X1 div 8 }
add BX, AX { BX = Offset adresy 1. bodu }
mov AX, DS { Uloží datový segment do ES }
mov ES, AX
mov AX, VRAMSegment { DS = Segment obrazové paměti }
mov DS, AX
mov DX, GraphicsAR { Port adresového registru graf. kontroleru }
mov AX, ModeRegister + $0200 { Nastaví zapisovací mód 2 }
out DX, AX
mov AL, DataRotate { Nastavení způsobu kombinování dat z CPU }
mov AH, ES: WriteMode { s latch-registry }
out DX, AX
and CX, ModMask { CX = X1 and 7 }
mov AH, BMask { Bitová maska pro levý bod (80h) }
shr AH, CL { Rotací vytvoří správnou masku }
mov DX, BP { Uloží znaménka do DX }
mov CX, SI { CX = Délka čáry }
inc CX
{ Určení predikace }
shl DI, 1 { DI = 2*deltax }
add BP, DI { BP = 2*deltax }
sub BP, SI { BP = 2*deltax - deltay }
shl SI, 1 { SI = 2*deltay }
mov ES: delta, SI { uloží 2*deltay }
mov SI, DI { SI = 2*deltax }
mov DI, BX { DI = Offset adresy 1. bodu }
test DX, neg_dx { Zjištění směru }
mov DX, GraphicsAR { DX = adresový port graf. kontroleru }
jnz @vert_neg_dx_init { Rozskok podle znaménka směrnice }
@vert_pos_dx_init: { Kladná směrnice }
mov BL, ES: ActColor { BL = Barva čáry }
mov BH, BL { BH = Barva čáry }
mov AL, BitMask { Index Bit mask registru }
out DX, AX
inc DX { DX = datový registr graf. kontroleru }
mov AL, AH { AL = Bitová maska }
@vert_pos_dx:
mov BL, BH { BL = Barva čáry }
xchg [DI], BL { Nakreslí bod }
cmp BP, 0 { Test predikce }
jng @vert_pos_dx_L1
sub BP, ES: delta { P >= 0, P = P - 2*deltay }
ror AL, 1 { Zvyš souřadnici x }
adc DI, 0 { Přesun do dalšího byte }
out DX, AL { Nastav novou bitovou masku }
@vert_pos_dx_L1:
add BP, SI { P = P + 2*deltax }
add DI, BytesPerRow { Zvyš souřadnici y }
loop @vert_pos_dx { Další bod }
jmp @l_done { Konec čáry }
@vert_neg_dx_init: { Záporná směrnice }
mov BL, ES: ActColor { BL = Barva čáry }
mov BH, BL { BH = Barva čáry }
mov AL, BitMask { Index Bit mask registru }
out DX, AX
inc DX { DX = datový registr graf. kontroleru }
mov AL, AH { AL = Bitová maska }
@vert_neg_dx:
mov BL, BH { BL = Barva čáry }
xchg [DI], BL { Nakreslí bod }
cmp BP, 0 { Test predikce }
jng @vert_neg_dx_L1
sub BP, ES: delta { P >= 0, P = P - 2*deltay }
rol AL, 1 { Zmenši souřadnici x }
sbb DI, 0 { Přesun do dalšího byte }
out DX, AL { Nastav novou bitovou masku }
@vert_neg_dx_L1:
add BP, SI { P = P + 2*deltax }
add DI, BytesPerRow { Zvyš souřadnici y }
loop @vert_neg_dx { Další bod }
jmp @l_done { Konec čáry }
@l_done:
mov DX, GraphicsAR { Smazání bitové masky }
mov AX, BitMask
out DX, AX
mov AX, ModeRegister
out DX, AX { Nastaví zapisovací mód 0 }
mov AX, DataRotate
out DX, AX { Vypne rotyci dat, standardní kombinování dat }
@exit:
pop DS { Obnoví obsah registrů DS }
pop BP { BP }
end;
procedure SetMode( Mode: byte); assembler;
{ Nastaví aktuální zobrazovací režim pomocí služby 00h BIOS }
asm
mov AH, 00h
mov AL, Mode
int 10h
end;
function GetMode: byte; assembler;
{ Službou BIOS zjistí zobrazovací režim }
asm
mov AH, 0fh
int 10h
end;
procedure InitGraph;
{ Inicializace grafiky }
begin
if not IsGraphicsMode then
OldVMode := GetMode; { Uloží číslo aktivního zobrazovacího režimu }
SetMode( $12); { Nastaví zobrazovací režim 640 x 480, 16 barev }
IsGraphicsMode := True; { Nastaví příznak přepnutí do grafiky }
end;
procedure CloseGraph;
{ Ukončí grafický režim }
begin
if IsGraphicsMode then SetMode( OldVMode); { Obnoví původní zobr. režim }
IsGraphicsMode := False;
end;
procedure SetColor( Color: byte);
{ Nastaví barvu použitou pro kreslení }
begin
ActColor := Color;
end;
procedure SetWriteMode( Mode: byte);
{ Nastaví zapisovací režim pro kreslení }
begin
WriteMode := Mode;
end;
begin
end.
Použití grafické knihovny demonstruje následující krátký
program, který navíc využívá některé z registrů karty VGA
k snadné implementaci vertikálního scrolování dvěma směry
zároveň. Všimněte si, že scrolování bude stejně rychlé na
všech počítačích, bez ohledu na jejich rychlost. Je to
způsobeno tím, že jednotlivé animační kroky jsou odděleny
čekáním na vertikální zpětný chod, který je v tomto
zobrazovacím režimu generován s frekvencí přibližně 60 Hz.
Program pro svojí správnou činnost potřebuje kartu VGA.
Uses
Sgraph, { Použvá jednotku SGraph }
Crt; { a Crt }
procedure SetStartAdr( Adr: word); assembler;
{ Procedura nastavující registry CRTC počáteční adresa index 0ch a 0dh }
asm
mov DX, 3dah { Vstupní stavový registr 1 }
@wait_retrace:
in AL, DX
and AL, 8
jz @wait_retrace { Čekání na vertikální zpětný chod }
@wait_display:
in AL, DX
and AL, 8
jnz @wait_display { Čekání na aktivní zobrazovací signál }
mov cx, Adr
mov dx, 03d4h { CRTC adresový registr }
mov al, 0ch { index počáteční adresa - vyšší byte }
mov ah, ch
out dx, ax
inc al { index počáteční adresa - nižší byte }
mov ah, cl { nižší byte adresy }
out dx, ax
end;
procedure SetLineComp( Line: word); assembler;
{ Nastaví registr CRTC porovnání řádky včetně 9. a 10. bitu }
asm
mov DX, 03d4h { CRTC adresový registr }
mov AL, 18h { index registru porovnání řádky }
out DX, AL
mov CX, Line
mov AL, CL
inc DX
out DX, AL { zápis 8 nižších byte čísla řádky }
dec DX
mov AL, 07h { index registru přetečení }
out DX, AL
inc DX
in AL, DX { čte nastavení registru přetečení }
mov CL, CH
and CL, 1
shl CL, 4
and AL, 11101111b { smaže 9. bit čísla řádky pro porovnání }
or AL, CL { nastaví 9. bit }
out DX, AL { zapíše registr }
dec DX
mov AL, 09h { registr počet řádek na znak - obsahuje 10. bit }
out DX, AL
inc DX
in AL, DX
shr CH, 1
and CH, 1
shl CH, 6
and AL, 10111111b { smazání staré hodnoty 10. bitu }
or AL, CH { nová hodnota 10. bitu }
out DX, AL { zapsání registru }
end;
const
MaxX = 639; { Maximální hodnota souřadnice x }
MaxY = 479; { Maximální hodnota souřadnice y }
var
i: word; { Pomocné proměnné }
x: word;
begin
InitGraph; { Inicializace grafiky }
SetWriteMode( XORPut); { Nastavení zapisovacího způsobu }
{ Nakreslení jednoduchého obrazce přes celou obrazovku }
for x := 0 to MaxX do
Line( MaxX div 2, MaxY div 2, x, 0);
for x := 0 to MaxY do
Line( MaxX div 2, MaxY div 2, MaxX, x);
for x := MaxX downto 0 do
Line( MaxX div 2, MaxY div 2, x, MaxY);
for x := MaxY downto 0 do
Line( MaxX div 2, MaxY div 2, 0, x);
{ Jednoduchá animace }
for i := 0 to MaxY div 2 do
begin
SetLineComp( i); { Scrolování dolů pomocí porovnání řádky }
SetStartAdr( i* 80); { Zároveň scrolování nahorů pomocí počáteční adresy }
end;
{ Obrácený směr animace }
for i := MaxY div 2 downto 0 do
begin
SetLineComp( i);
SetStartAdr( i* 80);
end;
Delay( 300); { Malá pauza nakonec }
SetLineComp( 1023); { Nastavení standardní hodnoty }
CloseGraph; { Ukončení grafiky, předchozí zobr. režim }
end.
Literatura:
[1] Kliewer, B.D. - EGA/VGA A programmer's reference guide
371 stran, McGraw-Hill Publishing Company, 1990
[2] Žára, J. - Počítačová grafika - principy a algoritmy
472 stran, Grada a.s., 1992
[3] Brown, R. - Interrupt List - freewarová el. příručka
1993
[Obsah]
Copyright © Jiří Kosek