package org.libtiff.jai.codec; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferShort; import java.awt.image.DataBufferUShort; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.awt.image.renderable.ParameterBlock; import java.io.IOException; import javax.media.jai.JAI; import javax.media.jai.RasterFactory; import org.libtiff.jai.util.JaiI18N; /** * Provides a base class for writing TIFF tile codecs, to be registered with the * XTIFFDirectory. This codec allows for both decoding and (optionally) encoding * of tiles, and also handles the colorspace conversion in decoding. * <p> * At the minimum you will need to implement the two methods decodeTilePixels() * for byte and short data, as well as the methods register() and create(). If * your decoder requires additional parameters from the tags, set them up in * initializeDecoding(), and initializeEncoding() for encoding. * <p> * To implement encoding, you must override the canEncode() method to return * true, and implement encodeTilePixels(). * * @author Niles Ritter * @see XTIFFTileCodec */ public abstract class XTIFFTileCodecImpl implements XTIFFTileCodec { // //////////////////////////////////////////////////// // // Implementation Section // // Override or implement methods here // //////////////////////////////////////////////////// /** * Registration method. Must be implemented by the extended class to * register itself with the XTIFFDirectory for all compression codes it * supports (e.g Fax codec supports 3 codes). * * @see XTIFFDirectory */ public abstract void register(); /** * Implement this to return the corresponding empty codec object. */ public abstract XTIFFTileCodec create(); /** * Indicate whether this codec can encode data. Override to return true only * if your codec implments encoding. */ public boolean canEncode() { return false; } /** * The initialization method particular to decoding. Extend for whatever * compression-specific information or parameters is needed. The decoding * parameter has already been assigned at this point, as well as the * XTIFFDirectory parsed from the input stream, and so all XTIFFFields are * available. */ public void initializeDecoding() {} /** * The initialization method particular to encoding. Extend for whatever * compression-specific information or parameters is needed. The decoding * parameter has already been assigned at this point, as well as the * XTIFFDirectory parsed from the input stream, and so all XTIFFFields are * available. */ public void initializeEncoding() {} /** * decode bpixel byte array of data into pixels, packed for 1,2,4 8 bit * pixels. Must implment this. * * @param bpixels the byte array of compressed input data * @param rect the rectangular shape of the target pixels * @param pixels the target decompressed pixels. */ public abstract void decodeTilePixels(byte[] bpixels, Rectangle rect, byte[] pixels); /** * decode bpixel byte array of data into pixels, packed for 16 bit pixels. * Must implment this. * * @param bpixels the byte array of compressed input data * @param rect the rectangular shape of the target pixels * @param pixels the target decompressed pixels. */ public abstract void decodeTilePixels(byte[] bpixels, Rectangle rect, short[] pixels); /** * encode the tile in pixels into bpixels and return the byte size of the * compressed data. Override this method if canEncode() = true; * * @param pixels input pixels * @param rect the array dimensions of samples * @param bpixels the target array of compressed byte data */ public int encodeTilePixels(int[] pixels, Rectangle rect, byte[] bpixels) { return 0; } // //////////////////////////////////////////////////// // // Common Section // //////////////////////////////////////////////////// protected XTIFFDirectory directory = null; protected RenderedImage image = null; protected int minY; protected int minX; protected int width; protected int length; protected int numBands; protected int tileLength; protected int tileWidth; protected int compression; protected SampleModel sampleModel; protected int[] sampleSize; protected char[] bitsPerSample; protected char[] colormap = null; /** * The empty constructor. */ public XTIFFTileCodecImpl() {} /** * The method for initializing information common to both encoder and * decoder. */ public void initialize() { width = (int) getLongField(XTIFF.TIFFTAG_IMAGE_WIDTH); length = (int) getLongField(XTIFF.TIFFTAG_IMAGE_LENGTH); isTiled = directory.isTiled(); if (isTiled) { tileWidth = (int) getLongField(XTIFF.TIFFTAG_TILE_WIDTH); tileLength = (int) getLongField(XTIFF.TIFFTAG_TILE_LENGTH); } else { tileWidth = width; tileLength = (int) getLongField(XTIFF.TIFFTAG_ROWS_PER_STRIP); } // Figure out what compression if any, is being used. XTIFFField compField = directory.getField(XTIFF.TIFFTAG_COMPRESSION); if (compField != null) { compression = compField.getAsInt(0); } else { compression = XTIFF.COMPRESSION_NONE; } XTIFFField cfield = directory.getField(XTIFF.TIFFTAG_COLORMAP); if (cfield != null) colormap = cfield.getAsChars(); // Read the TIFFTAG_BITS_PER_SAMPLE field XTIFFField bitsField = directory.getField(XTIFF.TIFFTAG_BITS_PER_SAMPLE); if (bitsField == null) { // Default bitsPerSample = new char[1]; bitsPerSample[0] = 1; } else { bitsPerSample = bitsField.getAsChars(); } image_type = directory.getImageType(); } /** * A common utility method for accessing the XTIFFFields in the current * image directory. */ protected long getLongField(int fld) { XTIFFField field = directory.getField(fld); if (field == null) return 0; else return field.getAsLong(0); } /** * This method may be used by the implementations register() method to * register itself with the XTIFFDirectory. * * @see XTIFFDirectory */ public void register(int comp) { XTIFFDirectory.registerTileCodec(comp, this); } /** * One-time common image parameter setup * * @param img the source image that will be encoded into a TIFF formatted * stream, or the TIFF image from which Raster tiles will be decoded. */ protected void setupSourceImage(RenderedImage img) { image = img; // Get raster parameters minY = image.getMinY(); minX = image.getMinX(); sampleModel = image.getSampleModel(); numBands = sampleModel.getNumBands(); sampleSize = sampleModel.getSampleSize(); } /** * Returns the TIFF compression type */ public int getCompression() { return compression; } // //////////////////////////////////////////////////// // // Encoding Section // //////////////////////////////////////////////////// protected XTIFFEncodeParam encodeParam = null; private int _pixels[]; protected boolean isTiled; /** * The method for creating an encoder from the XTIFFEncodeParam information. */ public XTIFFTileCodec create(XTIFFEncodeParam param) throws IOException { XTIFFTileCodecImpl codec = (XTIFFTileCodecImpl) create(); codec.initialize(param); return codec; } protected void initialize(XTIFFEncodeParam param) throws IOException { if (!canEncode()) throw new IOException("encoding not supported"); encodeParam = param; directory = param.getDirectory(); initialize(); initializeEncoding(); } /** * Encode the data into buffer and return byte count Normally you will not * need to override this method, but instead implement the * <code>encodeTilePixels()</code> method. */ public int encode(RenderedImage img, Rectangle rect, byte[] bpixels) { if (image == null) { setupSourceImage(img); setupBufferForEncoding(); } // Fill tile buffer, padding right with zeroes. getTilePixels(rect); // encode and return number of bytes compressed return encodeTilePixels(_pixels, rect, bpixels); } /** * One-time setup for encoding */ protected void setupBufferForEncoding() { // Set up input tile/strip buffer _pixels = new int[tileWidth * tileLength * numBands]; // if padding necessary do it now. int padRight = (tileWidth - (width % tileWidth)) % tileWidth; int padBottom = (tileLength - (length % tileLength)) % tileLength; if (!isTiled) padBottom = 0; if (padRight > 0 || padBottom > 0) { ParameterBlock pb = new ParameterBlock(); pb.addSource(image); pb.add(null) .add(padRight) .add(null) .add(padBottom) .add(null) .add(null); image = JAI.create("border", pb); } } /** * Get the portion of tile fitting into buffer. You probably won't need to * override this. * * @param rect the region to extract from image. */ protected void getTilePixels(Rectangle rect) { // Grab the pixels Raster src = image.getData(rect); int col = (int) rect.getX(); int row = (int) rect.getY(); int rows = (int) rect.getHeight(); int cols = (int) rect.getWidth(); src.getPixels(col, row, cols, rows, _pixels); } /** * If derived classes can make a better estimate for the maximum size of a * compressed tile, they should override this, which assumes conservatively * that it won't be worse than twice the original size. * * @param im the rendered image containing the image data */ public int getCompressedTileSize(RenderedImage im) { sampleModel = im.getSampleModel(); numBands = sampleModel.getNumBands(); sampleSize = sampleModel.getSampleSize(); return (int) Math.ceil(2 * tileWidth * tileLength * numBands * (sampleSize[0] / 8.0)); } // //////////////////////////////////////////////////// // // Decoding Section // //////////////////////////////////////////////////// protected XTIFFDecodeParam decodeParam = null; protected boolean decodePaletteAsShorts = false; protected int unitsInThisTile; protected byte _bdata[] = null; protected short _sdata[] = null; protected byte[] bpixvals = null; protected short[] spixvals = null; protected DataBuffer buffer = null; protected int dataType; protected int image_type; /** * The standard decoder creation method */ public XTIFFTileCodec create(XTIFFDecodeParam param) throws IOException { XTIFFTileCodecImpl codec = (XTIFFTileCodecImpl) create(); codec.initialize(param); return codec; } protected void initialize(XTIFFDecodeParam param) throws IOException { decodeParam = param; decodePaletteAsShorts = param.getDecodePaletteAsShorts(); directory = param.getDirectory(); initialize(); initializeDecoding(); } /** * One-time setup for encoding. Some configurations require a temp array for * unpacking 16-bit palette data. */ protected void setupBufferForDecoding() { // int length; buffer = sampleModel.createDataBuffer(); dataType = sampleModel.getDataType(); if (dataType == DataBuffer.TYPE_BYTE) { _bdata = ((DataBufferByte) buffer).getData(); bpixvals = _bdata; } else if (dataType == DataBuffer.TYPE_USHORT) { _sdata = ((DataBufferUShort) buffer).getData(); if (!decodePaletteAsShorts) spixvals = _sdata; } else if (dataType == DataBuffer.TYPE_SHORT) { _sdata = ((DataBufferShort) buffer).getData(); if (!decodePaletteAsShorts) spixvals = _sdata; } if (decodePaletteAsShorts) { int len = _sdata.length; if (bitsPerSample[0] == 16) spixvals = new short[len]; else bpixvals = new byte[len]; } } /** * Decode a rectangle of data stored in bpixels into a raster tile. Usually * you will not need to override this, but instead implement the * decodeTilePixels methods. */ public WritableRaster decode(RenderedImage img, Rectangle newRect, byte[] bpixels) { if (image == null) { setupSourceImage(img); } setupBufferForDecoding(); // set up every time unitsInThisTile = newRect.width * newRect.height * numBands; // uncompress data decodeTilePixels(bpixels, newRect); // post-processing of color data decodeColor(newRect); // put buffer into a tile return setTilePixels(newRect); } /** * Postprocess the uncompressed color data into the appropriate display * color model. This implementation Does a number of things: * <ul> * <li> For RGB color, reverse to BGR which apparently is faster for Java 2D * display * <li> For one-bit WHITE_IS_ZERO data, flip the values so that they will * look correct * <li> If the decodePaletteAsShorts flag is true then unpack the bits and * apply the lookup table, as 16-bit lookup is not supported in JAI. * </ul> * Override this if you have other color types. * * @see XTIFFDecodeParam */ protected void decodeColor(Rectangle newRect) { switch (dataType) { case DataBuffer.TYPE_BYTE: decodeColor(bpixvals, _bdata, newRect); break; case DataBuffer.TYPE_SHORT: case DataBuffer.TYPE_USHORT: if (bpixvals != null) decodeColor(bpixvals, _sdata, newRect); else decodeColor(spixvals, _sdata, newRect); } } /** * Decode a tile of data into either byte or short pixel buffers. Override * this if you have other buffer types (e.g. int) */ protected void decodeTilePixels(byte[] bpixels, Rectangle newRect) { // decodeTilePixels into the appropriate buffer if (bpixvals != null) decodeTilePixels(bpixels, newRect, bpixvals); else decodeTilePixels(bpixels, newRect, spixvals); } /** * Take the values from the buffer and store them in a WritableRaster * object. */ protected WritableRaster setTilePixels(Rectangle rect) { return (WritableRaster) RasterFactory.createWritableRaster(sampleModel, buffer, new Point((int) rect.getX(), (int) rect.getY())); } /** * A useful Method to interpret a byte array as shorts. Method depends on * whether the bytes are stored in a big endian or little endian format. */ protected void unpackShorts(byte byteArray[], short output[], int shortCount) { int j; int firstByte, secondByte; if (directory.isBigEndian()) { for (int i = 0; i < shortCount; i++) { j = 2 * i; firstByte = byteArray[j] & 0xff; secondByte = byteArray[j + 1] & 0xff; output[i] = (short) ((firstByte << 8) + secondByte); } } else { for (int i = 0; i < shortCount; i++) { j = 2 * i; firstByte = byteArray[j] & 0xff; secondByte = byteArray[j + 1] & 0xff; output[i] = (short) ((secondByte << 8) + firstByte); } } } // //////////////////////////////////////////////////////////////////////// // /// Color decoding section // //////////////////////////////////////////////////////////////////////// /** * Decode short pixel data, or interpret palette data as short from byte. */ protected void decodeColor(byte[] bpix, short[] sdata, Rectangle newRect) { // short sswap; switch (image_type) { case XTIFF.TYPE_PALETTE: if (bitsPerSample[0] == 8) { // At this point the data is 1 banded and will // become 3 banded only after we've done the palette // lookup, since unitsInThisTile was calculated with // 3 bands, we need to divide this by 3. int unitsBeforeLookup = unitsInThisTile / 3; // Expand the palette image into an rgb image with ushort // data type. int cmapValue; int count = 0, lookup, len = colormap.length / 3; int len2 = len * 2; for (int i = 0; i < unitsBeforeLookup; i++) { // Get the index into the colormap lookup = bpix[i] & 0xff; // Get the blue value cmapValue = colormap[lookup + len2]; sdata[count++] = (short) (cmapValue & 0xffff); // Get the green value cmapValue = colormap[lookup + len]; sdata[count++] = (short) (cmapValue & 0xffff); // Get the red value cmapValue = colormap[lookup]; sdata[count++] = (short) (cmapValue & 0xffff); } } else if (bitsPerSample[0] == 4) { int padding = newRect.width % 2; // int bytesPostDecoding = ((newRect.width + 1) / 2) // * newRect.height; int bytes = unitsInThisTile / 3; // Unpack the 2 pixels packed into each byte. byte[] data = new byte[bytes]; int srcCount = 0, dstCount = 0; for (int j = 0; j < newRect.height; j++) { for (int i = 0; i < newRect.width / 2; i++) { data[dstCount++] = (byte) ((bpix[srcCount] & 0xf0) >> 4); data[dstCount++] = (byte) (bpix[srcCount++] & 0x0f); } if (padding == 1) { data[dstCount++] = (byte) ((bpix[srcCount++] & 0xf0) >> 4); } } int len = colormap.length / 3; int len2 = len * 2; int cmapValue, lookup; int count = 0; for (int i = 0; i < bytes; i++) { lookup = data[i] & 0xff; cmapValue = colormap[lookup + len2]; sdata[count++] = (short) (cmapValue & 0xffff); cmapValue = colormap[lookup + len]; sdata[count++] = (short) (cmapValue & 0xffff); cmapValue = colormap[lookup]; sdata[count++] = (short) (cmapValue & 0xffff); } } else { throw new RuntimeException(JaiI18N.getString("XTIFFImageDecoder7")); } break; } } /** * Decode short color data, or interpret palette data as short. */ protected void decodeColor(short[] spix, short[] sdata, Rectangle newRect) { short sswap; switch (image_type) { case XTIFF.TYPE_GREYSCALE_WHITE_IS_ZERO: case XTIFF.TYPE_GREYSCALE_BLACK_IS_ZERO: // Since we are using a ComponentColorModel with this image, // we need to change the WhiteIsZero data to BlackIsZero data // so it will display properly. if (image_type == XTIFF.TYPE_GREYSCALE_WHITE_IS_ZERO) { if (dataType == DataBuffer.TYPE_USHORT) { for (int l = 0; l < sdata.length; l++) { sdata[l] = (short) (65535 - spix[l]); } } else if (dataType == DataBuffer.TYPE_SHORT) { for (int l = 0; l < sdata.length; l++) { sdata[l] = (short) (~spix[l]); } } } break; case XTIFF.TYPE_RGB: // Change to BGR order, as Java2D displays that faster for (int i = 0; i < unitsInThisTile; i += 3) { sswap = spix[i]; sdata[i] = spix[i + 2]; sdata[i + 2] = sswap; } break; case XTIFF.TYPE_ORGB: case XTIFF.TYPE_ARGB_PRE: case XTIFF.TYPE_ARGB: // Change from RGBA to ABGR for Java2D's faster special cases for (int i = 0; i < unitsInThisTile; i += 4) { // Swap R and A sswap = spix[i]; sdata[i] = spix[i + 3]; sdata[i + 3] = sswap; // Swap G and B sswap = spix[i + 1]; sdata[i + 1] = spix[i + 2]; sdata[i + 2] = sswap; } break; case XTIFF.TYPE_RGB_EXTRA: break; case XTIFF.TYPE_PALETTE: if (decodePaletteAsShorts) { // At this point the data is 1 banded and will // become 3 banded only after we've done the palette // lookup, since unitsInThisTile was calculated with // 3 bands, we need to divide this by 3. int unitsBeforeLookup = unitsInThisTile / 3; // Since unitsBeforeLookup is the number of shorts, // but we do our decompression in terms of bytes, we // need to multiply it by 2 in order to figure out // how many bytes we'll get after decompression. // int entries = unitsBeforeLookup * 2; if (dataType == DataBuffer.TYPE_USHORT) { // Expand the palette image into an rgb image with ushort // data type. int cmapValue; int count = 0, lookup, len = colormap.length / 3; int len2 = len * 2; for (int i = 0; i < unitsBeforeLookup; i++) { // Get the index into the colormap lookup = spix[i] & 0xffff; // Get the blue value cmapValue = colormap[lookup + len2]; sdata[count++] = (short) (cmapValue & 0xffff); // Get the green value cmapValue = colormap[lookup + len]; sdata[count++] = (short) (cmapValue & 0xffff); // Get the red value cmapValue = colormap[lookup]; sdata[count++] = (short) (cmapValue & 0xffff); } } else if (dataType == DataBuffer.TYPE_SHORT) { // Expand the palette image into an rgb image with // short data type. int cmapValue; int count = 0, lookup, len = colormap.length / 3; int len2 = len * 2; for (int i = 0; i < unitsBeforeLookup; i++) { // Get the index into the colormap lookup = spix[i] & 0xffff; // Get the blue value cmapValue = colormap[lookup + len2]; sdata[count++] = (short) cmapValue; // Get the green value cmapValue = colormap[lookup + len]; sdata[count++] = (short) cmapValue; // Get the red value cmapValue = colormap[lookup]; sdata[count++] = (short) cmapValue; } }// dataType }// decodePaletteAsShorts break; case XTIFF.TYPE_TRANS: break; } } /** * Decode byte color data */ protected void decodeColor(byte[] bpix, byte[] bdata, Rectangle newRect) { byte bswap; switch (image_type) { case XTIFF.TYPE_BILEVEL_WHITE_IS_ZERO: case XTIFF.TYPE_BILEVEL_BLACK_IS_ZERO: case XTIFF.TYPE_GREYSCALE_WHITE_IS_ZERO: case XTIFF.TYPE_GREYSCALE_BLACK_IS_ZERO: case XTIFF.TYPE_RGB_EXTRA: case XTIFF.TYPE_TRANS: // nothing break; case XTIFF.TYPE_RGB: if (bitsPerSample[0] == 8) { // Change to BGR order, as Java2D displays that faster for (int i = 0; i < unitsInThisTile; i += 3) { bswap = bpix[i]; bdata[i] = bpix[i + 2]; bdata[i + 2] = bswap; } } break; case XTIFF.TYPE_ORGB: case XTIFF.TYPE_ARGB_PRE: case XTIFF.TYPE_ARGB: if (bitsPerSample[0] == 8) { // Convert from RGBA to ABGR for Java2D for (int i = 0; i < unitsInThisTile; i += 4) { // Swap R and A bswap = bpix[i]; bdata[i] = bpix[i + 3]; bdata[i + 3] = bswap; // Swap G and B bswap = bpix[i + 1]; bdata[i + 1] = bpix[i + 2]; bdata[i + 2] = bswap; } } break; case XTIFF.TYPE_PALETTE: // break; }// switch }// decodeColor }