RotateImage Canvas

Sommario

Introduzione

Il progetto prevede l'implementazione di una classe che, data una immagine, la ruoti di 90 gradi in 90 gradi. Per velocizzare l'operazione creiamo 4 Image contenenti i vari "stadi" della rotazione, anche se avremmo potuto ricalcolarli volta per volta per risparmiare risorse.

Il codice è un buon esempio su come utilizzare le classi java.awt.image.PixelGrabber e java.awt.image.MemoryImageSource.

Indice

Demo

Ecco l'applet in funzione (nota: ho utilizzato il modello degli eventi JDK1.1 e successivo. Se l'applet non funziona è probabilmente dovuto al fatto che non avete un browser aggiornato :-( ). L'immagine è del team Forum: se vi capita date un'occhiata al loro sito perchè spaccano davvero...

Applet 1 - Clicca per ruotare

I parametri supportati dall'applet di esempio sono i seguenti:
Nome Tipo Descrizione
FileName
java.lang.String
Immagine da visualizzare (formato GIF o JPG)

Indice

RotateCanvas

Il canvas dovrebbe essere molto semplice da gestire dall'esterno: lo si costruisce passandogli una Image e si chiama il metodo rotate(void) quando si vuole ruotare l'immagine. In seguito si potrebbe potenziare la classe, per esempio aggiungendo un costruttore che accetti un ImageProducer oppure implementando un metodo che ruoti l'immagine di un multiplo di 90 gradi definito da un parametro. Sono cambiamenti triviali che lascio a voi come esercizio...

class RotateCanvas extends Canvas implements Runnable {

Come spesso nelle mie classi multithreaded, imposto un boolean che mi dice in ogni momento se ho tutto l'occorrente per visualizzare l'immagine o se devo ancora dire all'utente di aspettare. In questo caso l'ho chiamato (con fantasia) boolean done;

Nota: Stranamente il mio browser (IE 5) non gestisce bene i Vector. L'implementazione iniziale dell'elenco degli ActionListener era tramite un Vector ma, dopo svariati tentativi infruttuosi, ho ripiegato su un'array a lunghezza fissa. Ciò va bene perchè difficilmente avrete bisogno di più di 10 ActionListener però il codice perde di flessibilità. Di necessità virtù: guadagnamo in velocità! (magra consolazione)...

Image[] img;

ActionListener[] al;
int countListeners = 0;
boolean done = false;
int curNum = 0;
Color sfondo;

public final static int MAX_LISTENERS = 10;
	
public RotateCanvas(Image image, Color sfondo) {
	super();
	img = new Image[4];
	this.sfondo = sfondo;
	this.img[0] = image;
	al = new ActionListener[MAX_LISTENERS];
	Thread t = new Thread(this);
	t.start();
}

In run() impostiamo PixelGrabber sull'immagine dataci nel costruttore. Forziamo con "true" all'ultimo parametro i dati nel formato RGB per evitare problematiche dovute al formato del file (GIF o JPG). Perdiamo in velocità ma semplifichiamo notevolmente il progetto. Nei casi reali avremmo implementato il nostro ColorModel su misura. Ricordiamoci di chiamare grabPixels() per essere sicuri che il PixelGrabber abbia svolto il suo lavoro prima di ridare il controllo del Thread corrente al nostro programma. In seguito prendiamo i pixels con getPixels() (il cast in int[] è sicuro perchè abbiamo impostato true il campo forceRGB del costruttore).

public void run() {
	PixelGrabber pg = new PixelGrabber(img[0], 0, 0, img[0].getWidth(null), 
	img[0].getHeight(null), true);
	try {
		pg.grabPixels();
	} catch(InterruptedException e) { }
	int[] pix = (int[]) pg.getPixels();
	int[] copy = new int[pix.length];

Ora invertiamo l'immagine di 90 gradi.

	int w = img[0].getHeight(null);
	int h = img[0].getWidth(null);
	int count = 0;
	for(int col = (w-1); col>=0; col-=1) {
		for(int riga = 0; riga<h; riga++) {
			copy[col + riga*w] = pix[count++];
		}
	}

Creiamo una immagine passando attraverso MemoryImageSource (costruttore più semplice che assume che i dati siano in formato RGB). Poi chiamiamo la funzione java.awt.Component.createImage(ImageProducer ip) col nostro MemoryImageSource e, grazie a MediaTracker, ci sinceriamo che l'immagine sia effettivamente pronta prima di continuare.

	MemoryImageSource mis = new MemoryImageSource(w, h, copy, 0, w);
	img[1] = createImage(mis);
	MediaTracker mt = new MediaTracker(this);
	mt.addImage(img[1], 0);
	try {
		mt.waitForAll();
	} catch(InterruptedException e) { }
	pix = copy;	

Come potete vedere l'operazione è ripetuta 4 volte uguale nel codice sorgente completo: questo perchè un'algoritmo iterativo (tramite "for") mi creava qualche problema. Credo che si trattasse di un bug della JVM che ottimizzava il codice interno sovrapponendosi al lavoro compiuto dal Gabage Collector. Risultato? 2 o 3 immagini erano corrette ma le restanti avevano alcune righe inverite... Così ho aggirato il problema.

	copy = new int[pix.length];
	w = img[1].getHeight(null);
	h = img[1].getWidth(null);
	count = 0;
	for(int col = (w-1); col>=0; col-=1) {
		for(int riga = 0; riga<h; riga++) {
			copy[col + riga*w] = pix[count++];
		}
	}
	mis = new MemoryImageSource(w, h, copy, 0, w);
	img[2] = createImage(mis);
	mt = new MediaTracker(this);
	mt.addImage(img[2], 0);
	try {
		mt.waitForAll();
	} catch(InterruptedException e) { }
	pix = copy;

	copy = new int[pix.length];
	w = img[2].getHeight(null);
	h = img[2].getWidth(null);
	count = 0;
	for(int col = (w-1); col>=0; col-=1) {
		for(int riga = 0; riga<h; riga++) {
			copy[col + riga*w] = pix[count++];
		}
	}
	mis = new MemoryImageSource(w, h, copy, 0, w);
	img[3] = createImage(mis);
	mt = new MediaTracker(this);
	mt.addImage(img[3], 0);
	try {
		mt.waitForAll();
	} catch(InterruptedException e) { }
	pix = copy;

Terminata la fase di creazione delle immagini provvediamo ad informare gli ActionListener registrati e ridisegnamo la finestra.

	ActionEvent ae = new ActionEvent(this, 0, "Immagini pronte!");
	for(int i=0; i<countListeners; i++) {
		al[i].actionPerformed(ae);
	}	
	done = true;
	repaint();
}

Il metodo paint è diviso in 2 parti. Se le immagini non sono ancora pronte informo l'utente, altrimenti visualizzo l'immagine corrente. Come si nota provvedo a cancellare il canvas prima di disegnare l'immagine, cosa necessaria in caso di immagini non quadrate. Se non facessi così, l'immagine precedente lascierebbe un "ricordo" assai poco gradevole...

public void update(Graphics g) { paint(g); }
public void paint(Graphics g) {
	if(!done) {
		g.setColor(sfondo);
		g.fillRect(0, 0, getSize().width, getSize().height);
		g.setColor(Color.yellow);
		g.drawString("Preparazione delle immagini.", 20, getSize().height/2 - 10);
		g.drawString("Attendere prego...", 20, getSize().height/2 + 10);
		g.setColor(Color.cyan);
		g.drawString("MindFlavor :-)", 5, getSize().height-20);
	} else {
		g.setColor(sfondo);
		g.fillRect(0, 0, getSize().width, getSize().height);
		g.drawImage(img[curNum], 0, 0, null);
	}
}

Il metodo rotate() non fa altro che cambiare l'immagine corrente e chiamare il ridisegno della finestra.

public void rotate() {
	if(++curNum >= 4)
	curNum = 0;
	repaint();
}

Indice

L'Applet di esempio

L'applet di esempio mostra come si può utilizzare la classe RotateCanvas: si carica l'immagine, si crea il RotateCanvas con l'immagine passata e gli si dice quando deve ruotare (nel nostro caso rispondiamo al click del mouse).

public class RotateImage extends Applet {

public final static String FILE_NAME = "FileName";
Image img = null;
RotateCanvas rotate;

public void init() {
	String name = getParameter(FILE_NAME);
	if(name == null) {
		showStatus("Errore!");
	return;
	}
	img = getImage(getCodeBase(), name);
	MediaTracker mt = new MediaTracker(this);
	mt.addImage(img, 0);
	try {
		mt.waitForAll();
	} catch(InterruptedException e) { }
	rotate = new RotateCanvas(img, Color.black);
	rotate.addMouseListener(new MouseAdapter() {
		public void mousePressed(MouseEvent e) {
			rotate.rotate();
			repaint();
		}
	});
	setLayout(new BorderLayout());
	add(rotate, BorderLayout.CENTER);
}

}

Indice

Download

Descrizione Dimensione Download
Sorgenti e binari compilati
6 Kb

Indice

Ultimo aggiornamento: 8 Settembre 2003

@2002 Francesco Cogno