CONTENTS | PREV | NEXT JavaTM Image I/O API Guide


4.4 Writing Writer Plug-ins

MyFormatImageWriterSpi
The MyFormatImageWriterSpi call plays a similar role to the MyFormatImageReaderSpi class discussed in the previous section. However, instead of being responsible for determining whether a given stream can be read, it must deterine whether an image in memory can be written. Rather than inspecting the image itself, an ImageTypeSpecifier is used so that writers may be selected before an actual image is available.

package com.mycompany.imageio;

import java.io.IOException;
import java.util.Locale;
import javax.imageio.ImageWriter;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageInputStream;

public class MyFormatImageWriterSpi extends ImageWriterSpi {

	static final String vendorName = "My Company";
	static final String version = "1.0_beta33_build9467";
	static final String writerClassName =
		"com.mycompany.imageio.MyFormatImageWriter";
	static final String[] names = { "myformat" };
	static final String[] suffixes = { "myf" };
	static final String[] MIMETypes = { "image/x-myformat" };
	static final String[] readerSpiNames = {
		"com.mycompany.imageio.MyFormatImageReaderSpi" };
    
	static final boolean supportsStandardStreamMetadataFormat = false;
	static final String nativeStreamMetadataFormatName = null;
	static final String nativeStreamMetadataFormatClassName = null;
	static final String[] extraStreamMetadataFormatNames = null;
	static final String[] extraStreamMetadataFormatClassNames = null;
	static final boolean supportsStandardImageMetadataFormat = false;
	static final String nativeImageMetadataFormatName =
		"com.mycompany.imageio.MyFormatMetadata_1.0";
	static final String nativeImageMetadataFormatClassName =
		"com.mycompany.imageio.MyFormatMetadata";
	static final String[] extraImageMetadataFormatNames = null;
	static final String[] extraImageMetadataFormatClassNames = null;
    
	public MyFormatImageWriterSpi() {
		super(vendorName, version,
		      names, suffixes, MIMETypes,
		      writerClassName,
		      STANDARD_OUTPUT_TYPE, // Write to ImageOutputStreams
		      readerSpiNames,
		      supportsStandardStreamMetadataFormat,
		      nativeStreamMetadataFormatName,
		      nativeStreamMetadataFormatClassName,
		      extraStreamMetadataFormatNames,
		      extraStreamMetadataFormatClassNames,
		      supportsStandardImageMetadataFormat,
		      nativeImageMetadataFormatName,
		      nativeImageMetadataFormatClassName,
		      extraImageMetadataFormatNames,
		      extraImageMetadataFormatClassNames);
	}

	public boolean canEncodeImage(ImageTypeSpecifier imageType) {
		int bands = imageType.getNumBands();
		return bands == 1 || bands == 3;
	}
    
	public String getDescription(Locale locale) {
		// Localize as appropriate
		return "Description goes here";
	}

	public ImageWriter createWriterInstance(Object extension) {
	    return new MyFormatImageWriter(this);
	}
}
MyFormatImageWriter

package com.mycompany.imageio;

import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.Iterator;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;

public class MyFormatImageWriter extends ImageWriter {

	ImageOutputStream stream = null;

	public MyFormatImageWriter(ImageWriterSpi originatingProvider) {
		super(originatingProvider);
	}
 
	public void setOutput(Object output) {
		super.setOutput(output);
		if (output != null) {
			if (!(output instanceof ImageOutputStream)) {
				throw new IllegalArgumentException
					("output not an ImageOutputStream!");
			}
			this.stream = (ImageOutputStream)output;
		} else {
			this.stream = null;
		}
	}

The ImageWriteParam returned by getDefaultWriteParam must be customized based on the writer's capabilities. Since this writer does not support tiling, progessive encoding, or compression, we pass in values of false or null as appropriate:

	// Tiling, progressive encoding, compression are disabled by default
	public ImageWriteParam getDefaultWriteParam() {
		return new ImageWriteParam(getLocale());
	}

The format only handles image metadata. The convertImageMetadata method does very little; it could be defined to interpret the metadata classes used by other plug-ins.

	public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
		return null;
	}

	public IIOMetadata
		getDefaultImageMetadata(ImageTypeSpecifier imageType,
		                        ImageWriteParam param) {
		return new MyFormatMetadata();
	}

	public IIOMetadata convertStreamMetadata(IIOMetadata inData,
	                                         ImageWriteParam param) {
		return null;
	}

	public IIOMetadata convertImageMetadata(IIOMetadata inData,
	                                        ImageTypeSpecifier imageType,
	                                        ImageWriteParam param) {
		// We only understand our own metadata
		if (inData instanceof MyFormatMetadata) {
			return inData;
		} else {
			return null;
		}
	}

The actual writing of the image requires first applying the source region, source bands, and subsampling factors from the ImageWriteParam. The source region and source bands may be handled by creating a child Raster. For simplicity, we extract a single Raster from the source image. If the source image is tiled, we can save memory by extracting smaller Rasters as needed.

	public void write(IIOMetadata streamMetadata,
	                  IIOImage image,
	                  ImageWriteParam param) throws IIOException {
		RenderedImage im = image.getRenderedImage();

		Rectangle sourceRegion =
			new Rectangle(0, 0, im.getWidth(), im.getHeight());
		int sourceXSubsampling = 1;
		int sourceYSubsampling = 1;
		int[] sourceBands = null;
		if (param != null) {
			sourceRegion =
				sourceRegion.intersection(param.getSourceRegion());
			sourceXSubsampling = param.getSourceXSubsampling();
			sourceYSubsampling = param.getSourceYSubsampling();
			sourceBands = param.getSourceBands();

			int subsampleXOffset = param.getSubsamplingXOffset();
			int subsampleYOffset = param.getSubsamplingYOffset();
			sourceRegion.x += subsampleXOffset;
			sourceRegion.y += subsampleYOffset;
			sourceRegion.width -= subsampleXOffset;
			sourceRegion.height -= subsampleYOffset;
		}

		// Grab a Raster containing the region of interest
		int width = sourceRegion.width;
		int height = sourceRegion.height;
		Raster imRas = im.getData(sourceRegion);
		int numBands = imRas.getNumBands();

		// Check that sourceBands values are in range
		if (sourceBands != null) {
			for (int i = 0; i < sourceBands.length; i++) {
				if (sourceBands[i] >= numBands) {
					throw new IllegalArgumentException("bad band!");
				}
			}
		}

		// Translate imRas to start at (0, 0) and subset the bands
		imRas = imRas.createChild(sourceRegion.x, sourceRegion.y,
		                          width, height,
		                          0, 0,
		                          sourceBands);

		// Reduce width and height according to subsampling factors
		width = (width + sourceXSubsampling - 1)/sourceXSubsampling;
		height = (height + sourceYSubsampling - 1)/sourceYSubsampling;

		// Assume 1 band image is grayscale, 3 band image is RGB
		int colorType;
		if (numBands == 1) {
			colorType = MyFormatImageReader.COLOR_TYPE_GRAY;
		} else if (numBands == 3) {
			colorType = MyFormatImageReader.COLOR_TYPE_RGB;
		} else {
			throw new IIOException("Image must have 1 or 3 bands!");
		}
Once the image dimensions and color type of the image have been ascertained, the plug-in is ready to write the file header:

		try {
			byte[] signature = {
				(byte)'m', (byte)'y',  (byte)'f', (byte)'o',
				(byte)'r', (byte)'m',  (byte)'a', (byte)'t'
			};
			// Output header information
			stream.write(signature);
			stream.write(`\n');
			stream.writeInt(width);
			stream.writeInt(height);
			stream.writeByte(colorType);
			stream.write(`\n');
			
Next, the plug-in extracts the image metadata from the write method's IIOImage argument, and attempts to convert it into a MyFormatMetadata object by calling convertImageMetadata. If the result is non-null, the keywords and values are extracted from the metadata and written to the output:

			// Attempt to convert metadata, if present
			IIOMetadata imd = image.getMetadata();
			MyFormatMetadata metadata = null;
			if (imd != null) {
				ImageTypeSpecifier type =
					ImageTypeSpecifier.createFromRenderedImage(im);
				metadata =
					(MyFormatMetadata)convertImageMetadata(imd,
					                                       type,
					                                       null);
			}

			// Output metadata if present
			if (metadata != null) {
				Iterator keywordIter = metadata.keywords.iterator();
				Iterator valueIter = metadata.values.iterator();
				while (keywordIter.hasNext()) {
					String keyword = (String)keywordIter.next();
					String value = (String)valueIter.next();
					
					stream.writeUTF(keyword);
					stream.write(`\n');
					stream.writeUTF(value);
					stream.write(`\n');
				}
			}
			stream.writeUTF("END");
			stream.write(`\n');
Finally, the plug-in is ready to begin writing the pixel data. The image Raster is copied into an int array, one row at a time using the getPixels method. Then these values are subsampled using the horizontal subsampling factor, and copied into a byte array, which is written to the output with a single write call. The source row is then incremented by the vertical subsampling factor until the end of the source region is reached, and the output stream is flushed:

			// Output (subsampled) pixel values
			int rowLength = width*numBands;
			int xSkip = sourceXSubsampling*numBands;
			int[] rowPixels = imRas.getPixels(0, 0, width, 1,
			                                  (int[])null);
			byte[] rowSamples = new byte[rowLength];

			// Output every (sourceYSubsampling)^th row
			for (int y = 0; y < height; y += sourceYSubsampling) {
				imRas.getPixels(0, y, width, 1, rowPixels);

				// Subsample horizontally and convert to bytes
				int count = 0;
				for (int x = 0; x < width; x += xSkip) {
					if (colorType ==
						MyFormatImageReader.COLOR_TYPE_GRAY) {
						rowSamples[count++] = (byte)rowPixels[x];
					} else {
						rowSamples[count++] = (byte)rowPixels[x];
						rowSamples[count++] =
							(byte)rowPixels[x + 1];
						rowSamples[count++] =
							(byte)rowPixels[x + 2];
					}
				}

				// Output a row's worth of bytes
				stream.write(rowSamples, 0, width*numBands);
			}
			stream.flush();
		} catch (IOException e) {
			throw new IIOException("I/O error!", e);
		}
	}
}


CONTENTS | PREV | NEXT
Copyright © 2001 Sun Microsystems, Inc. All Rights Reserved.