package org.libtiff.jai.codecimpl; /* * XTIFF: eXtensible TIFF libraries for JAI. * * The contents of this file are subject to the JAVA ADVANCED IMAGING * SAMPLE INPUT-OUTPUT CODECS AND WIDGET HANDLING SOURCE CODE License * Version 1.0 (the "License"); You may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.sun.com/software/imaging/JAI/index.html * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. * * The Original Code is JAVA ADVANCED IMAGING SAMPLE INPUT-OUTPUT CODECS * AND WIDGET HANDLING SOURCE CODE. * The Initial Developer of the Original Code is: Sun Microsystems, Inc.. * Portions created by: Niles Ritter * are Copyright (C): Niles Ritter, GeoTIFF.org, 1999,2000. * All Rights Reserved. * Contributor(s): Niles Ritter */ import java.awt.Rectangle; import java.awt.color.ColorSpace; import java.awt.image.ColorModel; import java.awt.image.DataBuffer; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Vector; import org.libtiff.jai.codec.XTIFF; import org.libtiff.jai.codec.XTIFFDirectory; import org.libtiff.jai.codec.XTIFFEncodeParam; import org.libtiff.jai.codec.XTIFFField; import org.libtiff.jai.codec.XTIFFTileCodec; import org.libtiff.jai.util.JaiI18N; import com.sun.media.jai.codec.ImageEncodeParam; import com.sun.media.jai.codec.TIFFEncodeParam; import com.sun.media.jai.codec.TIFFField; import com.sun.media.jai.codecimpl.TIFFImageDecoder; import com.sun.media.jai.codecimpl.TIFFImageEncoder; /** * A baseline TIFF writer. The writer outputs TIFF images in either Bilevel, * Greyscale, Palette color or Full Color modes. * */ public class XTIFFImageEncoder extends TIFFImageEncoder { long firstIFDOffset = 0; XTIFFDirectory directory; XTIFFEncodeParam tparam; int width; int length; SampleModel sampleModel; int numBands; int sampleSize[]; int dataType; boolean dataTypeIsShort; ColorModel colorModel; int numTiles; int compression; boolean isTiled; long tileLength; long tileWidth; byte[] bpixels = null; long stripTileByteCounts[]; long stripTileOffsets[]; long currentOffset = 0; // Image Types public static final int XTIFF_BILEVEL_WHITE_IS_ZERO = 0; public static final int XTIFF_BILEVEL_BLACK_IS_ZERO = 1; public static final int XTIFF_PALETTE = 2; public static final int XTIFF_FULLCOLOR = 3; public static final int XTIFF_GREYSCALE = 4; /** * Standard constructor */ public XTIFFImageEncoder(OutputStream output, ImageEncodeParam param) { super(output, param); if (this.param == null || !(param instanceof XTIFFEncodeParam)) { this.param = new XTIFFEncodeParam((TIFFEncodeParam) param); } tparam = (XTIFFEncodeParam) this.param; directory = tparam.getDirectory(); } private File createTemp() throws IOException { String tmpdir = System.getProperty("tiff.io.tmpdir"); File file = null; if (tmpdir != null) file = File.createTempFile("libtiff.jai.", ".dat", new File(tmpdir)); else file = File.createTempFile("libtiff.jai.", ".dat"); file.deleteOnExit(); return file; } private void copyImageData(File tmp, OutputStream out, int total) throws IOException { int bufsize = 1024; int bytes = 0; byte[] buf = new byte[bufsize]; FileInputStream in = new FileInputStream(tmp); do { bytes = in.read(buf); out.write(buf, 0, bytes); total -= bytes; } while (total > 0); in.close(); } /** * Encodes a RenderedImage and writes the output to the OutputStream * associated with this ImageEncoder. */ public void encode(RenderedImage im) throws IOException { // Set comp into directory compression = tparam.getCompression(); // see if tiled isTiled = ((TIFFEncodeParam) param).getWriteTiled(); // Setup Directory fields. getImageFields(im); if (compression == XTIFF.COMPRESSION_NONE) { computeIFDOffset(); writeFileHeader(firstIFDOffset); currentOffset = 8; writeImageData(im, output); writeDirectory(directory.getFields(), 0); } else { // We have to write compressed data out to // a temp file to compute the IFD offset. // The only alternative is to compress the // data twice, which is just about as bad. currentOffset = 8; File tmp = null; try { tmp = createTemp(); OutputStream tmpOut = new FileOutputStream(tmp); int total = writeImageData(im, tmpOut); tmpOut.close(); writeFileHeader(currentOffset + currentOffset % 2); copyImageData(tmp, output, total); writeDirectory(directory.getFields(), 0); } finally { if (tmp != null) tmp.delete(); } } } /** * Precomputes the IFD Offset for uncompressed data. */ private void computeIFDOffset() { long bytesPerRow = (long) Math.ceil((sampleSize[0] / 8.0) * tileWidth * numBands); long bytesPerTile = bytesPerRow * tileLength; long lastTile = bytesPerTile; if (!isTiled) { // Last strip may have lesser rows long lastStripRows = length - (tileLength * (numTiles - 1)); lastTile = lastStripRows * bytesPerRow; } long totalBytesOfData = bytesPerTile * (numTiles - 1) + lastTile; // File header always occupies 8 bytes and we write the image data // after that. firstIFDOffset = 8 + totalBytesOfData; // Must begin on a word boundary if ((firstIFDOffset % 2) != 0) { firstIFDOffset++; } } private void writeFileHeader(long firstIFDOffset) throws IOException { // 8 byte image file header // Byte order used within the file - Big Endian output.write('M'); output.write('M'); // Magic value output.write(0); output.write(42); // Offset in bytes of the first IFD, must begin on a word boundary writeLong(firstIFDOffset); } // method for adding tags that haven't been set by user private void addIfAbsent(int tag, int type, int count, Object obj) { if (directory.getField(tag) == null) directory.addField(tag, type, count, obj); } private void getImageFields(RenderedImage im) /* throws IOException */{ width = im.getWidth(); length = im.getHeight(); // TIFF calls it length sampleModel = im.getSampleModel(); numBands = sampleModel.getNumBands(); sampleSize = sampleModel.getSampleSize(); dataType = sampleModel.getDataType(); if (dataType != DataBuffer.TYPE_BYTE && dataType != DataBuffer.TYPE_SHORT && dataType != DataBuffer.TYPE_USHORT) { // Support only byte and (unsigned) short. throw new Error(JaiI18N.getString("TIFFImageEncoder0")); } dataTypeIsShort = dataType == DataBuffer.TYPE_SHORT || dataType == DataBuffer.TYPE_USHORT; colorModel = im.getColorModel(); if (colorModel != null && colorModel instanceof IndexColorModel && dataTypeIsShort) { // Don't support (unsigned) short palette-color images. throw new Error(JaiI18N.getString("TIFFImageEncoder2")); } IndexColorModel icm = null; int sizeOfColormap = 0; char colormap[] = null; // Basic fields - have to be in increasing numerical order BILEVEL // ImageWidth 256 // ImageLength 257 // BitsPerSample 258 // Compression 259 // PhotoMetricInterpretation 262 // StripOffsets 273 // RowsPerStrip 278 // StripByteCounts 279 // XResolution 282 // YResolution 283 // ResolutionUnit 296 int photometricInterpretation = XTIFF.PHOTOMETRIC_RGB; int imageType = XTIFF_FULLCOLOR; // IMAGE TYPES POSSIBLE // Bilevel // BitsPerSample = 1 // Compression = 1, 2, or 32773 // PhotometricInterpretation either 0 or 1 // Greyscale // BitsPerSample = 4 or 8 // Compression = 1, 32773 // PhotometricInterpretation either 0 or 1 // Palette // ColorMap 320 // BitsPerSample = 4 or 8 // PhotometrciInterpretation = 3 // Full color // BitsPerSample = 8, 8, 8 // SamplesPerPixel = 3 or more 277 // Compression = 1, 32773 // PhotometricInterpretation = 2 if (colorModel instanceof IndexColorModel) { icm = (IndexColorModel) colorModel; int mapSize = icm.getMapSize(); if (sampleSize[0] == 1) { // Bilevel image if (mapSize != 2) { throw new IllegalArgumentException(JaiI18N.getString("TIFFImageEncoder1")); } byte r[] = new byte[mapSize]; icm.getReds(r); byte g[] = new byte[mapSize]; icm.getGreens(g); byte b[] = new byte[mapSize]; icm.getBlues(b); if ((r[0] & 0xff) == 0 && (r[1] & 0xff) == 255 && (g[0] & 0xff) == 0 && (g[1] & 0xff) == 255 && (b[0] & 0xff) == 0 && (b[1] & 0xff) == 255) { imageType = XTIFF_BILEVEL_BLACK_IS_ZERO; } else if ((r[0] & 0xff) == 255 && (r[1] & 0xff) == 0 && (g[0] & 0xff) == 255 && (g[1] & 0xff) == 0 && (b[0] & 0xff) == 255 && (b[1] & 0xff) == 0) { imageType = XTIFF_BILEVEL_WHITE_IS_ZERO; } else { imageType = XTIFF_PALETTE; } } else { // Palette color image. imageType = XTIFF_PALETTE; } } else { // If it is not an IndexColorModel, it can either be a greyscale // image or a full color image if ((colorModel == null || colorModel.getColorSpace().getType() == ColorSpace.TYPE_GRAY) && numBands == 1) { // Greyscale image imageType = XTIFF_GREYSCALE; } else { // Full color image imageType = XTIFF_FULLCOLOR; } } switch (imageType) { case XTIFF_BILEVEL_WHITE_IS_ZERO: photometricInterpretation = XTIFF.PHOTOMETRIC_WHITE_IS_ZERO; break; case XTIFF_BILEVEL_BLACK_IS_ZERO: photometricInterpretation = XTIFF.PHOTOMETRIC_BLACK_IS_ZERO; break; case XTIFF_GREYSCALE: // Since the CS_GRAY colorspace is always of type black_is_zero photometricInterpretation = XTIFF.PHOTOMETRIC_BLACK_IS_ZERO; break; case XTIFF_PALETTE: photometricInterpretation = XTIFF.PHOTOMETRIC_PALETTE; icm = (IndexColorModel) colorModel; sizeOfColormap = icm.getMapSize(); byte r[] = new byte[sizeOfColormap]; icm.getReds(r); byte g[] = new byte[sizeOfColormap]; icm.getGreens(g); byte b[] = new byte[sizeOfColormap]; icm.getBlues(b); int redIndex = 0, greenIndex = sizeOfColormap; int blueIndex = 2 * sizeOfColormap; colormap = new char[sizeOfColormap * 3]; for (int i = 0; i < sizeOfColormap; i++) { colormap[redIndex++] = (char) (r[i] << 8); colormap[greenIndex++] = (char) (g[i] << 8); colormap[blueIndex++] = (char) (b[i] << 8); } sizeOfColormap *= 3; // Since we will be writing the colormap field. break; case XTIFF_FULLCOLOR: photometricInterpretation = XTIFF.PHOTOMETRIC_RGB; break; } if (isTiled) { tileWidth = 16L; tileLength = 16L; XTIFFField fld = directory.getField(XTIFF.TIFFTAG_TILE_WIDTH); if (fld != null) tileWidth = (int) fld.getAsLong(0); fld = directory.getField(XTIFF.TIFFTAG_TILE_LENGTH); if (fld != null) tileLength = (int) fld.getAsLong(0); } else { // Default strip is 8 rows. tileLength = 8L; // tileWidth of strip is width tileWidth = width; XTIFFField fld = directory.getField(TIFFImageDecoder.TIFF_ROWS_PER_STRIP); if (fld != null) tileLength = fld.getAsLong(0); } numTiles = (int) Math.ceil((double) length / (double) tileLength) * (int) Math.ceil((double) width / (double) tileWidth); stripTileByteCounts = new long[numTiles]; stripTileOffsets = new long[numTiles]; // Image Width directory.addField(XTIFF.TIFFTAG_IMAGE_WIDTH, TIFFField.TIFF_LONG, 1, (Object) (new long[] { width })); // Image Length directory.addField(XTIFF.TIFFTAG_IMAGE_LENGTH, TIFFField.TIFF_LONG, 1, new long[] { length }); directory.addField(XTIFF.TIFFTAG_BITS_PER_SAMPLE, TIFFField.TIFF_SHORT, numBands, convertToChars(sampleSize)); directory.addField(XTIFF.TIFFTAG_COMPRESSION, TIFFField.TIFF_SHORT, 1, new char[] { (char) compression }); directory.addField(XTIFF.TIFFTAG_PHOTOMETRIC_INTERPRETATION, TIFFField.TIFF_SHORT, 1, new char[] { (char) photometricInterpretation }); directory.addField(XTIFF.TIFFTAG_SAMPLES_PER_PIXEL, TIFFField.TIFF_SHORT, 1, new char[] { (char) numBands }); if (isTiled) { directory.addField(XTIFF.TIFFTAG_TILE_WIDTH, TIFFField.TIFF_LONG, 1, new long[] { tileWidth }); directory.addField(XTIFF.TIFFTAG_TILE_LENGTH, TIFFField.TIFF_LONG, 1, new long[] { tileLength }); directory.addField(XTIFF.TIFFTAG_TILE_OFFSETS, TIFFField.TIFF_LONG, numTiles, stripTileOffsets); directory.addField(XTIFF.TIFFTAG_TILE_BYTE_COUNTS, TIFFField.TIFF_LONG, numTiles, stripTileByteCounts); } else { directory.addField(XTIFF.TIFFTAG_STRIP_OFFSETS, TIFFField.TIFF_LONG, numTiles, stripTileOffsets); directory.addField(XTIFF.TIFFTAG_ROWS_PER_STRIP, TIFFField.TIFF_LONG, 1, new long[] { tileLength }); directory.addField(XTIFF.TIFFTAG_STRIP_BYTE_COUNTS, TIFFField.TIFF_LONG, numTiles, stripTileByteCounts); } addIfAbsent(XTIFF.TIFFTAG_X_RESOLUTION, TIFFField.TIFF_RATIONAL, 1, new long[][] { { 72, 1 } }); addIfAbsent(XTIFF.TIFFTAG_Y_RESOLUTION, TIFFField.TIFF_RATIONAL, 1, new long[][] { { 72, 1 } }); addIfAbsent(XTIFF.TIFFTAG_RESOLUTION_UNIT, TIFFField.TIFF_SHORT, 1, new char[] { (char) 2 }); if (colormap != null) { directory.addField(XTIFF.TIFFTAG_COLORMAP, TIFFField.TIFF_SHORT, sizeOfColormap, colormap); } // Data Sample Format Extension fields. if (dataTypeIsShort) { // SampleFormat int[] sampleFormat = new int[numBands]; sampleFormat[0] = dataType == DataBuffer.TYPE_USHORT ? 1 : 2; for (int b = 1; b < numBands; b++) { sampleFormat[b] = sampleFormat[0]; } directory.addField(XTIFF.TIFFTAG_SAMPLE_FORMAT, TIFFField.TIFF_SHORT, numBands, convertToChars(sampleFormat)); // SMinSampleValue: set to data type minimum. int[] minValue = new int[numBands]; minValue[0] = dataType == DataBuffer.TYPE_USHORT ? 0 : Short.MIN_VALUE; for (int b = 1; b < numBands; b++) { minValue[b] = minValue[0]; } directory.addField(XTIFF.TIFFTAG_S_MIN_SAMPLE_VALUE, TIFFField.TIFF_SHORT, numBands, convertToChars(minValue)); // SMaxSampleValue: set to data type maximum. int[] maxValue = new int[numBands]; maxValue[0] = dataType == DataBuffer.TYPE_USHORT ? 65535 : Short.MAX_VALUE; for (int b = 1; b < numBands; b++) { maxValue[b] = maxValue[0]; } directory.addField(XTIFF.TIFFTAG_S_MAX_SAMPLE_VALUE, TIFFField.TIFF_SHORT, numBands, convertToChars(maxValue)); } } private char[] convertToChars(int[] shorts) { char[] out = new char[shorts.length]; for (int i = 0; i < shorts.length; i++) out[i] = (char) shorts[i]; return out; } protected int getSampleSize() { if (dataType == DataBuffer.TYPE_BYTE) return 1; else if (dataTypeIsShort) return 2; return 1; // what should go here? } protected int getTileSize() { return (int) (tileLength * tileWidth * numBands); } private int writeImageData(RenderedImage im, OutputStream out) throws IOException { int total = 0; // Get the encoder XTIFFTileCodec codec = directory.createTileCodec(tparam); // Create a buffer to hold the data // to be written to the file, so we can use array writes. int tsize = codec.getCompressedTileSize(im); bpixels = new byte[tsize]; // Encode one tile at a time Rectangle rect = new Rectangle(); float minX = (float) im.getMinX(); float minY = (float) im.getMinY(); float rows = (float) tileLength; float cols = (float) tileWidth; int i = 0; for (int row = 0; row < length; row += tileLength) { for (int col = 0; col < width; col += tileWidth) { if (!isTiled) rows = Math.min(tileLength, length - row); rect.setRect(minX + col, minY + row, cols, rows); int tileSize = codec.encode(im, rect, bpixels); out.write(bpixels, 0, tileSize); stripTileOffsets[i] = currentOffset; stripTileByteCounts[i++] = tileSize; currentOffset += tileSize; total += tileSize; } } return total; } private void writeDirectory(XTIFFField fields[], int nextIFDOffset) throws IOException { if (currentOffset % 2 == 1) { output.write(0); currentOffset++; } // 2 byte count of number of directory entries (fields) int numEntries = fields.length; long offsetBeyondIFD = currentOffset + 12 * numEntries + 4 + 2; Vector tooBig = new Vector(); XTIFFField field; int tag; int type; int count; // Write number of fields in the IFD writeUnsignedShort(numEntries); for (int i = 0; i < numEntries; i++) { field = fields[i]; // 12 byte field entry TIFFField // byte 0-1 Tag that identifies a field tag = field.getTag(); writeUnsignedShort(tag); // byte 2-3 The field type type = field.getType(); writeUnsignedShort(type); // bytes 4-7 the number of values of the indicated type count = field.getCount(); writeLong(count); // bytes 8 - 11 the value offset if (count * sizeOfType[type] > 4) { // We need an offset as data won't fit into 4 bytes writeLong(offsetBeyondIFD); offsetBeyondIFD += (count * sizeOfType[type]); tooBig.add(new Integer(i)); } else { writeValuesAsFourBytes(field); } } // Address of next IFD writeLong(nextIFDOffset); int index; // Write the tag values that did not fit into 4 bytes for (int i = 0; i < tooBig.size(); i++) { index = ((Integer) tooBig.elementAt(i)).intValue(); writeValues(fields[index]); } } private static final int[] sizeOfType = { 0, // 0 = n/a 1, // 1 = byte 1, // 2 = ascii 2, // 3 = short 4, // 4 = long 8, // 5 = rational 1, // 6 = sbyte 1, // 7 = undefined 2, // 8 = sshort 4, // 9 = slong 8, // 10 = srational 4, // 11 = float 8 // 12 = double }; private void writeValuesAsFourBytes(XTIFFField field) throws IOException { int dataType = field.getType(); int count = field.getCount(); switch (dataType) { // unsigned 8 bits case TIFFField.TIFF_BYTE: byte bytes[] = field.getAsBytes(); for (int i = 0; i < count; i++) { output.write(bytes[i]); } for (int i = 0; i < (4 - count); i++) { output.write(0); } break; // unsigned 16 bits case TIFFField.TIFF_SHORT: char shorts[] = field.getAsChars(); for (int i = 0; i < count; i++) { writeUnsignedShort((int) shorts[i]); } for (int i = 0; i < (2 - count); i++) { writeUnsignedShort(0); } break; // unsigned 32 bits case TIFFField.TIFF_LONG: long longs[] = field.getAsLongs(); for (int i = 0; i < count; i++) { writeLong(longs[i]); } break; } } private void writeValues(XTIFFField field) throws IOException { int dataType = field.getType(); int count = field.getCount(); switch (dataType) { // character data with NULL termination case TIFFField.TIFF_ASCII: String strings[] = field.getAsStrings(); for (int i = 0; i < strings.length; i++) { byte bytes[] = strings[i].getBytes(); for (int j = 0; j < bytes.length; j++) { output.write(bytes[j]); } if ((i + 1) < count) output.write(0); } break; // unsigned 8 bits case TIFFField.TIFF_BYTE: byte bytes[] = field.getAsBytes(); for (int i = 0; i < count; i++) { output.write(bytes[i]); } break; // unsigned 16 bits case TIFFField.TIFF_SHORT: char shorts[] = field.getAsChars(); for (int i = 0; i < count; i++) { writeUnsignedShort((int) shorts[i]); } break; // unsigned 32 bits case TIFFField.TIFF_LONG: long longs[] = field.getAsLongs(); for (int i = 0; i < count; i++) { writeLong(longs[i]); } break; // IEEE 8-byte double case TIFFField.TIFF_DOUBLE: double doubles[] = field.getAsDoubles(); for (int i = 0; i < count; i++) { writeDouble(doubles[i]); } break; case TIFFField.TIFF_RATIONAL: long rationals[][] = field.getAsRationals(); for (int i = 0; i < count; i++) { writeLong(rationals[i][0]); writeLong(rationals[i][1]); } break; } } // Here s is never expected to have value greater than what can be // stored in 2 bytes. private void writeUnsignedShort(int s) throws IOException { output.write((s & 0xff00) >>> 8); output.write(s & 0x00ff); } private void writeLong(long l) throws IOException { output.write((int) ((l & 0xff000000) >>> 24)); output.write((int) ((l & 0x00ff0000) >>> 16)); output.write((int) ((l & 0x0000ff00) >>> 8)); output.write(((int) l & 0x000000ff)); } // write 8-byte IEEE double private void writeDouble(double d) throws IOException { long lval = Double.doubleToLongBits(d); writeLong(lval >>> 32); writeLong((lval & 0xffffffff)); } }