14.11 Bilder anzeigen und Grafiken verwalten
Bilder sind neben dem Text das wichtigste visuelle Gestaltungsmerkmal. In Java können Grafiken an verschiedenen Stellen eingebunden werden. So zum Beispiel als Grafiken in Zeichengebieten (Canvas) oder als Icons in Buttons, die angeklickt werden und ihre Form ändern. Über Java können GIF-Bilder und JPEG-Bilder geladen werden.1
Hinweis GIF und JPEG: Das GIF-Format (Graphics Interchange Format) ist ein komprimierendes Verfahren, das 1987 von CompuServe-Betreibern zum Austausch von Bildern entwickelt wurde. GIF-Bilder können bis zu 1600 x 1600 Punkte umfassen. Die Komprimierung nach einem veränderten LZW5-Packverfahren nimmt keinen Einfluss auf die Bildqualität (sie ist verlustfrei). Jedes GIF-Bild kann aus maximal 256 Farben bestehen - bei einer Palette aus 16,7 Millionen Farben. Entsprechend dem Standard von 1989 können mehrere GIF-Bilder in einer Datei gespeichert werden. JPEG-Bilder sind dagegen in der Regel verlustbehaftet, und das Komprimierverfahren speichert die Bilder mit einer 24-Bit-Farbpalette. Der Komprimierungsfaktor kann prozentual eingestellt werden.
|
Jede Grafik wird als Exemplar der Klasse Image erzeugt. Allerdings unterscheiden sich die Verfahren, um an ein Image zu kommen, bei Applets und Applikationen. In beiden Fällen wird getImage() verwendet, eine Methode, die mehrfach überladen ist, um uns verschiedene Möglichkeiten an die Hand zu geben, Image-Objekte zu erzeugen.
Bilder in Applikationen
Grafiken in einer Applikation werden über die Klasse Toolkit eingebunden. Der Konstruktor kann eine URL beinhalten oder eine Pfadangabe zu der Grafikdatei.
abstract class java.awt.Toolkit
|
|
Image getImage( String filename )
Das Bild wird durch eine Pfadangabe überliefert. |
|
Image getImage( URL url )
Das Bild wird durch die URL angegeben. |
Beispiel Bilder in Applikationen anfordern
Image pic = Toolkit.getDefaultToolkit().getImage( "hanswurst.gif" );
|
Ein Image-Objekt wird erzeugt und das Objekt mit der Datei hanswurst.gif in Verbindung gebracht. Die Formulierung lässt »Laden der Datei« nicht zu, denn die Grafik wird erst aus der Datei beziehungsweise dem Netz geladen, wenn der erste Zeichenaufruf stattfindet. Somit schützt uns die Bibliothek vor unvorhersehbaren Ladevorgängen für Bilder, die später oder gar nicht genutzt werden.
Bilder in Applets
Die Applet-Klasse kennt ebenso zwei getImage()-Methoden, die wiederum die entsprechenden Methoden aus der Klasse AppletContext aufrufen.
interface java.applet.AppletContext
|
|
E Image getImage( URL url )
Das Bild wird durch die URL angegeben. |
Müssen wir in einem Applet die Grafik relativ zu einem Bezugspunkt angeben, der jedoch fehlt, so hilft uns die Funktion getCodeBase() weiter, die uns die relative Adresse des Applets übergibt. (Mit getDocumentBase() bekommen wir die URL des HTML-Dokuments, unter der das Applet eingebunden ist.)
Bilder aus dem Cache nehmen
Eine Webcam erzeugt kontinuierlich neue Bilder. Sollen diese in einem Applet präsentiert werden, so ergibt sich das Problem, dass ein erneuter Aufruf von getImage() lediglich das alte Bild liefert. Dies liegt an der Verwaltung der Image-Objekte, denn sie werden in einem Cache gehalten. Für sie gibt es keinen GC, der die Entscheidung fällt: »Das Bild ist alt«. Hier hilft die Methode flush() der Image-Klasse weiter. Sie löscht das Bild aus der internen Liste. Eine erneute Aufforderung zum Laden bringt also das gewünschte Ergebnis.
abstract class java.awt.Image
|
|
abstract void flush()
Gibt die für das Image belegten Ressourcen frei. |
Hier klicken, um das Bild zu Vergrößern
Hinweis Speicher sparen: Image-Objekte werden nicht automatisch freigegeben. flush() entsorgt diese Bilder, macht wieder Speicher frei und den Rechner wieder schneller.
|
14.11.1 Eine Grafik zeichnen
Eine Grafik wird durch die Funktion drawImage() gezeichnet. Wie erwähnt, wird sie, falls noch nicht vorhanden, vom Netz- oder Dateisystem geladen. Das folgende Programmlisting zeigt eine einfache Applikation mit einer Menüleiste, die über einen Dateiauswahldialog eine Grafik lädt. Die Größe des Fensters wird auf die Größe der Grafik gesetzt.
Listing 14.14 ImageViewer.java
import java.awt.*;
import java.awt.event.*;
public class ImageViewer extends Frame implements ActionListener
{
public ImageViewer()
{
setTitle( "Bildbetrachter" );
// Konstruiere die Menüzeile
MenuBar mbar = new MenuBar();
Menu menu = new Menu( "Datei" );
MenuItem menuitem =
new MenuItem( "Öffnen",new MenuShortcut((int)'Ö') );
menuitem.addActionListener( this );
menu.add( menuitem );
mbar.add( menu );
setMenuBar( mbar );
// Das Fenster mit X schließen
frame = this;
addWindowListener( new WindowAdapter() {
public void windowClosing ( WindowEvent e ) {
System.exit(0);
}
} );
setSize( 600, 400 );
}
public void paint( Graphics g )
{
if ( image != null )
{
g.drawImage( image, 0, 0, this );
setSize( image.getWidth(this), image.getHeight(this) );
}
}
public void actionPerformed( ActionEvent e )
{
FileDialog d = new FileDialog( frame, "Öffne Grafikdatei",
FileDialog.LOAD );
d.setFile( "*.jpg;*.gif" );
d.show();
String file = d.getDirectory() + d.getFile();
image = Toolkit.getDefaultToolkit().getImage( file );
if ( image != null )
repaint();
}
public static void main( String args[] )
{
new ImageViewer().show();
}
private Image image;
private Frame frame;
}
Hier klicken, um das Bild zu Vergrößern
Abbildung 14.6 Ein einfacher Bildbetrachter mit Dateiauswahldialog
14.11.2 Grafiken zentrieren
Eine Funktion zum Zentrieren einer Grafik braucht neben der Grafik als Image-Objekt und dem Graphics zum Zeichnen noch die Komponente, auf der die Grafik gezeichnet wird. Über die getSize()-Funktion des Component-Objekts kommen wir an die Breite und Höhe der Zeichenfläche. Wir holen uns die Hintergrundfarbe und füllen die Zeichenfläche, anschließend positionieren wir das Bild in der Mitte, indem wir die Breite und Höhe des Bilds von der Breite und Höhe der Zeichenfläche subtrahieren und anschließend durch zwei teilen.
public static void
centerImage( Graphics g, Component component, Image image )
{
g.setColor( component.getBackground() );
Dimension d = component.size();
g.fillRect( 0, 0, d.width, d.height );
g.drawImage( image,
( d.width - image.getWidth( null ) ) / 2,
( d.height - image.getHeight( null ) ) / 2,
null );
}
14.11.3 Laden von Bildern mit dem MediaTracker beobachten
Das Laden von Bildern mittels getImage() wird dann vom System angeregt, wenn das Bild zum ersten Mal benötigt wird. Diese Technik ist zwar ganz schön und entzerrt den Netzwerktransfer, ist aber für einige grafische Einsätze ungeeignet. Nehmen wir zum Beispiel eine Animation, dann können wir nicht erwarten, erst dann die Animation im vollen Ablauf zu sehen, wenn wir nacheinander alle Bilder im Aufbauprozess gesehen haben. Daher ist zu wünschen, dass die Bilder erst einmal alle geladen werden können, bevor sie angezeigt werden. Die Klasse MediaTracker ist eine Hilfsklasse, mit der wir den Ladeprozess von Media-Objekten, bisher nur Bilder, beobachten können. Um den Überwachungsprozess zu starten, werden die Media-Objekte dem MediaTracker zur Beobachtung übergeben. Neben diesem besitzt die Klasse gegenüber der herkömmlichen Methode noch weitere Vorteile:
|
Bilder können in Gruppen organisiert werden. |
|
Bilder können synchron oder asynchron geladen werden. |
|
Die Bildergruppen können unabhängig geladen werden. |
Ein MediaTracker-Objekt erzeugen
Um ein MediaTracker-Objekt zu erzeugen, rufen wir seinen Konstruktor mit einem einzigen Parameter vom Typ Component auf:
MediaTracker tracker = new MediaTracker( this );
Wenn wir Applet oder Frame erweitern, kann dies - so wie im Beispiel - der this-Zeiger sein. Dies zeigt aber schon die Einschränkung der Klasse auf das Laden von Bildern, denn was hat eine Musik schon mit einer Komponente zu tun?
Bilder beobachten
Nachdem ein MediaTracker-Objekt erzeugt ist, fügt die addImage(Image)-Methode ein Bild in eine Warteliste ein. Eine weitere überladene Methode addImage(Image, Gruppe ID) erlaubt die Angabe einer Gruppe. Dieser Identifier entspricht gleichzeitig einer Priorität, in der die Bilder geholt werden. Gehören also Bilder zur gleichen Gruppe, ist die Priorität immer dieselbe. Bilder mit einer niedrigeren Gruppennummer werden mit einer niedrigeren Priorität geholt als Bilder mit einer höheren ID. Eine dritte Methode von addImage() erlaubt die Angabe einer Skalierungsgröße. Nach dieser wird das geladene Bild skaliert und eingefügt. Sehen wir uns einmal eine typische Programmsequenz an, die dem Medienüberwacher ein Hintergrundbild sowie einige animierte Bilder überreicht:
Image bg = getImage( "background.gif" ),
anim[] = new Image[MAX_ANIM];
MediaTracker tracker = new MediaTracker( this );
tracker.addImage( bg, 0 );
for ( int i = 0; i < MAX_ANIM; i++ ) {
anim[i] = getImage( getDocumentBase(), "anim" + i + ".gif" );
tracker.addImage( anim[i], 1 );
}
Das Hintergrundbild wird dem MediaTracker-Objekt hinzugefügt. Die ID, also die Gruppe, ist 0. Das Bild-Array anim[] wird genauso gefüllt und überwacht. Die ID des Felds ist 1. Also gehören alle Bilder dieser Animation zu einer weiteren Gruppe.
Um den Ladeprozess anzustoßen, benutzen wir eine der Methoden waitForAll() oder waitForID(). Die waitForID()-Methode wird benutzt, um Bilder mit einer bestimmten Gruppe zu laden. Die Gruppennummer muss natürlich dieselbe vergebene Nummer sein, die bei der addImage()-Methode verwendet wurde. Beide Methoden arbeiten synchron, bleiben also so lange in der Methode, bis alle Bilder geladen wurden oder ein Fehler beziehungsweise eine Unterbrechung auftrat. Da das also das ganze restliche Programm blockieren würde, werden diese Ladeoperationen gerne in Threads gesetzt. Wie diese Methoden in einem Thread verwendet werden, zeigt das folgende Programmsegment. Der Block ist idealerweise in einer run()-Methode platziert oder, bei einem Applet, in der init()-Methode.
try {
tracker.waitForID( 0 );
tracker.waitForID( 1 );
}
catch ( InterruptedException e ) { return; }
Die waitForID()-Methode wirft einen Fehler aus, falls sie beim Ladevorgang unterbrochen wurde. Daher müssen wir unsere Operationen in einen try- und catch-Block setzen.
Während das Bild geladen wird, können wir seinen Ladezustand mit den Methoden checkID() überprüfen. checkID() bekommt als ersten Parameter eine Gruppe zugeordnet und überprüft dann, ob die Bilder, die mit der Gruppe verbunden sind, geladen wurden. Wenn ja, gibt die Methode true zurück, auch dann, wenn der Prozess fehlerhaft ist oder abgebrochen wurde. Ist der Ladeprozess noch nicht gestartet, dann veranlasst checkID(Gruppe) dies nicht. Um dieses Verhalten zu steuern, regt die überladene Funktion checkID(Gruppe, true) das Laden an. Beide geben false zurück, falls der Ladeprozess noch nicht beendet ist.
Eine weitere Überprüfungsfunktion ist checkAll(). Diese arbeitet wie checkID(), nur, dass sie auf alle Bilder in allen Gruppen achtet und nicht auf die ID angewiesen ist. Wie checkID() gibt es checkAll() ebenfalls in zwei Varianten. Die zweite startet den Ladeprozess, falls die Bilder noch nicht geladen wurden.
Die MediaTracker-Klasse verfügt über vier Konstanten, die verschiedene Flags vertreten, um den Status des Objekts zu erfragen. Einige der Methoden geben diese Konstanten ebenso zurück.
Konstante
|
Bedeutung
|
LOADING
|
Ein Medien-Objekt wird gerade geladen.
|
ABORTED
|
Das Laden eines Objekts wurde unterbrochen.
|
ERRORED
|
Ein Fehler trat während des Ladens auf.
|
COMPLETE
|
Das Medien-Objekt wurde erfolgreich geladen.
|
Tabelle 14.4 Flags der Klasse MediaTracker
Mit der Methode statusID(), welche ja den Zustand des Ladens überwacht, können wir leicht die Fälle herausfinden, in denen das Bild erfolgreich beziehungsweise nicht erfolgreich geladen werden konnte. Dazu verknüpfen wir einfach durch den Und-Operator die Konstante mit dem Rückgabewert von statusAll() oder statusID():
if ( (tracker.statusAll() & MediaTracker.ERRORED) != 0 )
Wie wir sehen, können wir durch solche Zeilen leicht herausfinden, ob bestimmte Bilder schon geladen sind. MediaTracker.COMPLETE sagt uns »ja«, und wenn ein Fehler auftritt, dann ist der Rückgabewert MediaTracker.ERRORED. Wir wollen diese Flags nun verwenden, um in einer paint()-Methode das Vorhandensein von Bildern zu überprüfen, und wenn möglich, diese dann anzuzeigen. Erinnern wir uns daran, dass in der Gruppe 0 ein Hintergrundbild lag und in Gruppe 1 die zu animierenden Bilder. Wenn ein Fehler auftritt, zeichnen wir ein rotes Rechteck auf die Zeichenfläche und signalisieren damit, dass etwas nicht funktioniert.
public void paint( Graphics g )
{
if ( tracker.statusID(0, true) == MediaTracker.ERRORED )
{
g.setColor( Color.red );
g.fillRect( 0, 0, size().width, size().height );
return;
}
g.drawImage( bg, 0, 0, this );
if ( tracker.statusID(1) & MediaTracker.COMPLETE) )
g.drawImage( anim[counter%MAX_ANIM], 50, 50, this );
}
class java.awt.MediaTracker
implements Serializable
|
|
static final int ABORTED
Flag, welches anzeigt, dass das Medium nicht geladen werden konnte. Rückgabewert von statusAll() oder statusID(). |
|
static final int ERRORED
Während des Ladens gab es Fehler. Rückgabewert von statusAll() und statusID(). |
|
static final int COMPLETE
Medium konnte geladen werden. Rückgabewert von statusAll() und statusID(). |
|
MediaTracker( Component comp )
Erzeugt einen MediaTracker auf einer Komponente, auf der das Bild möglicherweise angezeigt wird. |
|
void addImage( Image image, int id )
Fügt ein Bild nichtskaliert der Ladeliste hinzu. Ruft addImage(image, id, -1, -1) auf. |
|
void addImage( Image image, int id, int w, int h )
Fügt ein skaliertes Bild der Ladeliste hinzu. Soll ein Bild in einer Richtung nicht skaliert werden, ist -1 einzutragen. |
|
public boolean checkAll()
Überprüft, ob alle vom MediaTracker überwachten Medien geladen worden sind. Falls der Ladeprozess noch nicht angestoßen wurde, wird dieser auch nicht initiiert. |
|
boolean checkAll( boolean load )
Überprüft, ob alle vom MediaTracker überwachten Medien geladen worden sind. Falls der Ladeprozess noch nicht angestoßen wurde, wird dieser dazu angeregt. |
|
boolean isErrorAny()
true, wenn eines der überwachten Bilder einen Fehler beim Laden verursachte. |
|
Object[] getErrorsAny()
Liefert eine Liste aller Objekte, die einen Fehler aufweisen. null, wenn alle korrekt geladen wurden. |
|
void waitForAll() throws InterruptedException
Das Laden aller vom MediaTracker überwachten Bilder wird angestoßen, und es wird so lange gewartet, bis alles geladen wurde oder ein Fehler beim Laden oder Skalieren auftritt. |
|
boolean waitForAll( long ms ) throws InterruptedException
Startet den Ladeprozess. Die Funktion kehrt erst dann zurück, wenn alle Bilder geladen wurden oder die Zeit überschritten wurde. true, wenn alle korrekt geladen wurden. |
|
int statusAll( boolean load )
Liefert einen mit Oder verknüpften Wert der Flags LOADING, ABORTED, ERRORED und COMPLETE. Der Ladeprozess wird bei load auf true gestartet. |
|
boolean checkID( int id )
Überprüft, ob alle Bilder, die mit der ID id verbunden sind, geladen wurden. Der Ladeprozess wird mit dieser Methode nicht angestoßen. Liefert true, wenn alle Bilder geladen sind oder ein Fehler auftrat. |
|
boolean checkID( int id, boolean load )
Wie checkID(id). Allerdings werden nur die Bilder geladen, die bisher noch nicht geladen wurden. |
|
boolean isErrorID( int id )
Liefert den Fehlerstatus von allen Bildern mit der ID id. true, wenn eines der Bilder beim Laden einen Fehler aufweist. |
|
Object[] getErrorsID( int id )
Liefert eine Liste aller Medien, die einen Fehler aufweisen. |
|
void waitForID( int id ) throws InterruptedException
Startet den Ladeprozess für die gegebene ID. Die Methode wartet solange, bis alle Bilder geladen sind. Bei einem Fehler oder Abbruch wird angenommen, dass alle Bilder ordentlich geladen wurden. |
|
boolean waitForID( int id, long ms ) throws InterruptedException
Wie waitForID(), nur stoppt der Ladeprozess nach einer festen Anzahl von Millisekunden. |
|
int statusID( int id, boolean load )
Liefert einen mit Oder verknüpften Wert der Flags LOADING, ABORTED, ERRORED und COMPLETE. Ein noch nicht geladenes Bild hat den Status 0. Ist der Parameter load gleich true, dann werden die Bilder geladen, die bisher nocht nicht geladen wurden. |
|
void removeImage( Image image )
Entfernt ein Bild von der Liste der Medienelemente. Dabei werden alle Objekte, die sich nur in der Skalierung unterscheiden, entfernt. |
|
public void removeImage( Image image, int id )
Entfernt das Bild mit der ID id von der Liste der Medienelemente. Dabei werden auch die Objekte entfernt, bei denen sich die Bilder nur in der Skalierung unterscheiden. |
|
public void removeImage( Image image, int id, int width, int height )
Entfernt ein Bild mit den vorgegebenen Ausmaßen und der ID id von der Liste der Medienelemente. Doppelte Elemente werden ebenso gelöscht. |
14.11.4 Kein Flackern durch Double-Buffering
Zeichnen wir komplexe Grafiken, dann fällt beim Ablauf des Programms deutlich auf, dass der Zeichenvorgang durch Flackern gestört ist. Dieses Flackern tritt in zwei Fällen auf:
|
Wenn wir Bildschirminhalte verschieben und Teile verdeckt werden, muss über die update()- und paint()-Methode der verdeckte Bildausschnitt neu aufgebaut werden. |
|
In der paint()-Methode kommen oft rechenintensive Zeichenoperationen vor, und das Bild muss mittels der Grafikoperationen neu aufgebaut werden. Zeichnen wir ein Dreieck, so müssen wir drei Linien zeichnen. Aber während die Linien gezeichnet werden, fährt der Rasterstrahl mehrmals über den Schirm, und bei jedem Rasterdurchlauf sehen wir ein neues Bild, das immer einen Teil mehr von sich preisgibt. Bei aufwändigen Zeichenoperationen sind nun viele Rasterstrahldurchläufe nötig, bis das Bild komplett ist. |
Hinweis Double-Buffering: Eine einfache und elegante Methode, diesem Flackern zu entkommen, ist die Technik des Double-Buffering. Eine zweite Zeichenebene wird angelegt und auf dieser dann gezeichnet. Ist die Zeichnung komplett, wird sie zur passenden Zeit in den sichtbaren Bereich hineinkopiert.
|
Über Double-Buffering vermeiden wir zusätzliche Zeichenoperationen auf der sichtbaren Fläche, indem wir alle Operationen auf einem Hintergrundbild durchführen. Immer dann, wenn das Bild, beispielsweise eine Konstruktionszeichnung, fertig ist, kopieren wir das Bild in den Vordergrund. Dann kann nur noch bei dieser Kopiermethode Flackern auftreten. Glücklicherweise ist das Zeichnen auf Hintergrundbildern nicht schwieriger als auf Vordergrundbildern, denn die Operationen sind auf einem beliebigen Image erlaubt.
Zunächst benötigen wir einen Offscreen-Puffer für Grafiken als Image-Objekt, auf dem wir die Zeichenoperationen anwenden, zum Beispiel durch die folgenden Zeilen:
Graphics offscreenGraphics;
Image offscreenImage;
Innerhalb der paint()-Methode - oder bei einem Applet gerne in der init()-Funktion - erzeugen wir die Zeichenfläche mit der Funktion createImage(). Die Größe der Fläche muss übergeben werden. Diese können wir über die getSize()-Methode erfragen. Alle von Component abgeleiteten Objekte implementieren getSize().
Neben dem Bild müssen wir noch das Graphics-Objekt initialisieren:
offscreenImage = createImage( 400, 400 );
offscreenGraphics = offscreenImage.getGraphics();
Wo wir vorher innerhalb der paint()-Methoden immer die Grafikoperationen mit dem Graphics g der Methode paint() benutzten, ersetzen wir dieses g durch offscreenGraphics. Unsere Zeichenoperationen verschieben wir von der paint()-Methode in eine eigene Methode, zum Beispiel offPaint(). So werden die drei Linien in der paint()-Methode
public void paint( Graphics g )
{
g.drawLine( 10, 20, 100, 200 );
g.drawLine( 100, 200, 60, 100 );
g.drawLine( 60, 100, 10, 20 );
}
zu
private void offPaint()
{
offscreenGraphics.drawLine( 10, 20, 100, 200 );
offscreenGraphics.drawLine( 100, 200, 60, 100 );
offscreenGraphics.drawLine( 60, 100, 10, 20 );
}
Die Urimplementation der update()-Methode ist so programmiert, dass sie den Bildschirm löscht und anschließend paint() aufruft. Genauer: Der Code der update()-Methode ist in Component durch den Zweizeiler
public void update( Graphics g )
{
clearBackground();
paint( g );
}
gegeben. clearBackground() zeichnet ein gefülltes Rechteck in der Hintergrundfarbe über die Zeichenfläche. Auch dieses Löschen ist für das Flackern verantwortlich. Es macht Sinn, aus der update()-Methode sofort paint() aufzurufen. Die meisten Applikationen überschreiben daher die Implementierung von update().
public void update( Graphics g )
{
paint( g );
}
Somit fällt das lästige und zeitintensive Bildschirmlöschen weg. Da in unserer paint()-Methode ohnehin das gesamte Rechteck gezeichnet wird, können keine Bereiche ungeschrieben bleiben. Der Code der paint()-Methode ist daher nicht mehr spektakulär. Wir haben die Grafik im Hintergrund aufgebaut, und sie muss nun in den eigentlichen Zeichenbereich mit drawImage() kopiert werden. Aus paint() heraus haben wir den aktuellen Graphic-Kontext g, und dann zeichnet
public void paint( Graphics g )
{
if ( offscreenImage != null )
g.drawImage( offscreenImage, 0, 0, this );
}
das Bild. Wohlgemerkt, dieser Funktionsaufruf ist der einzige in paint().
14.11.5 Bilder skalieren
Die Methode getScaledInstance() der Klasse Image gibt ein neues Image-Objekt mit größeren oder kleineren Ausmaßen zurück. Das neue Bild wird wieder nur dann berechnet, wenn es auch benötigt wird - das Verhalten ist also ebenso asynchron wie bei der gesamten Bildverwaltung über die Image-Klasse. Beim Vergrößern oder Verkleinern kommt es zu Pixelfehlern, und das Vergrößern der Pixel beeinflusst das Endergebnis und die Geschwindigkeit. Stellen wir uns vor, ein Bild der Größe 100 x 100 Pixel soll um das Doppelte vergrößert werden. Das Resultat ist ein Bild 200 x 200 Pixel, doch aus einem Bildpunkt muss nun die Information für drei weitere Punkte abgeleitet werden. Eine Lösung wäre, die Farbwerte der Punkte einfach zu duplizieren, dann bleibt die Schärfe, aber das Bild wirkt wie in groben Blöcken. Eine andere Möglichkeit wäre, die Farbinformationen für die neuen Punkte aus den Informationen der Nachbarpunkte zu errechnen. Das Bild wirkt glatter, aber auch etwas unschärfer bei hoher Skalierung. Und ebenso wie beim Vergrößern der Bilder sollten auch beim Verkleinern die Bildinformationen nicht einfach wegfallen, sondern, wenn möglich, zu neuen Farbwerten zusammengefasst werden. So erwarten wir von einem Algorithmus, dass dieser bei einer Schrumpfung von drei Farbwerten zu einem Farbwert die drei Informationen zu einem neuen Wert zusammenlegt.
Damit diese Anforderungen erfüllt werden können, verlangt getScaledInstance() nicht nur die neue Breite und Höhe, sondern auch eine Konstante für die Art der Skalierung. Der Parameter bestimmt den Algorithmus - mögliche Konstanten sind SCALE_DEFAULT, SCALE_FAST, SCALE_SMOOTH, SCALE_REPLICATE und SCALE_AREA_AVERAGING.
Skalierungs-Parameter
|
Bedeutung
|
SCALE_DEFAULT
|
Verwendet einen Standard-Skalierungsalgorithmus.
|
SCALE_FAST
|
Verwendet einen Skalierungsalgorithmus, der mehr Wert auf Geschwindigkeit als auf Glätte des Bilds legt.
|
SCALE_SMOOTH
|
Verwendet einen Algorithmus mit guter Bildqualität und legt weniger Wert auf Geschwindigkeit.
|
SCALE_REPLICATE
|
Benutzt für den Skalierungsalgorithmus den ReplicateScaleFilter.
|
SCALE_AREA_AVERAGING
|
Verwendet den AreaAveragingScaleFilter.
|
Tabelle 14.5 Parameter für getScaledImage()
Mit Hilfe dieser Konstanten lässt sich die Funktion mit Parametern füllen:
|
Image getScaledInstance( int width, int height, int hints )
Liefert ein skaliertes Bild mit den neuen Ausmaßen width und height. Das neue Bild kann asynchron gefördert werden. hints gibt den Skalierungsalgorithmus als Konstante an. Ist die Höhe oder Breite negativ, so berechnet sich der Wert aus dem anderen, um das Seitenverhältnis beizubehalten. |
Beispiel Betrachten wir einige Zeilen Quellcode, der eine Grafik lädt und zwei neue Image-Exemplare konstruiert.
|
Die erste Skalierung soll das Original um einen Prozentwert verändern, und die zweite Skalierung soll - unabhängig von der korrekten Wiedergabe der Seitenverhältnisse - das Bild auf die Größe des Bildschirms bringen. Wir wollen das Bild immer Image.SCALE_SMOOTH skaliert haben.
Image image = new ImageIcon("ottosHaus.jpg").getImage(),
int percent = 175;
Image scaled1 = image.getScaledInstance(
(image.getWidth() * percent) / 100,
(image.getHeight() * percent) / 100,
Image.SCALE_SMOOTH );
Image scaled2 = image.getScaledInstance(
Toolkit.getDefaultToolkit().getScreenSize().width,
Toolkit.getDefaultToolkit().getScreenSize().height,
Image.SCALE_SMOOTH );
Hinter den Kulissen
Was auf den ersten Blick so aussieht wie die Wahl zwischen unglaublich vielen Varianten, entpuppt sich als typische Informatikerlösung: entweder schnell und schmutzig oder schön und lahm. Aber so ist nun mal das Leben. Der Quelltext macht dies deutlich:
public Image getScaledInstance(int width, int height, int hints)
{
ImageFilter filter;
if ((hints & (SCALE_SMOOTH | SCALE_AREA_AVERAGING)) != 0)
filter = new AreaAveragingScaleFilter(width, height);
else
filter = new ReplicateScaleFilter(width, height);
ImageProducer prod;
prod = new FilteredImageSource(getSource(), filter);
return Toolkit.getDefaultToolkit().createImage(prod);
}
Bei der Wahl zwischen sanftem Bild und schnellem Algorithmus wird auf die zwei Filterklassen AreaAveragingScaleFilter und ReplicateScaleFilter abgebildet. Sie berechnen jeweils das neue Bild über einen Bildproduzenten. ReplicateScaleFilter ist der einfachere von beiden. Bei der Vergrößerung werden die Pixel einer Zeile oder Spalte einfach verdoppelt. Wird verkleinert, werden einfach Reihen oder Spalten weggelassen. Mit einem AreaAveragingScaleFilter erhalten wir die besseren Resultate, da Pixel nicht einfach kopiert werden, sondern weil wir eingefügte Pixel aus einer Mittelwertberechnung erhalten. Der Algorithmus heißt im Englischen auch nearest neighbor algorithm.
1 Benannt nach den Erfindern Lempel, Ziv und Welch.
|