Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5 (Tiger-Release)
Buch: Java ist auch eine Insel
gp Kapitel 14 Grafikprogrammierung mit dem AWT
  gp 14.1 Das Abstract-Window-Toolkit
    gp 14.1.1 Java Foundation Classes
  gp 14.2 Das Toolkit
  gp 14.3 Fenster unter grafischen Oberflächen
    gp 14.3.1 AWT-Fenster darstellen
    gp 14.3.2 Swing-Fenster darstellen
    gp 14.3.3 Sichtbarkeit des Fensters
    gp 14.3.4 Größe und Position des Fensters verändern
    gp 14.3.5 Hauptprogramm von Frame/JFrame ableiten
    gp 14.3.6 Fenster- und Dialog-Dekoration
  gp 14.4 Grundlegendes zum Zeichnen
    gp 14.4.1 Die paint()-Methode für das AWT-Frame
    gp 14.4.2 Zeichen von Inhalten mit JFrame
    gp 14.4.3 Auffordern zum Neuzeichnen mit repaint()
    gp 14.4.4 Fensterinhalte ändern und die ereignisorientierte Programmierung
  gp 14.5 Einfache Zeichenfunktionen
    gp 14.5.1 Linien
    gp 14.5.2 Rechtecke
    gp 14.5.3 Ovale und Kreisbögen
    gp 14.5.4 Polygone und Polylines
  gp 14.6 Zeichenketten schreiben
    gp 14.6.1 Einen neuen Zeichensatz bestimmen
    gp 14.6.2 Ableiten eines neuen Fonts aus einem gegebenen Font
    gp 14.6.3 Zeichensätze des Systems ermitteln
    gp 14.6.4 Die Klasse FontMetrics
    gp 14.6.5 True Type Fonts
  gp 14.7 Clipping-Operationen
  gp 14.8 Farben
    gp 14.8.1 Zufällige Farbblöcke zeichnen
    gp 14.8.2 Farbanteile zurückgeben
    gp 14.8.3 Vordefinierte Farben
    gp 14.8.4 Farben aus Hexadezimalzahlen erzeugen
    gp 14.8.5 Einen helleren oder dunkleren Farbton wählen
    gp 14.8.6 Farbmodelle HSB und RGB
    gp 14.8.7 Die Farben des Systems
  gp 14.9 Bilder anzeigen und Grafiken verwalten
    gp 14.9.1 Bilder laden: eine Übersicht
    gp 14.9.2 Bilder über Toolkit laden
    gp 14.9.3 Das Image zeichnen
    gp 14.9.4 Grafiken zentrieren
    gp 14.9.5 Bilder im Speicher erzeugen
    gp 14.9.6 Kein Flackern durch Double-Buffering
    gp 14.9.7 Bilder skalieren
    gp 14.9.8 VolatileImage
    gp 14.9.9 Programm-Icon/Fenster-Icon setzen
    gp 14.9.10 Bilder lesen mit ImageIO
    gp 14.9.11 Schreiben mit ImageIO
    gp 14.9.12 Kann ImageIO ein Format behandeln?
    gp 14.9.13 Komprimieren mit ImageIO
    gp 14.9.14 Bilder im GIF-Format speichern
    gp 14.9.15 Gif speichern mit dem ACME-Paket
    gp 14.9.16 JPEG-Dateien mit dem Sun-Paket schreiben
    gp 14.9.17 Java Image Management Interface (JIMI)
  gp 14.10 Java 2D-API
    gp 14.10.1 Grafische Objekte zeichnen
    gp 14.10.2 Geometrische Objekte durch Shape gekennzeichnet
    gp 14.10.3 Eigenschaften geometrischer Objekte
    gp 14.10.4 Transformationen mit einem AffineTransform-Objekt
  gp 14.11 Von Produzenten, Konsumenten und Beobachtern
    gp 14.11.1 Producer und Consumer für Bilder
    gp 14.11.2 Beispiel für die Übermittlung von Daten
    gp 14.11.3 Bilder selbst erstellen
    gp 14.11.4 Die Bildinformationen wieder auslesen
  gp 14.12 Filter
    gp 14.12.1 Grundlegende Eigenschaft von Filtern
    gp 14.12.2 Konkrete Filterklassen
    gp 14.12.3 Mit CropImageFilter Teile ausschneiden
    gp 14.12.4 Transparenz
  gp 14.13 Drucken
    gp 14.13.1 Drucken mit dem einfachen Ansatz
    gp 14.13.2 Ein PrintJob
    gp 14.13.3 Drucken der Inhalte
    gp 14.13.4 Komponenten drucken
    gp 14.13.5 Den Drucker am Parallelport ansprechen
    gp 14.13.6 Bekannte Drucker
  gp 14.14 Graphic Layers Framework
  gp 14.15 Grafikverarbeitung ohne grafische Oberfläche
    gp 14.15.1 Xvfb-Server
    gp 14.15.2 Pure Java AWT Toolkit (PJA)


Galileo Computing

14.9 Bilder anzeigen und Grafiken verwaltedowntop

Bilder sind neben dem Text ein wichtigstes visuelles Gestaltungsmerkmal. In Java können Grafiken an verschiedenen Stellen eingebunden werden. So zum Beispiel als Grafiken in Zeichengebieten (Canvas) oder als Icons in Schaltflächen, die angeklickt werden und ihre Form ändern. Über Java können GIF-, PNG- und JPEG-Bilder geladen werden.


Hinweis   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 LZW–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.


Galileo Computing

14.9.1 Bilder laden: eine Übersicht  downtop

Die Java-API bringt auch gleich mehrere Möglichkeiten mit, um an Bilder zu kommen – einige nur durch Zusatzbibliotheken.

gp  getImage() liefert Image-Objekte. Die Methode hängt an Toolkit bei Applikationen und an AppletContext bei Applets.
gp  Der Media-Tracker überwacht das Laden seit Java 1.0, und steckt hinter getImage().
gp  ImageIcon lädt für Swing Bilder, die sich direkt auf der grafischen Oberfläche auf Komponenten platzieren lassen. Nutzt im Hintergrund den Media-Tracker.
gp  Über statische Funktionen wie read() der Klasse ImageIO seit Java 1.4. Das Paket ImageIO wurde in Java 1.4 eingeführt, um das Lesen und Schreiben von Grafiken zu vereinheitlichen.
gp  Das Paket com.sun.image.codec.jpeg definert seit Java 1.2 Funktionen zum Lesen und Schreiben von JPGs. Das Paket zeigt jedoch durch die Benennung an, dass es nicht ganz offiziell ist, und damit nicht jeder Java Laufzeitumgebung bekannt sein muss.
gp  Über die externe Java-Bibliothek JAI (Java Advanced Imaging API). Hier kommen noch Formate wie TIFF und WBMP dazu. Informationen gibt die Seite http://java.sun.com/products/java-media/jai/iio.html.
gp  JIMI (Java Image Management Interface) ist eine hundertprozentige Java-Klassenbibliothek, die hauptsächlich Lade- und Speicherroutinen für Bilder zur Verfügung stellt. Die Klasse JimiUtils stellt beispielsweise eine getThumbnail()-Methode bereit, die zu einer Datei ein Vorschaubild als Image-Objekt berechnet. Ebenso stellt JIMI Möglichkeiten zur Anzeige bereit, um etwa sehr große Grafiken speichersparend zu verwalten. Diese Technik nennt sich Smart-Scrolling und kann von der JimiCanvas-Komponente übernommen werden. So wird nur der Bildteil im Speicher gehalten, der gerade sichtbar ist. Für die Speicherverwaltung stellt JIMI ein eigenes Speicherverwaltungssystem, das VMM (Virtual Memory Management), bereit, ebenso wie eine eigene Image-Klasse, die schnelleren Zugriff auf die Pixelwerte erlaubt. Zusätzlich bietet JIMI eine Reihe von Filtern für Rotation und Helligkeitsanpassung, die auf JIMI- und AWT-Bildern arbeiten. Auch Farbreduktion ist ein Teil von JIMI. JIMI-Bilder lassen sich im Gegensatz zu den bekannten AWT-Bildern serialisieren.

Für exotische Formate – etwa das Windows Icon-Format – hilft nur eine Suche im Web. Im Fall der ICO-Dateien hilft die freie Bibliothek AC.lib ICO unter http://www.acproductions.de/commercial/aclibico/.


Galileo Computing

14.9.2 Bilder über Toolkit laden  downtop

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  

gp  Image getImage( String filename )
Das Bild wird durch eine Pfadangabe überliefert.
gp  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.


Hinweis   Da die getImage()-Funktion für URLs und Strings definiert ist, ist vor folgendem Konstrukt natürlich nur zu warnen:

getImage( "http://hostname/grafik"; );
Gewiss führt dies zu einem gnadenlosen Fehler, denn eine Datei mit dem Namen http://hostname/grafik gibt es nicht! Korrekt heißt es:

getImage( new URL("http://hostname/grafik";) );

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  

gp  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.)

Asynchrones Laden get getImage() und dem MediaTracker

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:

gp  Bilder können in Gruppen organisiert werden.
gp  Bilder können synchron oder asynchron geladen werden.
gp  Die Bildergruppen können unabhängig geladen werden.

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  

gp  abstract void flush()
Gibt die für das Image belegten Ressourcen frei.

Abbildung
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.


Galileo Computing

14.9.3 Das Image zeichnen  downtop

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.

Listing 14.15   ImageViewer.java


import java.awt.*;
import java.awt.event.*;
import java.io.File;

import javax.swing.*;
import javax.swing.filechooser.FileFilter;

public class ImageViewer extends JFrame implements ActionListener
{
private ViewComponent viewComponent = new ViewComponent();

public ImageViewer()
{
super( "Bildbetrachter" );

JMenuBar mbar = new JMenuBar();
JMenu menu = new JMenu( "Datei" );
JMenuItem item = new JMenuItem( "Öffnen" );
item.setAccelerator( KeyStroke.getKeyStroke(KeyEvent.VK_O, KeyEvent.CTRL_MASK) );
item.addActionListener( this );
menu.add( item );

mbar.add( menu );
setJMenuBar( mbar );

add( viewComponent );

setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
setSize( 600, 400 );
}

public void actionPerformed( ActionEvent e )
{
JFileChooser d = new JFileChooser();
d.setFileFilter( new FileFilter()
{
public boolean accept(File f)
{
return f.isDirectory() || f.getName().toLowerCase().endsWith(".jpg")
|| f.getName().toLowerCase().endsWith(".gif");
}
public String getDescription()
{
return "*.jpg;*.gif";
}

} );
d.showOpenDialog( null );
File file = d.getSelectedFile();
viewComponent.setImage( file );
}

public static void main( String args[] )
{
new ImageViewer().setVisible( true );
}
}

class ViewComponent extends JComponent
{
private Image image;

protected void paintComponent( Graphics g )
{
if ( image != null )
g.drawImage( image, 0, 0, this );
}

public void setImage( File file )
{
image = Toolkit.getDefaultToolkit().getImage( file.getAbsolutePath() );
if ( image != null )
repaint();
}
}

Abbildung
Hier klicken, um das Bild zu Vergrößern

Abbildung 14.6   Ein einfacher Bildbetrachter mit Dateiauswahldialog


Galileo Computing

14.9.4 Grafiken zentrieren  downtop

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.widthimage.getWidth( null ) ) / 2,
               ( d.heightimage.getHeight( null ) ) / 2,
               null );
}

Galileo Computing

14.9.5 Bilder im Speicher erzeugen  downtop

Nicht immer kommen die Bilder vom Datensystem oder aus dem Internet. Mit der Java-Bibliothek lassen sich einfach auch eigene Image-Objekte anlegen. Dazu bietet jede AWT-Komponente – und Frame und auch Panel sind AWT-Komponenten – die Methode createImage().


Image image = panel.createImage( 400, 400 );

Die Rückgabe ist ein Image-Objekt und dies bietet mit getGraphics() Zugriff auf den Grafik-Kontext.


Graphics g = image.getGraphics();

Wenn die Komponente noch nicht angezeigt wurde, liefert createImage() die Rückgabe null, so dass hier leicht eine NullPointerException enstehen kann,



abstract class java.awt.  Component  
implements ImageObserver, MenuContainer, Serializable

gp  Image createImage( int width, int height )
Liefert in neues Hintergrundbild mit den gegebenen Ausmaßen.

BufferedImage

Die Einschränkung, dass createImage() immer eine AWT-Komponente benötigt ist lästig. Seit Java 1.2 wurde zur Vereinfachung der Grafikverarbeitung die Klasse BufferedImage eingeführt, die eine Erweiterung der Image-Klasse ist. Der Konstruktor der Klasse wird mit den Ausmaßen parametrisiert und zusätzlich mit einem Speichermodell für die Bildinformationen.


int h = 400,
    b = 600;

BufferedImage img = new BufferedImage( b, h,
                                       BufferedImage.TYPE_INT_RGB );

Das notwendige dritte Argument kennzeichnet den Speichertyp; hier sind die Farben durch je 8 Bit rot, grün, blau abgebildet.

Da BufferedImage ein Image ist, gibt es auch wieder eine getGraphics()-Methode, die ein Graphics-Objekt liefert, um das Bild zu bemalen. Diese Methode existiert aber nur aus Gründen der Kompatibilität. Es ist üblich, hier die Methode createGraphics() einzusetzen, da sie ein Graphics2D-Objekt – eine Unterklasse von Graphics – liefert.


Graphics2D g = img.createGraphics();
g.setColor( Color.WHITE );
g.fillRect( 0, 0, b1, h1 );

Alternativ kann zum Löschen des Hintergrundes auch g.setBackground(Color.WHITE); g.clearRect(Argumente); verwendet werden.



class java.awt.image.BufferedImage
extends Image
implements RenderedImage, Transparency, WritableRenderedImage

gp  BufferedImage( int width, int height, int imageType )
Liefert ein neues Hintergrundbild mit den gegebenen Ausmaßen.

Galileo Computing

14.9.6 Kein Flackern durch Double-Buffering  downtop

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:

gp  Wenn wir Bildschirminhalte verschieben und Teile verdeckt werden, muss über die update()- und paint()-Methode der verdeckte Bildausschnitt neu aufgebaut werden.
gp  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().


Galileo Computing

14.9.7 Bilder skalieren  downtop

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.


Tabelle 14.4   Argumente für getScaledImage()

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.

Mit Hilfe dieser Konstanten lässt sich die Funktion aufrufen:



class java.awt.  Image  

gp  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.


Galileo Computing

14.9.8 VolatileImage  downtop

Die unter 1.4 neu eingeführte abstrakte Klasse VolatileImage mit ihren Unterklassen bietet eine effiziente Möglichkeit, Bilder direkt im Speicher der Grafikkarte abzulegen, sofern dies vom Betriebssystem unterstützt wird. Eine Konsequenz ist, dass die Informationen dann natürlich verloren gehen, wenn beispielsweise ein Fenster den Inhalt überdeckt. Doch das Einsatzfeld dieser direkten Manipulation liegt im Bereich von schnellen Bildschirmdarstellungen, wie sie zum Beispiel bei Spielen oder Video-Übertragungen nötig sind. Die VolatileImage-Objekte erkennen defekte Darstellungen und leiten sie an die Software weiter, die dann wiederum eine Bildschirmaktualisierung vornehmen kann.

Damit VolatileImage-Objekte erstellt werden können, gibt es die Methode createVolatileImage(w, h) in den Klassen Component und ComponentPeer. Da ein VolatileImage sich genauso wie ein Image-Objekt verhält, ändert sich am Zeichnen mit drawImage() nichts.

Mehr über die Einsatzgebiete und eine Beispielimplementierung findet sich unter der Web-Adresse ftp://ftp.java.sun.com/docs/j2se/1.4/VolatileImage.pdf.


Galileo Computing

14.9.9 Programm-Icon/Fenster-Icon setzedowntop

Unter Windows ist jedem Fenster ein kleines Bildchen zugeordnet, das links am Fenster neben den Menüs untergebracht ist. Dies ist ein Programm-Icon; es lässt sich in Java durch die setIconImage()-Funktion setzen. Der Methode wird ein Image-Objekt übergeben, welches die Grafik der Größe 16 x 16 Pixel beinhaltet. Doch hier gilt, was für andere Bilder auch gilt: Durch einen Aufruf von getImage() wird eine Grafik zwar vorbereitet, aber noch nicht physikalisch geladen. Bei der drawImage()-Methode wird der Ladevorgang durchgeführt, setIconImage() könnte sich nun ähnlich verhalten – macht es aber nicht. Versuchen wir es etwa mit nachstehendem Code, der direkt in der Erweiterung von Frame liegt, so führt


image = Toolkit.getDefaultToolkit().getImage( "image.gif" );
  setIconImage  ( image );
show();

mitunter zum Absturz. Erstaunlicherweise kann die Vertauschung der zwei Zeilen setIconImage() und show() korrekt verlaufen, ohne einen Laufzeitfehler zu produzieren. Wir müssen wieder mit der Funktion prepareImage() darauf achten, dass tatsächlich von der Datei oder vom Netz geladen wird. Erst dann dürfen wir setIconImage() aufrufen. Praktisch ist in diesem Fall jedoch die ImageIcon-Klasse, da sie automatisch das Bild lädt.



class java.awt.  Frame  
extends Window
implements MenuContainer

gp  void setIconImage( Image image )
Ordnet dem Fenster eine kleine Grafik zu. Nicht alle grafischen Oberflächen erlauben diese Zuordung.

Galileo Computing

14.9.10 Bilder lesen mit ImageIO  downtop

ImageIO ist sehr einfach zu nutzen. Eine kleine statische Funktion, und schon liegt eine Grafik vor. Das neue Paket liest immer GIF, JPEG und PNG. (Eine präzisere Liste der angemeldeten Leser liefert ImageIO.getReaderFormatNames() und ImageIO.getReaderMIMETypes().) Lesen aus einer Datei ist genauso einfach wie Lesen aus einem Datenstrom oder einer URL-Quelle:


// Lesen von Datei

File date = new File( "bild.gif" );
Image image = ImageIO.read( datei );

// Lesen aus einer Datenquelle, etwa Server

InputStream is = new BufferedInputStream( socket.getInputStream() );
image = ImageIO.read( is );

// Lesen von einer URL gegebenen Quelle

URL url = new URL( "http://hostname.com/image.gif"; );
image = ImageIO.read( url );

Diese Bilder können auf unterschiedliche Arten weiterverarbeitet werden. Sie lassen sich in der paint()-Methode über drawImage() anzeigen und auch als Grafiken in Swing verwenden. Zwar fordert Swing sie als ImageIcon an, doch die Klasse ist so gütig, einen Konstruktor anzubieten, der ein Image-Objekt akzeptiert.


Galileo Computing

14.9.11 Schreiben mit ImageIO  downtop

So wie die statische Funktion read() eine Grafik liest, so schreibt write() sie im PNG oder JPG-Format. Vorraussetzung ist eine Grafik, die als RenderedImage vorliegt. Die Schnittstelle wird zum Beispiel von BufferedImage implementiert, der wichtigsten Klasse für Bildinformationen.


BufferedImage bi = new BufferedImage( 100, 100, BufferedImage.TYPE_INT_RGB );
Graphics2D g2d = bufferedImage.createGraphics();
g2d.fillOval( 0, 0, 100, 100 );
g2d.dispose();

In bi liegt nun eine Grafik der Ausmaße 100x100 mit einem gefüllten Kreis. Gilt es ihn abzuspeichern, wird die Funktion write() mit einem Verweis auf das RenderedImage sowie dem Datenformat und einem File-Objekt aufgerufen.


String typ = "png";
File datei = new File( "newimage.".concat(typ) );
ImageIO.write( rendImage, typ, datei );

ImageIO erlaubt standardmäßig das Speichern in JPG und PNG; GIF ist aus Lizenzgründen problematisch. Eine Liste der unterstützten Formate liefert ImageIO.getWriterFormatNames() beziehungsweise ImageIO.getWriterMIMETypes().


String[] types = ImageIO.getWriterMIMETypes();
System.out.println( Arrays.asList(types) ); 
// [image/png, image/jpeg, image/x-png]

Galileo Computing

14.9.12 Kann ImageIO ein Format behandeln?  downtop

Ob ein Grafikformat gelesen werden kann, bestimmt im Grunde getImageReadersByFormatName(), denn die Funktion liefert eine Liste von ImageReader-Objekten, die das Format übernehmen würden. Da die Liste über einen Iterator gegeben ist, kann die Frage, ob ImageIO ein bestimmtes Format lesen kann, über das Ergebnis von getImageReadersByFormatName().hasNext() beantwortet werden. Soll für eine Endung die Möglichkeit des Lesens erfragt werden, liefert canReadExtension() die Antwort – auch wieder über einen Iterator:


public static boolean canReadExtension( String ext )
{
  return ImageIO.getImageReadersBySuffix(ext).iter.hasNext();
}

getImageReadersByMIMEType() liefert einen Iterator der MIME-Typen für Grafik-Leser.

Die Anfragemöglichkeit gibt es natürlich nicht nur für die Leser, sondern äquivalent auch für die Schreiber. Hier erfüllen getImageWritersByFormatName(), getImageWritersBySuffix() und getImageWritersByMIMEType() ihren Zweck.

Die Anfragetypen richten sich bisher nach den Dateiendungen oder MIME-Typen. Diese Aussagen erfordern aber Unterstützung vom Datensystem oder vom Server. Was ist, wenn eine Grafik über das Netzwerk übertragen wird, die Typinformationen aber fehlen? Dann helfen Funktionen wie getImageReadersBySuffix() nicht, sondern eine inhaltliche Analyse muss her. Helfende Hand ist die Funktion createImageInputStream(), die drei Datengeber analysieren kann:

File-Objekte, lesbare RandomAccessFile-Objkete und InputStream-Objekte. Da die Entwickler nun aber nicht drei unterschiedliche Funktionen mit unterschiedlichen Parametern für createImageInputStream() vorsehen wollten, nahmen sie die Oberklasse – das ist Object.


ImageInputStream iis = ImageIO.createImageInputStream( o );

Die Rückgabe ist ein ImageInputStream, der, obwohl er InputStream im Namen trägt, kein Eingabestrom im klassichen Sinne ist. ImageInputStream erlaubt einen Datenzugriff mit wahlfreier Positionierung und createImageInputStream() ist die Fabrik-Funktion, die den ImageInputStream für eine Datenquelle liefert. Für Benutzer ist ImageInputStream aber immer noch nicht gedacht; Benutzer arbeiten mit ImageReader-Objekten. Ein passendes ImageReader-Objekt für die Bytes liefert getImageReaders().


Iterator = ImageIO.getImageReaders( iis );

Der Iterator liefert alle ImageReader, die das Datenformat für den Binärstrom verarbeiten können. Uns reicht der erste:


if ( iter.hasNext() )
  ImageReader reader = (ImageReader) iter.next();
else
  // Keiner Reader, der das Format versteht.

Galileo Computing

14.9.13 Komprimieren mit ImageIO  downtop

Die statischen Funktionen ImageIO.write() und ImageIO.read() sind nur Hilfsfunktionen, die im Hintergrund einen passenden ImageWriter und ImageReader suchen und ihm die Arbeit überlassen. Während der Kontakt zu dem tatsächlichen ImageReader eher selten ist, gibt es einen guten Grund, sich mit dem schreibenden ImageWriter näher zu beschäftigen – ihm können über ein ImageWriteParam-Objekt zusätzliche Parameter übertragen werden, etwa der Kompressionsgrad, der sich zwischen 0.0f und 1.0f bewegt.

Ein Beispiel, welches ein Jpg in eine Datei schreibt, muss zunächst einen ImageWriter erfragen und anschließend den ImageOutputStream, um die Daten schreiben zu können. Nach dem Aufbau der Parameter über ein gefülltes ImageWriteParam-Objekt lässt sich das Bild speichern. Zwar verfügt ImageWriter über eine Funktion write(RenderedImage), um zum Beispiel ein BufferedImage zu schreiben, doch im Fall der Parameter muss das Bild als IIOImage vorliegen. IIOImage versammelt die Bildinformationen (RenderedImage oder Raster), zusammen mit Vorschaubild und Metadaten.


IIOImage iioi = new IIOImage( renderedImage, null, null );

Damit ist write() nun glücklich und kann die Daten schreiben. Anschließend werden die Datenströme geschlossen, und das Bild ist auf der Platte.


File ausgabe = ...

Iterator iter = ImageIO.getImageWritersByFormatName( "jpg" );

if ( iter.hasNext() )
{
  ImageWriter writer = (ImageWriter) iter.next();

  ImageOutputStream ios = ImageIO.createImageOutputStream( ausgabe );
  writer.setOutput(ios);

  ImageWriteParam iwparam = new ImageWriteParam();
  iwparam.setCompressionMode( ImageWriteParam.MODE_EXPLICIT ) ;
  iwparam.setCompressionQuality( compressionQuality );

  writer.write( null, new IIOImage(rendImage, null, null), iwparam );

  ios.flush();
  writer.dispose();
  ios.close();
}
else
  // Kein Writer für das Format gefunden.

Galileo Computing

14.9.14 Bilder im GIF-Format speichern  downtop

Java bietet uns als nette Hilfe das Laden von GIF- und JPG-kodierten Grafiken an. Leider blieben Routinen zum Speichern in dem einen oder anderen Dateityp auf der Strecke – und auch erst seit Java 1.2 hilft uns die Klasse JPEGImageEncoder beim Sichern von JPGs. Doch ist das Laden von GIF-Dateien überhaupt gestattet? Da UNISYS das Patent auf den Kompressionsalgorithmus Welch-LZW für GIF-Dateien hält, ist es eine rechtliche Frage, ob wir UNISYS Geld für das Laden von GIF-Dateien zum Beispiel aus Applets bezahlen müssen. Auf die an UNISYS gestellte Frage »If I make an applet (for profit) which loads a GIF image using the Java API function, will I need a license from you?« antwortet Cheryl D. Tarter von UNISYS: »Yes, you need a license from Unisys«. Das heißt im Klartext, dass eigentlich alle bezahlen müssten. Eine weitere Anfrage an die für Lizenzen zuständige Stelle bestätigte dies. Mit einer Klage seitens UNISYS ist jedoch nicht zu rechnen, und beim Lesen von GIF-Dateien ist somit keine Gefahr zu erwarten. Wer jedoch Bibliotheken zum Schreiben von LZW-komprimierten GIF-Dateien anbietet, sollte vorsichtig sein. Der Patentinhaber ist im Jahr 2000 dazu übergegangen, von Betreibern von Web-Seiten pauschal 5.000 Dollar Lizenzgebühren einzufordern, wenn sie nicht nachweisen können, dass die verwendeten GIF-Grafiken mit lizensierter Software erstellt wurden. Eine nette Web-Seite zu dem Thema findet sich unter http://burnallgifs.org/.

Der GIFEncoder von Adam Doppelt

Bei der schwierigen Lizenzfrage von GIF ist es verständlich, wenn auch nicht tröstend, dass wir einmal eine Routine brauchen. Um Problemen aus dem Weg zu gehen, hat Sun also gleich die Finger von einer GIF-sicheren Routine gelassen beziehungsweise hat eine Speicherroutine ohne Komprimierung implementiert. Um dennoch ohne zusätzliche Bibliotheken eine GIF-Datei im GIF87a-Format non-interlaced zu sichern, hat Adam Doppelt (E-Mail: amd@marimba.com) die Klasse GIFEncoder geschrieben, die es gestattet, beliebige Image-Objekte oder Bytefelder zu speichern. Die Klasse liegt zum Beispiel unter http://www.gurge.com/amd/old/java/GIFEncoder/index.html.

Um Daten zu sichern, wird ein Exemplar der GIFEncoder-Klasse angelegt. Die Klasse besitzt zwei Konstruktoren, wobei entweder ein geladenes Image-Objekt gesichert werden kann oder drei Felder mit den RGB-Werten. Über die Write()-Funktion der Klasse wird die Datei dann in einen Ausgabestrom geschrieben. Dieser sollte gepuffert sein, da die Kodierung ohnehin schon lange genug dauert. Folgende Zeilen leisten das Gesuchte:


  GIFEncoder encode = new GIFEncoder( image );  
OutputStream output = new BufferedOutputStream(
                        new FileOutputStream( "image.gif" ) );
  encode.Write  ( output );

Da beim herkömmlichen GIF-Format die Bilder nicht mehr als 256 Farben besitzen können (GIF24 behebt das Problem, ist aber nicht sehr verbreitet), müssen 24-Bit-Grafiken umgewandelt werden. Hier wird ein Quantization-Algorithmus verwendet. Eine Referenz findet der Leser auf der Web-Seite von Adam Doppelt. Die API-Dokumentation ist jedoch hier etwas widersprüchlich, da der Autor angibt, ein Bild mit mehr als 256 Farben würde eine AWTException ergeben.



class   GIFEncoder  

gp  GIFEncoder( byte r[][], byte g[][], byte b[][] )
Erzeugt ein GIFEncoder-Objekt aus drei Feldern mit getrennten roten, grünen und blauen Farben. Somit bezieht sich etwa r[x][y] auf die Rotintensität des Pixels in der Spalte x und Zeile y.
gp  GIFEncoder( Image image )
Erzeugt ein GIFEncoder-Objekt aus einem Image-Objekt.
gp  void Write( OutputStream out ) throws IOException
Schreibt das Bild in den Datenstrom.

Listing 14.16   giftest.java


import java.awt.*;
import java.io.*;
import java.net.*;

// This app will load the image URL given as the first argument, and

// save it as a GIF to the file given as the second argument. Beware
// of not having enough memory!

public class giftest
{
    public static void main(String args[]) throws Exception {
        if (args.length != 2) {
            System.out.println("giftest [url to load] [output file]");
            return;
        }

        // need a component in order to use MediaTracker
         Frame f = new Frame("GIFTest");
        // load an image
        Image image = f.getToolkit().getImage(new URL(args[0]));

        // wait for the image to entirely load
        MediaTracker tracker = new MediaTracker(f);
        tracker.addImage(image, 0);
        try
            tracker.waitForID(0);
        catch (InterruptedException e);
        if (tracker.statusID(0, true) != MediaTracker.COMPLETE)
            throw new AWTException("Could not load: "+args[0]+" "+
                                   tracker.statusID(0, true));

        // encode the image as a GIF
        GIFEncoder encode = new GIFEncoder(image);
        OutputStream output = new BufferedOutputStream(
            new FileOutputStream(args[1]));
        encode.Write(output);

        System.exit(0);
    }
}

Ganz unproblematisch ist die Klasse von Adam Doppelt nicht. Da Image-Objekte vollständig im Speicher liegen müssen, bekommt GIFEncoder schon mal Probleme mit großen Bildern. So kann etwa folgende Fehlermeldung auftreten:


java.awt.AWTException: Grabber returned false: 192.

Galileo Computing

14.9.15 Gif speichern mit dem ACME-Paket  downtop

Jef Poskanzer, bekannt ist auch seine Firma ACME Laboratories, hat ebenfalls einen GIF- und PPM-Konverter veröffentlicht. Eine Beschreibung des GIF-Konverters im JavaDoc-Format liegt unter http://www.acme.com/java/software/Acme.JPM.Encoders.GifEncoder.html; und für das PPM-Format heißt die HTML-Datei Acme.JPM.Encoders.PpmEncoder. html. Auf den Seiten finden sich auch die Links zum Downloaden der Java-Klassen. Diese liegen im Quellcode vor und müssen von uns compiliert werden. Der Vorteil ist, dass wir die Paketanweisung ändern können, so dass die Klasse auf unsere Paket-Struktur angepasst werden kann. So schön die Klasse auch ist, sie hängt leider noch von der Klasse ImageEncoder ab, so dass hier gleich mehrere Klassen installiert werden müssen. Die Alternative von Adam Doppelt bietet den Vorteil, dass hier nur eine Klasse eingesetzt wird. Die ACME-Klassen haben jedoch den Vorteil, dass das Bild auch von einem ImageProducer erzeugt werden kann und das Bild dann auch interlaced sein darf.


Galileo Computing

14.9.16 JPEG-Dateien mit dem Sun-Paket schreiben  downtop

Da es rechtliche Probleme mit dem GIF-Format beim Schreiben gibt, wollte Sun keine Lizenzen zahlen und hat sich gegen Schreibmethoden entschieden. JPEG dagegen ist vom Komitee Joint Photographic Experts Group als freies Format für Bildkompressionen entworfen worden. Daher haben sich die Entwickler der Java-Bibliotheken für JPEG-Klassen zum Kodiereren und Enkodieren (auch Dekodieren genannt) entschieden.

Neben der Möglichkeit, unter Java 1.4 mit ImageIO zu arbeiten, gibt es einen zweiten – etwas inoffiziellen – Weg über das Paket com.sun.image.codec.jpeg, das bei einem Sun Java SDK (und Lizenznehmern) seit Version 1.2 existiert. Alternative Bibliotheken sind dann nicht mehr nötig. Und da auch JPG nichtkomprimierend (unter Umständen immer noch mit einer minimalen Farbverfälschung) speichern kann, bietet es sich als Alternative zu GIF an.

JPEGCodec

Damit wir mit JPEG-Bildern arbeiten können, benötigen wir einen Decoder. Dazu liefert die Fabrik-Methode JPEGCodec.createJPEGEncoder() ein JPEGImageEncoder-Objekt. JPEGImageEncoder selbst ist eine Schnittstelle, die JPEG-Dateien liest oder im Fall von JPEG ImageDecoder schreibt. Dazu verwendet die Klasse intern einen Datenpuffer, der vom Typ BufferedImage sein muss. Transparenz ist für die Bilder nicht erlaubt. Mit einem konkreten Objekt können dann die Image-Daten geschrieben werden. Dafür ist nur ein beliebiges OutputStream-Objekt nötig.


JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder( out );
encoder.encode( img );

Diese beiden Zeilen schreiben ein JPEG. Mit ImageIO ist das auch nicht viel einfacher.

Qualität der Kompression

JPEG-Bilder sind im Gegensatz zu GIF-Bildern verlustkomprimiert, doch diese Verluste lassen sich klein halten. Über eine diskrete Kosinustransformation werden 8 x 8 große Pixelblöcke vereinfacht. Die Komprimierung nutzt die Unfähigkeit des Auges aus, Farbunterschiede so stark wahrzunehmen wie Helligkeitsunterschiede. So können Punkte, die eine ähnliche Helligkeit, aber eine andere Farbe besitzen, zu einem Wert werden. Bei einer hohen Kompression treten so genannte Artefakte (engl. degradation) auf, die unschön wirken. Bei einer sehr hohen Kompression ist das Bild sehr klein (aber auch hässlich).

Um nun noch die Qualität des Bilds einzustellen, wird eine Schnittstelle JPEGEncodeParam eingeführt. Das Encoder-Objekt bietet die Methode getDefaultJPEGEncodeParam() an, mit der wir an die Standardparameter kommen. Das Einstellen der Qualität geht über die Methode setQuality(qualiy, true).


JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam( img );
param.setQuality( qualiy, true );

Der Qualitätsfaktor ist ein Float und kann sich zwischen 0 und 1 bewegen. 1 bedeutet im Prinzip keine Kompression und somit höchste Qualität. Ein Wert um 0,75 ist ein hoher Wert für Qualitätsbilder, der Wert 0,5 für mittlere Bilder und 0,25 für stärkere Artefakte und hohe Kompression.

Bilder in verschiedenen Kompressionsstufen speichern

Wir wollen nun ein Programm entwickeln, das eine Zufallsgrafik aus gefüllten Rechtecken erzeugt und in den Qualitätsstufen 1,0 bis 0,0 in 0,25-Schritten speichert.

Listing 14.17   CodecDemo.java


import java.io.*;
import java.awt.*;
import java.text.*;
import java.awt.image.*;
import com.sun.image.codec.jpeg.*;

class JPEGCodecDemo
{
  public static void main( String args[] ) throws Exception
  {
    int n = 400;

    BufferedImage img = new BufferedImage( n, n,
                              BufferedImage.TYPE_INT_RGB );
    // Placebografik anlegen

    Graphics g = img.getGraphics();

    g.setColor( Color.WHITE );
    g.fillRect( 0, 0, n1, n1 );

    for ( int i=0; i < 100; i++ )
    {
      g.setColor( new Color( (int)(Math.random()*256),
                 (int)(Math.random()*256), (int)(Math.random()*256) ) );

      g.fillRect( (int)(Math.random()*n), (int)(Math.random()*n),
                    (int)(Math.random()*n/2), (int)(Math.random()*n/2) );
    }

    g.dispose();

    // Bild in ein Array schreiben

    int size = 0;

    for ( float quality = 1f; quality >= 0; quality -= 0.25 )
    {
      ByteArrayOutputStream out = new ByteArrayOutputStream( 0xfff );

      JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder( out );

      JPEGEncodeParam param;
      param = encoder.getDefaultJPEGEncodeParam( img );

      param.setQuality( quality, true );
      encoder.encode( img, param );

      FileOutputStream fos = new FileOutputStream("JPG"+quality+".jpg");
      fos.write( out.toByteArray() );
      fos.close();
      out.close();

      System.out.print( "Quality: " + quality +
                        " Size: " + out.size() + "k " +
                        " Ratio: " );

      size = (size == 0 ) ? size = out.size() : size ;

      DecimalFormat df = new DecimalFormat( "##.##%" );
      float ratio = (float)out.size()/size;
      System.out.println( df.format(ratio) );
    }
  }
}

Die Ausgabe des Programms für ein Bild ist etwa Folgende:


Quality: 1.0 Size: 34636k  Ratio: 100  %
Quality: 0.75 Size: 14573k  Ratio: 42,07  %
Quality: 0.5 Size: 11366k  Ratio: 32,82  %
Quality: 0.25 Size: 8586k  Ratio: 24,79  %
Quality: 0.0 Size: 4336k  Ratio: 12,52  %

Da die Zufallsgrafik immer anders aussieht, werden natürlich auch die Dateigrößen immer anders aussehen. Es lässt sich ablesen, dass beispielsweise eine Datei mit einem Qualitätsfaktor 0,75 etwa 42  % der Größe der Ursprungsdatei entspricht.


Galileo Computing

14.9.17 Java Image Management Interface (JIMI)  toptop

Ursprünglich vertrieb Activated Intelligence das Paket, doch Sun stellt es für die Allgemeinheit unter http://java.sun.com/products/jimi/ zur Verfügung. Die von JIMI unterstützten Formate sind vielfältig: Activated Pseudo Format (APF), BMP, Windows .ico-Format (CUR und ICO), GIF (nicht komprimierend), JPEG, Windows .pcx-Format für Paintbrush-Dateien (PCX), Portable Network Graphics (PNG), PICT, Adobe Photoshop (PSD), Sunraster, Targa (TGA), Tag Image File Format (TIFF), X-BitMap und X-Pixmap (XBM, XPM). Nicht für alle Formate gibt es gleichfalls Dekodierer und Kodierer. Ein Teil der Kodierer und Dekodierer befindet sich schon in der Java Advanced Imaging API. Das Paket in der JAI ist com.sun.media.jai.codec. Längerfristig stellt sich die Frage, ob JIMI in JAI integriert wird oder ob es ein Extrapaket bleiben wird.


Beispiel   Eine Photoshop-Datei soll geladen und als PNG-Grafik gespeichert werden.

Image image = Jimi.getImage("rein_damit.psd" );
Jimi.putImage( image, "alles_raus.png" );

Die Installation einer Java-Bibliothek ist immer ganz einfach, so auch bei der JIMI-Bibliothek. Die Datei Jimi/JimiProClasses.zip muss im Pfad aufgenommen werden, und dann können die Klassen schon in den Java-Programmen genutzt werden.

Listing 14.18   JimiDemo.java


import java.awt.*;
import java.awt.image.*;
import com.sun.jimi.core.Jimi;

public class JimiDemo
{
  public static void main( String args[] ) throws Exception
  {
    // Bild erzeugen

    BufferedImage image = new BufferedImage( 500, 500,
                            BufferedImage.TYPE_3BYTE_BGR );

    // Bild bemalen

    Graphics g = image.getGraphics();

    for ( int i=0; i<2000; i++ )
    {
      int x = rand(500), y=rand(500);

      g.setColor( new Color(rand(256*32)) );
      g.drawRect( x, y, rand(500)-x, rand(500)-y );
    }

    g.dispose();


    // Bild speichern

    String mimes[] = { "bmp", "pcx", "png", "psd", "tga", "xbm" };
    // "jpg" funktioniert so nicht.
    // für gif, tiff gibt es keinen Encoder
    // xpm kodiert nur palettenbasierte Grafiken

    for ( int i = 0; i < mimes.length; i++ )
    {
      String mime = "image/" + mimes[i];
      String filename = "JimiDemoGfx." + mimes[i];

      System.out.print( "Saving " + filename + "..." );
      Jimi.putImage( mime, image, filename );
      System.out.println( "done" );
    }
    System.exit( 0 );
  }

  private static int rand( int max )
  {
    return (int) (Math.random()*max);
  }
}





1   Benannt nach den Erfindern Lempel, Ziv und Welch.

2   Das große »W« ist kein Tippfehler.





Copyright © Galileo Press GmbH 2004
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de