Schnelle Echtzeitlupe

Eine schnelle Lupe kann man in den verschiedensten Programmen gut gebrauchen. Es gibt mehrere Möglichkeiten, eine Lupe zu programmieren. Eine einfache, aber recht langsame Methode besteht darin, den zu vergrößernden Ausschnitt mit der Funktion vro_cpyfm() pixelweise ,auseinanderzublit∂ten‘. Der Vorteil der Methode liegt darin, daß sie inallen Auflösungen funktioniert, derNach-teil: besitzt man keinen Blitterchip und kein NVDI, hakt es etwas bei der Vergrößerung, große Lupen werden sogar richtig träge.

Will man eine sehr schnelle Lupe, muß man, dachte ich mir, den Assembler bemühen, um jedes Bit einzeln zu vergrößern. Gesagt, getan, viele Bits wurden geshiftet und ausmaskiert, aber die Geschwindigkeit der STAD-Lupe war nicht zu erreichen. Beim experimentieren fiel mir dann auf, daß, wenn man byteweise und nicht wortweise vergrößert, es ja nur 256 verschiedene Möglichkeiten statt 65535 gibt; die könnte man sich doch auch schon vorher errechnen und dann beim Vergrößern einfach nur noch kopieren.

Erstellen der Vergrößerungstabellen

Das Prinzip ist klar: bei zweifacher Vergrößerung wird aus einem Byte ein Integer, bei vierfacher Vergrößerung ein Langwort, bei achtfacher Vergrößerung muß man das Quellbyte auf zwei Langworte erweitern. Die drei Arrays heißen sinnigerweise auch zweifach, vierfach und achtfach. Jedes Bit der möglichen 256 Bytes wird einzeln geprüft, ob es gesetzt ist oder nicht. Ist ein Bit gesetzt, werden in den Vergrößerungstabellen an der entsprechenden Stelle 2, 4 oder 8 Bits gesetzt (mittels OR-Verknüpfung), ansonsten gelöscht (mittels NOT-AND-Verknüpfung). Dazu werden die Arrays maske2, maske4 und maske8 benötigt, die die vergrößerten Bits enthalten. Das Erstellen der Tabellen erledigt die Funktion tabellen_init().

Groß, größer, am größten

Zu Anfang des Programms zuerst das Übliche: GEM Bescheid sagen, daß wir da sind, und ein Handle für das VDI besorgen. Der Funktion open_work() wird ein Zeiger auf einen MFDB übergeben, der auf die Bildschirmadresse initialisiert wird. Dies kann ganz einfach durch NULL-Setzen geschehen, da das GEM bei vro_cpyfm() dann automatisch alle zum Bildschirm gehörenden Parameter in der Struktur einträgt. Die Funktion init_mfdb() hat mit der Lupe eigentlich nichts zu tun, mit ihr kann man eine beliebige MFDB-Struktur füllen: anhand der Breite und Höhe des gewünschten Rasters wird der Speicherbedarf ausgerechnet (unter Beachtung der Zahl der Farbebenen), ein Flag legt fest, ob das Raster auf eine durch 256 teilbare Adresse gesetzt werden soll (falls man einen Setscreen() auf das Raster machen will). Wir holen uns ein Raster für das zu ladende Bild und zwei Raster für die Lupe. Ein Raster wird für den zu vergrößernden Ausschnitt, das andere für den vergrößerten Auschnitt benötigt. Maximal kann die Lupe den gesamten Bildschirm einnehmen. Schnell noch das Bild zum Vergrößern laden (irgendein *.DOO) aber wehe, es ist nicht dal.

Aufgerufen wird die Lupe über die Funktion lupe() in der folgenden Endlosschleife (Rechtsklick beendet). Übergabeparameter sind zwei Rasteradressen für Quelle (in unserem Fall das Bild) und Ziel (der Bildschirm), vier Integer, die das zu vergrößernde Rechteck beschreiben (X,Y,W,H, man könnte auch eine GRECT-Struktur benutzen), die beiden Zielkoordinaten, und natürlich der Vergrößerungsfaktor. Die Funktion lupe() füllt mit den Koordinatenangaben ein Array und kopiert den Quellausschnitt auf das erste Lupenraster. Da die Funktion vro_cpyfm() sich die Parameter zum 'Blitten' aus der MFDB-Struktur holt, wird die Wortbreite des Lupenrasters auf die Wortbreite des Quellrechtecks angepaßt, damit die Bytes alle hintereinander auf dem Raster liegen. Anhand des Faktors wird entschieden, welche Lupen-Subroutine aufgerufen wird, danach wird die Vergrößerung auf das Zielraster kopiert.

Die drei Routinen sublupe2(), sublupe4(), und sublupe8() arbeiten recht ähnlich. Jeweils eine Zeile wird vergrößert, indem das Quellbyte als Index in die Vergrößerungstabelle benutzt wird (Aha!), daher die hohe Geschwindigkeit der Lupe. Die vergrößerte Zeile wird dann entweder einmal oder dreimal untereinander kopiert. Bei achtfacher Vergrößerung wird die vergrößerte Zeil sechsmal kopiert und die letzte Zeile gelöscht, was zusammen mit der Maske ,0x7F‘ das weiße Gitter ergibt. Will man kein weißes Gitter, muß man die Zeile siebenmal kopieren und die Maske ,0xFF‘ benutzen (im Array maskeß//). Solche kurzen Routinen verleiten mich immer dazu, sie mir mit dem hervorragenden Pure-Debugger genauer in Assembler anzusehen, da man (auch bei Pure-C) immer noch etwas zum Optimieren finden kann. Und es fanden sich auch einige Ansatzpunkte: die Kopierschleifen, bei denen die Zähler nicht zur Indexberechnung gebraucht werden, wurden vom Compiler mit ,CMP‘ (beim Aufwärtszählen) bzw. mit ,TST‘ (beim Abwärtszählen bis 0) auf die Abbruchbedingung geprüft. Dabei werden unnötige Sprungbefehle verwendet, die sich mit einer Assembler-Schleife, die ,DBF‘ oder ,DBRA‘ (zählt bis -1 herunter) benutzt, vermeiden lassen. Die Geschwindigkeitsvorteile lassen sich aber nur ausnutzen, wenn man einen Blitterchip besitzt oder NVDI benutzt, da die meiste Zeit nicht zum Vergrößern, sondern zum 'Blitten' verbraucht wird, besonders wenn die Ausschnitte nicht auf Wortgrenzen liegen (was leider meist der Fall ist). Z.B. dauert die zweifache Vergrößerung von 320x200 Pixel 640x400 Pixel 0.04 Sek. in C und 0.035 Sek. in Assembler. Für die Assembler-Freaks gibt es also auch die entsprechenden Listings, die mit dem Pure-Assembler übersetzt wurden. Aber da eignet sich wohl jeder andere der EXPORT und IMPORT versteht. Das Vereinbaren der Funktionen als MODULE kann auch weggelassen werden, nur bindet der Linker dann immer alle drei Funktionen ein, auch wenn nur eine gebraucht wird.

Ein Nachteil soll aber nicht verschwiegen werden: die Lupen laufen nur in monochromer Auflösung, wobei die Bildschirmgröße allerdings egal ist (mit BIGSCREEN getestet). Wer Lust hat, kann sich ja mal mit den verschiedenen Anordnungen der Farbebenen bei Grafikkarten etc. auseinandersetzen. Die Funktion vr_tmfm(), die ja ein Raster in ein auflösungsunabhängiges Format umwandelt, stellte sich nämlich als großer Bremsklotz heraus. Ansonsten viel Spaß beim Vergrößern.

/*-----------------------------------------*/
/*                                         */
/*             Echtzeitlupe in C           */
/*       programmiert von Ulrich Witte     */
/*                mit PURE-C               */
/*                                         */
/*         (c) 1992 MAXON Computer         */
/*                                         */
/*-----------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <tos.h>
#include <aes.h>
#include <vdi.h>

#define min(a, b) (((a) < (b)) ? (a) : (b))
#define max(a, b) (((a) > (b)) ? (a) : (b))

#define FALSE 0 
#define TRUE 1

#define BREITE 32 
#define HOEHE 32

typedef unsigned char byte;/* Spart Tipparbeit */

			/* Globale Variablen */

unsigned maske2[] = {3,12,48,192,768,
		3072,12288,49152U}; 
unsigned long maske4[] = {0x0000000f,
			0x000000f0,
			0x00000f00,
			0x0000f000,
			0x000f0000,
			0x00f00000,
			0x0f000000,
			0xf0000000};

/*
unsigned long maske8[] = {0x00000000,0x000000ff,
		0x00000000,0x0000ff00, 
		0x00000000,0x00ff0000, 
		0x00000000,0xff000000, 
		0x000000ff,0x00000000, 
		0x0000ff00,0x00000000, 
		0x00ff0000,0x00000000, 
		0xff000000,0x00000000} ;

*/

unsigned long maske8[] = {0x00000000,0x0000007f,
		0x00000000,0x00007f00, 
		0x00000000,0x007f0000, 
		0x00000000,0x7f000000, 
		0x0000007f,0x00000000, 
		0x00007f00,0x00000000, 
		0x007f0000,0x00000000, 
		0x7f000000,0x00000000} ;

MFDB bildschirm,bild,lupe1,lupe2; 
int aes_handle,vdi_handle,work_out[57], work_out_ext[57]; 
unsigned zweifach[256]; 
unsigned long vierfach[256]; 
unsigned long achtfach[512]; 
int cw,ch,zw,zh;

		/* Prototypen */ 

void tabellen_init(void);
int init_mfdb(MFDB *block,int breite,int hoehe, int flag); 
int open_work(MFDB *form); 
int main(void);
void lupe(MFDB *quelle, MFDB *ziel,
		int qx, int qy, int qw, int qh, int zx, int zy, int faktor); 
void sublupe2(byte *src, unsigned *dst, int bytes, int lines); 
void sublupe4(byte *src, unsigned long *dst, int bytes, int lines); 
void sublupe8(byte *src, unsigned long *dst, int bytes, int lines); 
void nothing(byte *src, void *dst, int bytes, int lines);

int align(int x,int n);

int main(void)
{
	int i;
	int mx, my, mk; 

	appl_init();
	vdi_handle = open_work(&bildschirm); 
	init_mfdb(&bild,639,399,0);
	init_mfdb(&lupe1,work_out[0],work_out[1],0); init_mfdb(&lupe2,work_out[0],work_out[1],0);
		/* Das Bild muß ’DESK.DOO' heißen, */
		/* wenn keins da ist wird eben weiß */
		/* vergrößert (sieht man nur nicht viel) */ 
	i = Fopen("DESK.DOO",0); 
	if (i >= 0)
	{
		Fread(i, 32000L,bild.fd_addr);
		Fclose(i);
	}
	tabellen_init();
	graf_mouse(M_OFF,0); /* Maus verstecken */
	do 
	{
		graf_mkstate(&mx,&my,&mk,&i); 
		lupe(&bild,&bildschirm,
			min(mx,639 - BREITE),min(my,399 - HOEHE), 
			BREITE,HOEHE,0,50,2); 
		lupe(&bild,&bildschirm,
			min(mx,639 - BREITE),min(my,399 - HOEHE), 
			BREITE,HOEHE,128,50,4); 
		lupe(&bild,&bildschirm,
			min(mx,639 - BREITE),min(my,399 - HOEHE), 
			BREITE,HOEHE,320,50,8);
	} while(!(mk & 2));
	graf_mouse(M_ON,0); /* Maus wieder an */
	v_clsvwk(vdi_handle); /* und abmelden */
	appl_exit(); 
	return 0;
}

/*--------------------------------------------
	Funktion tabellen_init

	Aufgabe: Erstellt die Vergrößerungstabellen 
	für 2, 4 und 8-fache Vergrößerung

	Eingabe: nichts

	Ausgabe: nichts

	Besonderes: nichts 
----------------------------------------------*/

void tabellen_init(void)
{
	int i,j,k;

	for (i = 0 ; i < 256 ; i++) /* pro Buchstabe */ 
	{	/* 8 Bits testen */
		for (j = 1, k = 0; j < 256 ; j *= 2, k++)
		{
			if ((byte)i & j)	/* Bit gesetzt */
			{					/* Stelle setzen */
				zweifach[i]	|= maske2[k];
				vierfach[i]	|= maske4[k];
				achtfach[i * 2]	|= maske8[k * 2];
				achtfach[i *2+1]	|=	maske8[k *2+1];
			}
			else				/* Bit nicht gesetzt */
			{					/* Stelle löschen */
				zweifach[i] &= ~maske2[k]; 
				vierfach[i] &= ~maske4[k]; 
				achtfach[i * 2] &= ~maske8[k * 2]; 
				achtfach[i * 2 + 1] &= ~maske8[k*2 + 1];
			}
		}
	}
}

/*-------------------------------------------------

	Funktion lupe

	Aufgabe:	Vorbereitung	der	Vergrößerung

	Eingabe: -quelle: Zeiger auf Quellraster 
			-ziel:	Zeiger auf Zielraster
			-qx,qy:	x, y - Koordinaten des Quellausschnitts 
			-qw,qh:	Breite, Höhe des QuellausSchnitts 
			-zx,zy:	x, y - Koordinaten des Zielbreichs 
			-faktor: Vergrößerungsfaktor (derzeit 2,4,8)

	Ausgabe: nichts

	Besonderes: nichts 
------------------------------------------------*/

void lupe(MFDB *quelle, MFDB *ziel,
		int qx, int qy, int qw, int qh, 
		int zx, int zy, int faktor)
{
	int xy[8];
	void (*vergroessern)() = nothing;
	/* Dummyfunktion laden: */
	/* falls falscher Faktor übergeben wird */
	/* nur RTS statt Bomben! */

	/* Breite auf Wortgrenze bringen */ 
	qw = align(qw,16);
	/* MFDB-Wortbreite korrigieren */ 
	lupe1.fd_wdwidth = (qw >> 4);
		/* Bitblit-Array für Quellraster */ 
	xy[0] = xy[2] = qx; 
	xy[1] = xy[3] = qy; 
	xy[4] = xy[5] = 0; 
	xy[2] += (xy[6] = qw - 1); 
	xy[3] += (xy[7] = qh - 1);
		/* und 'blitten' */ 
	vro_cpyfm(vdi_handle,3,xy,quelle,&lupel); 
	switch (faktor)
	{	/*	je Faktor entsprechende Funktion laden */
		case 2:
			vergroessern = sublupe2; 
			break; 
		case 4:
			vergroessern = sublupe4; 
			break; 
		case 8:
			vergroessern = sublupe8; 
			break;
	}
	vergroessern(lupel.fd_addr,lupe2.fd_addr,
			(qw >> 3) , qh) ;
		/* Wortbreite für Zielraster setzen */ 
	lupe2.fd_wdwidth = (qw » 4) * faktor;
		/* Bitblit-Array für Zielraster */ 
	xy[0] = xy[1] = 0; 
	xy[4] = xy[6] = zx;
	xy[5] = xy[7] = zy;
	xy[6] += (xy[2] = qw * faktor - 1);
	xy[7] += (xy[3] = qh * faktor - 1);
		/* Vergrößerung ins Zielraster blitten */
	vro_cpyfm(vdi_handle,3,xy,&lupe2,ziel);
}

/*-------------------------------------------------
	Funktion sublupe2

	Aufgabe: Ausschnitt in x- und y-Richtung

	von src nach dst zweifach vergrößern

	Eingabe:-src: Quelladresse 
			-dst: Zieladresse 
			-bytes: Breite in Bytes 
			-lines: Höhe in Pixelzeilen

	Ausgabe: nichts

	Besonderes: nichts 
-------------------------------------------------*/

void sublupe2(byte *src, unsigned *dst, int bytes, int lines)
{
	unsigned *nextline; int i,j,f;
	for (i = 0 ; i < lines ; i++)
	{
		nextline = dst;
		for (j = 0 ; j < bytes ; j++)
			*dst++ = zweifach[*src++]; 
		for (j = 0 ; j < bytes ; j++)
			*dst++ = *nextline++;

	}

}

/*-------------------------------------------------
	Funktion sublupe4

	Aufgabe: Ausschnitt in x- und y-Richtung

	von src nach dst vierfach vergrößern

	Eingabe:-src: Quelladresse 
			-dst: Zieladresse 
			-bytes: Breite in Bytes 
			-lines: Höhe in Pixelzeilen

	Ausgabe: nichts

	Besonderes: nichts 
-------------------------------------------------*/

void sublupe4(byte *src, unsigned long *dst, int bytes, int lines)
{
	unsigned long *nextline; 
	int i,j,k,f;
	for (i = 0 ; i < lines ; i++)
	{
		nextline = dst;
		for (j = 0 ; j < bytes ; j++)
			*dst++ = vierfach[*src++]; 
		for (j = 0 ; j <=2 ; j++)
		{
			for (k = 0 ; k < bytes ; k++)
				*dst++ = *nextline++; 
			nextline -= bytes;

		}
	}
}

/*-------------------------------------------------
	Funktion sublupe8

	Aufgabe:	Ausschnitt in x- und y-Richtung
				von src nach dst achtfach vergrößern

	Eingabe: -src: Quelladresse 
			-dst: Zieladresse
			-bytes: Breite in Bytes 
			-lines: Höhe in Pixelzeilen

Ausgabe: nichts

Besonderes: nichts 
-------—-----------------------------------------*/

void sublupe8(byte *src, unsigned long *dst, int bytes, int lines)
{
	unsigned long *nextline,*ptr; 
	int i,j,k;
	int offset = bytes * 2;

	for (i = 0 ; i < lines ; i++)
	{
		nextline = dst;
		for (j = 0 ; j < bytes ; j++)
		{
			ptr = &achtfach[(unsigned)(*src++) * 2]; 
			*dst++ = *ptr++;
			*dst++ = *ptr;
		}
		for (j = 0 ; j <= 5 ; j++)
		{
			for (k = 0 ; k < bytes ; k++)
			{
				*dst++ = *nextline++;
				*dst++ = *nextline++;
			}
			nextline -= offset;
		}
		for (k = 0 ; k < bytes ; k++)
		{
			*dst++ = 0L; /*	weißes	Gitter	*/
			*dst++ = 0L;
		}

	}
}

/*-----------------------------------------

Funktion align

Aufgabe:	x auf den nächsten durch
			n teilbaren Wert setzen

Eingabe: 	-x: zu setzender Wert 
			-n: Teiler

Ausgabe: - x auf den nächsten durch n teilbaren Wert gesetzt

Besonderes: nichts 
-----------------------------------------*/

int align(int x,int n)
{
	x += (n >> 1) - 1; 
	x = n * (x / n) ; 
	return x;
}

/*-------------------------------------
	Funktion nothing

	Aufgabe: Dummy-Funktion
-------------------------------------*/

void nothing(byte *src, void *dst, int bytes, int lines)
{
	return;
}

/*--------------------------------------------
	Funktion open_work

	Aufgabe: GEM-Initialisierung, erweiterte Workstation-Info holen

	Eingabe: 	-form: Zeiger auf MFBD-Strukur, die auf Bildschirm gesetzt wird

	Ausgabe: vdi_handle

	Besonderes:	Programmabbruch, falls keine Workstation geöffnet werden kann

-------------------------------------------*/

int open_work(MFDB *form)
{
	int x;
	int work_in[11]; 
	int vdi_handle;

	for(x = 0; x < 10; work_in[x++] = 1)
		;
	work_in[10] = 2;

	aes_handle = graf_handle(&zw,&zh,&cw,&ch); 
	vdi_handle = work_in[0] = aes_handle; 
	v_opnvwk(work_in, &vdi_ handle, work_out);

	if (vdi_handle == 0) /* keine Workstation */
	{
		Cconws("\033E Error: GEM-Initialisierung" "nicht möglich!");
		Bconin(2); 
		exit(1);
	}
	form->fd_addr = NULL;
		/* erweiterte Parameter für Farbebenen */ 
	vq_extnd(vdi_handle,1,work_out_ext); 
	return vdi_handle; /* alles OK */
}

/*--------------------------------------------

	Funktion init_mfdb

	Aufgabe: füllt eine MFDB-Struktur

	Eingabe: 	-block: Zeiger auf MFDB-Strukur, die gefüllt wird 
				-breite: Rasterbreite in Pixel
				-hoehe: Rasterhöhe in Pixel

				-flag: TRUE = Rasteradresse auf durch 256 teilbare Adresse setzen (für Setscreen)

	Ausgabe: 1 = ok, 0 = Fehler

	Besonderes: nichts 
--------------------------------------------*/

int init_mfdb(MFDB *block,int breite,int hoehe, int flag)
{
	int farbebenen = work_out_ext[4]; 
	
	hoehe++;
	if(breite & 0xf)
		breite += (0x10 - breite & Oxf); 
	block->fd_addr = (char *)malloc( (flag ? 256L : 0L) + 
		((long)hoehe * (long)(breite » 3) *
		(long)farbebenen)); 
	if (block->fd_addr == NULL) return FALSE;
		/* evtl. Adresse anpassen */ 
	if (flag)
		if ((long)block->fd_addr & 0xff)
			(long)block->fd_addr +=
			(0x100 - (long)block->fd_addr & Oxff); 
	block->fd_w = breite; 
	block->fd_h = hoehe; 
	block->fd_wdwidth = breite >> 4; 
	block->fd_stand = 0; 
	block->fd_nplanes = farbebenen; 
	return TRUE;
}


EXPORT sublupe2,sublupe4,sublupe8

IMPORT zweifach,vierfach,achtfach

; Parameter für alle 3 Routinen:
; A0 = Quelladresse,
; A1 = Zieladresse,
; D0 = Breite in Bytes 
; D1 = Zeilen in Pixel

TEXT

MODULE sublupe2:
			movem.l a2-a3/d3,-(a7) 
			lea zweifach,a3 
			subq #1,d0
			subq #1,d1
nextline:	movea.l a1,a2 
			move.w d0,d2 
nextbyte:	clr.w d3
			move.b (a0)+,d3 ; Quellbyte * 2 als 
							; Index in die 
							; Vergrößerungstabeile
			add.w d3,d3
			move.w (a3,d3.w),(a1)+ ; Wert kopieren 
			dbf d2,nextbyte 
			move.w d0,d2 
copyline:	move.w (a2)+,(a1)+	; Zeile	kopieren
			dbf d2,copyline 
			dbf d1,nextline 
			movem.l (a7)+,a2~a3/d3 
			rts
ENDMOD

MODULE sublupe4:
			movem.l a2-a3/d3-d4,-(a7)
			lea vierfach, a3
			move.w d0,d4	; Abstand in der Tabelle
			ext.l	d4		; (Zugriff auf	Longs)
			lsl.l	#2,d4	; ausrechnen
			subq #1,d1 
			subq #1,d0
nextline:	movea.l a1,a2 
			move.w d0,d2 
nextbyte:	clr.w	d3
			move.b	(a0)+,d3	; wie sublupe2
			add.w	d3,d3	; Quellbyte	* 4
			add.w	d3,d3
			move.l (a3,d3.w),(a1)+
			dbf 	d2,nextbyte
			moveq	#2,d3
dreilines:	move.w d0,d2	; Ergebnis 3 mal
							; kopieren 
copyline:	move.l	(a2)+,(a1)+ 
			dbf		d2,copyline 
			suba.l	d4,a2 
			dbf		d3,dreilines 
			dbf		d1,nextline 
			movem.l (a7)+,a2-a3/d3-d4 
			rts
ENDMOD

MODULE sublupe8:
			movem.l a2-a4/d3-d7,-(a7) 
			move.w d0,d2 
			move.w d0,d7 
			ext.l d2
			lsl.l #3,d2 		; Tabellenabstand 2 Longs 
			subq #1,d1 
			subq #1,d7
			lea achtfach,a3 
nextline:	movea.l a1,a2 
			move.w d7,d3 
nextbyte:	clr.w d6
			move.b (a0)+,d6
			add.w d6,d6			; Quellbyte * 8	1
			add.w d6,d6
			add.w d6,d6
			lea (a3,d6.w),a4
			move.l (a4)+,(a1)+	; 2 Longs Ergebnis
			move.l (a4)/(a1)+	; kopieren
			dbf d3,nextbyte
			moveq #5,d4			; 5	Zeilen + 1 weiße
								; oder 6 Zeilen kopieren 
								; (je nach Maske)
fivelines:	move.w d7,d3 
copyline:	move.l (a2)+,(a1)+ 
			move.l (a2)+,(a1)+ 
			dbf d3,copyline 
			suba.l d2,a2 
			dbf d4,fivelines 
			move.w d7,d0		; weiße	Zeile
clearline:	clr.1 (a1)+
			clr.l (a1)+ 
			dbf d0,clearline 
			dbf d1,nextline 
			movem.l (a7)+,a2-a4/d3-d7 
			rts
ENDMOD

END

Ulrich Witte
Aus: ST-Computer 10 / 1992, Seite 85

Links

Copyright-Bestimmungen: siehe Über diese Seite