Galileo Computing < openbook >
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.


Java ist auch eine Insel von Christian Ullenboom
Buch: Java ist auch eine Insel (Galileo Computing)
gp Kapitel 14 Grafikprogrammierung mit dem AWT
gp 14.1 Das Abstract-Window-Toolkit
gp 14.1.1 Java Foundation Classes
gp 14.2 Fenster unter grafischen Oberflächen
gp 14.2.1 Fenster öffnen
gp 14.2.2 Größe und Position des Fensters verändern
gp 14.2.3 Fenster- und Dialog-Dekoration
gp 14.3 Das Toolkit
gp 14.3.1 Einen Hinweis beepen
gp 14.4 Grundlegendes zum Zeichnen
gp 14.4.1 Die paint()-Methode
gp 14.4.2 Auffordern zum Neuzeichnen mit repaint()
gp 14.4.3 Fensterinhalte ändern und die ereignisorientierte Programmierung
gp 14.5 Punkte, Linien und Rechtecke aller Art
gp 14.5.1 Linien
gp 14.5.2 Rechtecke
gp 14.6 Alles was rund ist
gp 14.7 Polygone und Polylines
gp 14.7.1 Die Polygon-Klasse
gp 14.7.2 N-Ecke zeichnen
gp 14.7.3 Vollschlanke Linien zeichnen
gp 14.8 Zeichenketten schreiben
gp 14.8.1 Einen neuen Zeichensatz bestimmen
gp 14.8.2 Ableiten eines neuen Fonts aus einem gegebenen Font
gp 14.8.3 Zeichensätze des Systems ermitteln
gp 14.8.4 Die Klasse FontMetrics
gp 14.8.5 True Type Fonts
gp 14.9 Clipping-Operationen
gp 14.10 Farben
gp 14.10.1 Zufällige Farbblöcke zeichnen
gp 14.10.2 Farbanteile zurückgeben
gp 14.10.3 Vordefinierte Farben
gp 14.10.4 Farben aus Hexadezimalzahlen erzeugen
gp 14.10.5 Einen helleren oder dunkleren Farbton wählen
gp 14.10.6 Farbmodelle HSB und RGB
gp 14.10.7 Die Farben des Systems
gp 14.11 Bilder anzeigen und Grafiken verwalten
gp 14.11.1 Eine Grafik zeichnen
gp 14.11.2 Grafiken zentrieren
gp 14.11.3 Laden von Bildern mit dem MediaTracker beobachten
gp 14.11.4 Kein Flackern durch Double-Buffering
gp 14.11.5 Bilder skalieren
gp 14.12 Programm-Icon setzen
gp 14.12.1 VolatileImage
gp 14.13 Grafiken speichern
gp 14.13.1 Bilder im GIF-Format speichern
gp 14.13.2 Gif speichern mit dem ACME-Paket
gp 14.13.3 JPEG-Dateien mit dem Sun-Paket schreiben
gp 14.13.4 Java Image Management Interface (JIMI)
gp 14.14 Von Produzenten, Konsumenten und Beobachtern
gp 14.14.1 Producer und Consumer für Bilder
gp 14.14.2 Beispiel für die Übermittlung von Daten
gp 14.14.3 Bilder selbst erstellen
gp 14.14.4 Die Bildinformationen wieder auslesen
gp 14.15 Filter
gp 14.15.1 Grundlegende Eigenschaft von Filtern
gp 14.15.2 Konkrete Filterklassen
gp 14.15.3 Mit CropImageFilter Teile ausschneiden
gp 14.15.4 Transparenz
gp 14.16 Alles wird bunt mit Farbmodellen
gp 14.16.1 Die abstrakte Klasse ColorModel
gp 14.16.2 Farbwerte im Pixel mit der Klasse DirectColorModel
gp 14.16.3 Die Klasse IndexColorModel
gp 14.17 Drucken
gp 14.17.1 Drucken mit dem einfachen Ansatz
gp 14.17.2 Ein PrintJob
gp 14.17.3 Drucken der Inhalte
gp 14.17.4 Komponenten drucken
gp 14.17.5 Den Drucker am Parallelport ansprechen
gp 14.18 Java 2D-API
gp 14.18.1 Grafische Objekte zeichnen
gp 14.18.2 Geometrische Objekte durch Shape gekennzeichnet
gp 14.18.3 Eigenschaften geometrischer Objekte
gp 14.18.4 Transformationen mit einem AffineTransform-Objekt
gp 14.19 Graphic Layers Framework
gp 14.20 Grafikverarbeitung ohne grafische Oberfläche
gp 14.20.1 Xvfb-Server
gp 14.20.2 Pure Java AWT Toolkit (PJA)


Galileo Computing

14.13 Grafiken speicherndowntop


Galileo Computing

14.13.1 Bilder im GIF-Format speicherndowntop

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()-Funktion1 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( "DATEI" ) );
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.13.2 Gif speichern mit dem ACME-Paketdowntop

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.13.3 JPEG-Dateien mit dem Sun-Paket schreibendowntop

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. Sie sind (noch) nicht in den Core-APIs eingebunden, sondern liegen im Paket com.sun.image.codec.jpeg, das nur Teil des JDK beziehungsweise JRE von Sun ist und somit nur von Lizenznehmern zusätzlich angeboten wird. Alternative Bibliotheken sind dann nicht mehr nötig. Und da auch JPG nichtkomprimierend (allerdings immer noch mit einer leichten Farbverfälschung) speichern kann, bietet es sich als Alternative zu GIF an.

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. BufferedImage ist eine Erweiterung der Image-Klasse. 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. Einfacher kann dies nicht sein.

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, n-1, n-1 );
    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.13.4 Java Image Management Interface (JIMI)toptop

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.

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 Das große »W« ist kein Tippfehler.





Copyright (c) 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