14.13 Grafiken speichern
14.13.1 Bilder im GIF-Format speichern
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.
|
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. |
|
GIFEncoder( Image image )
Erzeugt ein GIFEncoder-Objekt aus einem Image-Objekt. |
|
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.
14.13.2 Gif speichern mit dem ACME-Paket
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.
14.13.3 JPEG-Dateien mit dem Sun-Paket schreiben
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.
14.13.4 Java Image Management Interface (JIMI)
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.
|