/* * Copyright (c) 2001 Sun Microsystems, Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * -Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * -Redistribution in binary form must reproduct the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Sun Microsystems, Inc. or the names of contributors may * be used to endorse or promote products derived from this software without * specific prior written permission. * * This software is provided "AS IS," without a warranty of any kind. ALL * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE * POSSIBILITY OF SUCH DAMAGES. * * You acknowledge that Software is not designed,licensed or intended for use in * the design, construction, operation or maintenance of any nuclear facility. */ package com.fr.third.JAI; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.awt.Rectangle; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ComponentSampleModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.IndexColorModel; import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.WritableRaster; import java.util.ArrayList; import java.util.Iterator; import java.util.SortedSet; import java.util.TreeSet; import java.util.zip.Deflater; /** * A baseline TIFF writer. The writer outputs TIFF images in either Bilevel, * Greyscale, Palette color or Full Color modes. * */ public class TIFFImageEncoder extends ImageEncoderImpl { // Image Types private static final int TIFF_UNSUPPORTED = -1; private static final int TIFF_BILEVEL_WHITE_IS_ZERO = 0; private static final int TIFF_BILEVEL_BLACK_IS_ZERO = 1; private static final int TIFF_GRAY = 2; private static final int TIFF_PALETTE = 3; private static final int TIFF_RGB = 4; private static final int TIFF_CMYK = 5; private static final int TIFF_YCBCR = 6; private static final int TIFF_CIELAB = 7; private static final int TIFF_GENERIC = 8; // Compression types private static final int COMP_NONE = TIFFEncodeParam.COMPRESSION_NONE; private static final int COMP_GROUP3_1D = TIFFEncodeParam.COMPRESSION_GROUP3_1D; private static final int COMP_GROUP3_2D = TIFFEncodeParam.COMPRESSION_GROUP3_2D; private static final int COMP_GROUP4 = TIFFEncodeParam.COMPRESSION_GROUP4; private static final int COMP_JPEG_TTN2 = TIFFEncodeParam.COMPRESSION_JPEG_TTN2; private static final int COMP_PACKBITS = TIFFEncodeParam.COMPRESSION_PACKBITS; private static final int COMP_DEFLATE = TIFFEncodeParam.COMPRESSION_DEFLATE; // Incidental tags private static final int TIFF_JPEG_TABLES = 347; private static final int TIFF_YCBCR_SUBSAMPLING = 530; private static final int TIFF_YCBCR_POSITIONING = 531; private static final int TIFF_REF_BLACK_WHITE = 532; // ExtraSamples types private static final int EXTRA_SAMPLE_UNSPECIFIED = 0; private static final int EXTRA_SAMPLE_ASSOCIATED_ALPHA = 1; private static final int EXTRA_SAMPLE_UNASSOCIATED_ALPHA = 2; // Default values private static final int DEFAULT_ROWS_PER_STRIP = 8; private static final char[] intsToChars(int[] intArray) { int arrayLength = intArray.length; char[] charArray = new char[arrayLength]; for(int i = 0; i < arrayLength; i++) { charArray[i] = (char)(intArray[i]&0x0000ffff); } return charArray; } public TIFFImageEncoder(OutputStream output, ImageEncodeParam param) { super(output, param); if (this.param == null) { this.param = new TIFFEncodeParam(); } } /** * Encodes a RenderedImage and writes the output to the * OutputStream associated with this ImageEncoder. */ public void encode(RenderedImage im) throws IOException { // Write the file header (8 bytes). writeFileHeader(); // Get the encoding parameters. TIFFEncodeParam encodeParam = (TIFFEncodeParam)param; Iterator iter = encodeParam.getExtraImages(); if(iter != null) { int ifdOffset = 8; RenderedImage nextImage = im; TIFFEncodeParam nextParam = encodeParam; boolean hasNext; do { hasNext = iter.hasNext(); ifdOffset = encode(nextImage, nextParam, ifdOffset, !hasNext); if(hasNext) { Object obj = iter.next(); if(obj instanceof RenderedImage) { nextImage = (RenderedImage)obj; nextParam = encodeParam; } else if(obj instanceof Object[]) { Object[] o = (Object[])obj; nextImage = (RenderedImage)o[0]; nextParam = (TIFFEncodeParam)o[1]; } } } while(hasNext); } else { encode(im, encodeParam, 8, true); } } private int encode(RenderedImage im, TIFFEncodeParam encodeParam, int ifdOffset, boolean isLast) throws IOException { // Currently all images are stored uncompressed. int compression = encodeParam.getCompression(); // Get tiled output preference. boolean isTiled = encodeParam.getWriteTiled(); // Set bounds. int minX = im.getMinX(); int minY = im.getMinY(); int width = im.getWidth(); int height = im.getHeight(); // Get SampleModel. SampleModel sampleModel = im.getSampleModel(); // Retrieve and verify sample size. int sampleSize[] = sampleModel.getSampleSize(); for(int i = 1; i < sampleSize.length; i++) { if(sampleSize[i] != sampleSize[0]) { throw new Error(JaiI18N.getString("TIFFImageEncoder0")); } } // Check low bit limits. int numBands = sampleModel.getNumBands(); if((sampleSize[0] == 1 || sampleSize[0] == 4) && numBands != 1) { throw new Error(JaiI18N.getString("TIFFImageEncoder1")); } // Retrieve and verify data type. int dataType = sampleModel.getDataType(); switch(dataType) { case DataBuffer.TYPE_BYTE: if(sampleSize[0] != 1 && sampleSize[0] == 4 && sampleSize[0] != 8) { throw new Error(JaiI18N.getString("TIFFImageEncoder2")); } break; case DataBuffer.TYPE_SHORT: case DataBuffer.TYPE_USHORT: if(sampleSize[0] != 16) { throw new Error(JaiI18N.getString("TIFFImageEncoder3")); } break; case DataBuffer.TYPE_INT: case DataBuffer.TYPE_FLOAT: if(sampleSize[0] != 32) { throw new Error(JaiI18N.getString("TIFFImageEncoder4")); } break; default: throw new Error(JaiI18N.getString("TIFFImageEncoder5")); } boolean dataTypeIsShort = dataType == DataBuffer.TYPE_SHORT || dataType == DataBuffer.TYPE_USHORT; ColorModel colorModel = im.getColorModel(); if (colorModel != null && colorModel instanceof IndexColorModel && dataType != DataBuffer.TYPE_BYTE) { // Don't support (unsigned) short palette-color images. throw new Error(JaiI18N.getString("TIFFImageEncoder6")); } IndexColorModel icm = null; int sizeOfColormap = 0; int colormap[] = null; // Set image type. int imageType = TIFF_UNSUPPORTED; int numExtraSamples = 0; int extraSampleType = EXTRA_SAMPLE_UNSPECIFIED; if(colorModel instanceof IndexColorModel) { // Bilevel or palette icm = (IndexColorModel)colorModel; int mapSize = icm.getMapSize(); if(sampleSize[0] == 1 && numBands == 1) { // Bilevel image if (mapSize != 2) { throw new IllegalArgumentException( JaiI18N.getString("TIFFImageEncoder7")); } 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 = TIFF_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 = TIFF_BILEVEL_WHITE_IS_ZERO; } else { imageType = TIFF_PALETTE; } } else if(numBands == 1) { // Non-bilevel image. // Palette color image. imageType = TIFF_PALETTE; } } else if(colorModel == null) { if(sampleSize[0] == 1 && numBands == 1) { // bilevel imageType = TIFF_BILEVEL_BLACK_IS_ZERO; } else { // generic image imageType = TIFF_GENERIC; if(numBands > 1) { numExtraSamples = numBands - 1; } } } else { // colorModel is non-null but not an IndexColorModel ColorSpace colorSpace = colorModel.getColorSpace(); switch(colorSpace.getType()) { case ColorSpace.TYPE_CMYK: imageType = TIFF_CMYK; break; case ColorSpace.TYPE_GRAY: imageType = TIFF_GRAY; break; case ColorSpace.TYPE_Lab: imageType = TIFF_CIELAB; break; case ColorSpace.TYPE_RGB: if(compression == COMP_JPEG_TTN2 && encodeParam.getJPEGCompressRGBToYCbCr()) { imageType = TIFF_YCBCR; } else { imageType = TIFF_RGB; } break; case ColorSpace.TYPE_YCbCr: imageType = TIFF_YCBCR; break; default: imageType = TIFF_GENERIC; // generic break; } if(imageType == TIFF_GENERIC) { numExtraSamples = numBands - 1; } else if(numBands > 1) { numExtraSamples = numBands - colorSpace.getNumComponents(); } if(numExtraSamples == 1 && colorModel.hasAlpha()) { extraSampleType = colorModel.isAlphaPremultiplied() ? EXTRA_SAMPLE_ASSOCIATED_ALPHA : EXTRA_SAMPLE_UNASSOCIATED_ALPHA; } } if(imageType == TIFF_UNSUPPORTED) { throw new Error(JaiI18N.getString("TIFFImageEncoder8")); } // Check JPEG compatibility. if(compression == COMP_JPEG_TTN2) { if(imageType == TIFF_PALETTE) { throw new Error(JaiI18N.getString("TIFFImageEncoder11")); } else if(!(sampleSize[0] == 8 && (imageType == TIFF_GRAY || imageType == TIFF_RGB || imageType == TIFF_YCBCR))) { throw new Error(JaiI18N.getString("TIFFImageEncoder9")); } } int photometricInterpretation = -1; switch (imageType) { case TIFF_BILEVEL_WHITE_IS_ZERO: photometricInterpretation = 0; break; case TIFF_BILEVEL_BLACK_IS_ZERO: photometricInterpretation = 1; break; case TIFF_GRAY: case TIFF_GENERIC: // Since the CS_GRAY colorspace is always of type black_is_zero photometricInterpretation = 1; break; case TIFF_PALETTE: photometricInterpretation = 3; 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 int[sizeOfColormap * 3]; for (int i=0; i 0 ? encodeParam.getTileWidth() : im.getTileWidth(); tileHeight = encodeParam.getTileHeight() > 0 ? encodeParam.getTileHeight() : im.getTileHeight(); } else { tileWidth = width; tileHeight = encodeParam.getTileHeight() > 0 ? encodeParam.getTileHeight() : DEFAULT_ROWS_PER_STRIP; } // Re-tile for JPEG conformance if needed. JPEGEncodeParam jep = null; if(compression == COMP_JPEG_TTN2) { // Get JPEGEncodeParam from encodeParam. jep = encodeParam.getJPEGEncodeParam(); // Determine maximum subsampling. int maxSubH = jep.getHorizontalSubsampling(0); int maxSubV = jep.getVerticalSubsampling(0); for(int i = 1; i < numBands; i++) { int subH = jep.getHorizontalSubsampling(i); if(subH > maxSubH) { maxSubH = subH; } int subV = jep.getVerticalSubsampling(i); if(subV > maxSubV) { maxSubV = subV; } } int factorV = 8*maxSubV; tileHeight = (int)((float)tileHeight/(float)factorV + 0.5F)*factorV; if(tileHeight < factorV) { tileHeight = factorV; } if(isTiled) { int factorH = 8*maxSubH; tileWidth = (int)((float)tileWidth/(float)factorH + 0.5F)*factorH; if(tileWidth < factorH) { tileWidth = factorH; } } } int numTiles; if(isTiled) { // NB: Parentheses are used in this statement for correct rounding. numTiles = ((width + tileWidth - 1)/tileWidth) * ((height + tileHeight - 1)/tileHeight); } else { numTiles = (int)Math.ceil((double)height/(double)tileHeight); } long tileByteCounts[] = new long[numTiles]; long bytesPerRow = (long)Math.ceil((sampleSize[0] / 8.0) * tileWidth * numBands); long bytesPerTile = bytesPerRow * tileHeight; for (int i=0; i 0) { int[] extraSamples = new int[numExtraSamples]; for(int i = 0; i < numExtraSamples; i++) { extraSamples[i] = extraSampleType; } fields.add(new TIFFField(TIFFImageDecoder.TIFF_EXTRA_SAMPLES, TIFFField.TIFF_SHORT, numExtraSamples, intsToChars(extraSamples))); } // Data Sample Format Extension fields. if(dataType != DataBuffer.TYPE_BYTE) { // SampleFormat int[] sampleFormat = new int[numBands]; if(dataType == DataBuffer.TYPE_FLOAT) { sampleFormat[0] = 3; } else if(dataType == DataBuffer.TYPE_USHORT) { sampleFormat[0] = 1; } else { sampleFormat[0] = 2; } for(int b = 1; b < numBands; b++) { sampleFormat[b] = sampleFormat[0]; } fields.add(new TIFFField(TIFFImageDecoder.TIFF_SAMPLE_FORMAT, TIFFField.TIFF_SHORT, numBands, intsToChars(sampleFormat))); // NOTE: We don't bother setting the SMinSampleValue and // SMaxSampleValue fields as these both default to the // extrema of the respective data types. Probably we should // check for the presence of the "extrema" property and // use it if available. } // Bilevel compression variables. boolean inverseFill = encodeParam.getReverseFillOrder(); boolean T4encode2D = encodeParam.getT4Encode2D(); boolean T4PadEOLs = encodeParam.getT4PadEOLs(); TIFFFaxEncoder faxEncoder = null; // Add bilevel compression fields. if((imageType == TIFF_BILEVEL_BLACK_IS_ZERO || imageType == TIFF_BILEVEL_WHITE_IS_ZERO) && (compression == COMP_GROUP3_1D || compression == COMP_GROUP3_2D || compression == COMP_GROUP4)) { // Create the encoder. faxEncoder = new TIFFFaxEncoder(inverseFill); // FillOrder field. fields.add(new TIFFField(TIFFImageDecoder.TIFF_FILL_ORDER, TIFFField.TIFF_SHORT, 1, new char[] {inverseFill ? (char)2 : (char)1})); if(compression == COMP_GROUP3_2D) { // T4Options field. long T4Options = 0x00000000; if(T4encode2D) { T4Options |= 0x00000001; } if(T4PadEOLs) { T4Options |= 0x00000004; } fields.add(new TIFFField(TIFFImageDecoder.TIFF_T4_OPTIONS, TIFFField.TIFF_LONG, 1, new long[] {T4Options})); } else if(compression == COMP_GROUP4) { // T6Options field. fields.add(new TIFFField(TIFFImageDecoder.TIFF_T6_OPTIONS, TIFFField.TIFF_LONG, 1, new long[] {(long)0x00000000})); } } // Initialize some JPEG variables. com.sun.image.codec.jpeg.JPEGEncodeParam jpegEncodeParam = null; com.sun.image.codec.jpeg.JPEGImageEncoder jpegEncoder = null; int jpegColorID = 0; if(compression == COMP_JPEG_TTN2) { // Initialize JPEG color ID. jpegColorID = com.sun.image.codec.jpeg.JPEGDecodeParam.COLOR_ID_UNKNOWN; switch(imageType) { case TIFF_GRAY: case TIFF_PALETTE: jpegColorID = com.sun.image.codec.jpeg.JPEGDecodeParam.COLOR_ID_GRAY; break; case TIFF_RGB: jpegColorID = com.sun.image.codec.jpeg.JPEGDecodeParam.COLOR_ID_RGB; break; case TIFF_YCBCR: jpegColorID = com.sun.image.codec.jpeg.JPEGDecodeParam.COLOR_ID_YCbCr; break; } // Get the JDK encoding parameters. Raster tile00 = im.getTile(0, 0); jpegEncodeParam = com.sun.image.codec.jpeg.JPEGCodec.getDefaultJPEGEncodeParam( tile00, jpegColorID); // Modify per values passed in. JPEGImageEncoder.modifyEncodeParam(jep, jpegEncodeParam, numBands); // JPEGTables field. if(jep.getWriteImageOnly()) { // Write an abbreviated tables-only stream to JPEGTables field. jpegEncodeParam.setImageInfoValid(false); jpegEncodeParam.setTableInfoValid(true); ByteArrayOutputStream tableStream = new ByteArrayOutputStream(); jpegEncoder = com.sun.image.codec.jpeg.JPEGCodec.createJPEGEncoder( tableStream, jpegEncodeParam); jpegEncoder.encode(tile00); byte[] tableData = tableStream.toByteArray(); fields.add(new TIFFField(TIFF_JPEG_TABLES, TIFFField.TIFF_UNDEFINED, tableData.length, tableData)); // Reset encoder so it's recreated below. jpegEncoder = null; } } if(imageType == TIFF_YCBCR) { // YCbCrSubSampling: 2 is the default so we must write 1 as // we do not (yet) do any subsampling. int subsampleH = 1; int subsampleV = 1; // If JPEG, update values. if(compression == COMP_JPEG_TTN2) { // Determine maximum subsampling. subsampleH = jep.getHorizontalSubsampling(0); subsampleV = jep.getVerticalSubsampling(0); for(int i = 1; i < numBands; i++) { int subH = jep.getHorizontalSubsampling(i); if(subH > subsampleH) { subsampleH = subH; } int subV = jep.getVerticalSubsampling(i); if(subV > subsampleV) { subsampleV = subV; } } } fields.add(new TIFFField(TIFF_YCBCR_SUBSAMPLING, TIFFField.TIFF_SHORT, 2, new char[] {(char)subsampleH, (char)subsampleV})); // YCbCr positioning. fields.add(new TIFFField(TIFF_YCBCR_POSITIONING, TIFFField.TIFF_SHORT, 1, new char[] {compression == COMP_JPEG_TTN2 ? (char)1 : (char)2})); // Reference black/white. long[][] refbw; if(compression == COMP_JPEG_TTN2) { refbw = new long[][] { // no headroon/footroom {0, 1}, {255, 1}, {128, 1}, {255, 1}, {128, 1}, {255, 1} }; } else { refbw = new long[][] { // CCIR 601.1 headroom/footroom (presumptive) {15, 1}, {235, 1}, {128, 1}, {240, 1}, {128, 1}, {240, 1} }; } fields.add(new TIFFField(TIFF_REF_BLACK_WHITE, TIFFField.TIFF_RATIONAL, 6, refbw)); } // ---- No more automatically generated fields should be added // after this point. ---- // Add extra fields specified via the encoding parameters. TIFFField[] extraFields = encodeParam.getExtraFields(); if(extraFields != null) { ArrayList extantTags = new ArrayList(fields.size()); Iterator fieldIter = fields.iterator(); while(fieldIter.hasNext()) { TIFFField fld = (TIFFField)fieldIter.next(); extantTags.add(new Integer(fld.getTag())); } int numExtraFields = extraFields.length; for(int i = 0; i < numExtraFields; i++) { TIFFField fld = extraFields[i]; Integer tagValue = new Integer(fld.getTag()); if(!extantTags.contains(tagValue)) { fields.add(fld); extantTags.add(tagValue); } } } // ---- No more fields of any type should be added after this. ---- // Determine the size of the IFD which is written after the header // of the stream or after the data of the previous image in a // multi-page stream. int dirSize = getDirectorySize(fields); // The first data segment is written after the field overflow // following the IFD so initialize the first offset accordingly. tileOffsets[0] = ifdOffset + dirSize; // Branch here depending on whether data are being comrpressed. // If not, then the IFD is written immediately. // If so then there are three possibilities: // A) the OutputStream is a SeekableOutputStream (outCache null); // B) the OutputStream is not a SeekableOutputStream and a file cache // is used (outCache non-null, tempFile non-null); // C) the OutputStream is not a SeekableOutputStream and a memory cache // is used (outCache non-null, tempFile null). OutputStream outCache = null; byte[] compressBuf = null; File tempFile = null; int nextIFDOffset = 0; boolean skipByte = false; Deflater deflater = null; int deflateLevel = Deflater.DEFAULT_COMPRESSION; boolean jpegRGBToYCbCr = false; if(compression == COMP_NONE) { // Determine the number of bytes of padding necessary between // the end of the IFD and the first data segment such that the // alignment of the data conforms to the specification (required // for uncompressed data only). int numBytesPadding = 0; if(sampleSize[0] == 16 && tileOffsets[0] % 2 != 0) { numBytesPadding = 1; tileOffsets[0]++; } else if(sampleSize[0] == 32 && tileOffsets[0] % 4 != 0) { numBytesPadding = (int)(4 - tileOffsets[0] % 4); tileOffsets[0] += numBytesPadding; } // Update the data offsets (which TIFFField stores by reference). for (int i = 1; i < numTiles; i++) { tileOffsets[i] = tileOffsets[i-1] + tileByteCounts[i-1]; } if(!isLast) { // Determine the offset of the next IFD. nextIFDOffset = (int)(tileOffsets[0] + totalBytesOfData); // IFD offsets must be on a word boundary. if(nextIFDOffset % 2 != 0) { nextIFDOffset++; skipByte = true; } } // Write the IFD and field overflow before the image data. writeDirectory(ifdOffset, fields, nextIFDOffset); // Write any padding bytes needed between the end of the IFD // and the start of the actual image data. if(numBytesPadding != 0) { for(int padding = 0; padding < numBytesPadding; padding++) { output.write((byte)0); } } } else { // If compressing, the cannot be written yet as the size of the // data segments is unknown. if((output instanceof SeekableOutputStream)) { // Simply seek to the first data segment position. ((SeekableOutputStream)output).seek(tileOffsets[0]); } else { // Cache the original OutputStream. outCache = output; try { // Attempt to create a temporary file. tempFile = File.createTempFile("jai-SOS-", ".tmp"); tempFile.deleteOnExit(); RandomAccessFile raFile = new RandomAccessFile(tempFile, "rw"); output = new SeekableOutputStream(raFile); // this method is exited! } catch(Exception e) { // Allocate memory for the entire image data (!). output = new ByteArrayOutputStream((int)totalBytesOfData); } } int bufSize = 0; switch(compression) { case COMP_GROUP3_1D: // This initial buffer size is based on an alternating 1-0 // pattern generating the most bits when converted to code // words: 9 bits out for each pair of bits in. So the number // of bit pairs is determined, multiplied by 9, converted to // bytes, and a ceil() is taken to account for fill bits at the // end of each line. The "2" addend accounts for the case // of the pattern beginning with black. The buffer is intended // to hold only a single row. bufSize = (int)Math.ceil((((tileWidth + 1)/2)*9 + 2)/8.0); break; case COMP_GROUP3_2D: case COMP_GROUP4: // Calculate the maximum row as the G3-1D size plus the EOL, // multiply this by the number of rows in the tile, and add // 6 EOLs for the RTC (return to control). bufSize = (int)Math.ceil((((tileWidth + 1)/2)*9 + 2)/8.0); bufSize = tileHeight*(bufSize + 2) + 12; break; case COMP_PACKBITS: bufSize = (int)(bytesPerTile + ((bytesPerRow+127)/128)*tileHeight); break; case COMP_JPEG_TTN2: bufSize = 0; // Set color conversion flag. if(imageType == TIFF_YCBCR && colorModel != null && colorModel.getColorSpace().getType() == ColorSpace.TYPE_RGB) { jpegRGBToYCbCr = true; } break; case COMP_DEFLATE: bufSize = (int)bytesPerTile; deflater = new Deflater(encodeParam.getDeflateLevel()); break; default: bufSize = 0; } if(bufSize != 0) { compressBuf = new byte[bufSize]; } } // ---- Writing of actual image data ---- // Buffer for up to tileHeight rows of pixels int[] pixels = null; float[] fpixels = null; // Whether to test for contiguous data. boolean checkContiguous = ((sampleSize[0] == 1 && sampleModel instanceof MultiPixelPackedSampleModel && dataType == DataBuffer.TYPE_BYTE) || (sampleSize[0] == 8 && sampleModel instanceof ComponentSampleModel)); // Also create a buffer to hold tileHeight lines of the // data to be written to the file, so we can use array writes. byte[] bpixels = null; if(compression != COMP_JPEG_TTN2) { if(dataType == DataBuffer.TYPE_BYTE) { bpixels = new byte[tileHeight * tileWidth * numBands]; } else if(dataTypeIsShort) { bpixels = new byte[2 * tileHeight * tileWidth * numBands]; } else if(dataType == DataBuffer.TYPE_INT || dataType == DataBuffer.TYPE_FLOAT) { bpixels = new byte[4 * tileHeight * tileWidth * numBands]; } } // Process tileHeight rows at a time int lastRow = minY + height; int lastCol = minX + width; int tileNum = 0; for (int row = minY; row < lastRow; row += tileHeight) { int rows = isTiled ? tileHeight : Math.min(tileHeight, lastRow - row); int size = rows * tileWidth * numBands; for(int col = minX; col < lastCol; col += tileWidth) { // Grab the pixels Raster src = im.getData(new Rectangle(col, row, tileWidth, rows)); boolean useDataBuffer = false; if(compression != COMP_JPEG_TTN2) { // JPEG access Raster if(checkContiguous) { if(sampleSize[0] == 8) { // 8-bit ComponentSampleModel csm = (ComponentSampleModel)src.getSampleModel(); int[] bankIndices = csm.getBankIndices(); int[] bandOffsets = csm.getBandOffsets(); int pixelStride = csm.getPixelStride(); int lineStride = csm.getScanlineStride(); if(pixelStride != numBands || lineStride != bytesPerRow) { useDataBuffer = false; } else { useDataBuffer = true; for(int i = 0; useDataBuffer && i < numBands; i++) { if(bankIndices[i] != 0 || bandOffsets[i] != i) { useDataBuffer = false; } } } } else { // 1-bit MultiPixelPackedSampleModel mpp = (MultiPixelPackedSampleModel)src.getSampleModel(); if(mpp.getNumBands() == 1 && mpp.getDataBitOffset() == 0 && mpp.getPixelBitStride() == 1) { useDataBuffer = true; } } } if(!useDataBuffer) { if(dataType == DataBuffer.TYPE_FLOAT) { fpixels = src.getPixels(col, row, tileWidth, rows, fpixels); } else { pixels = src.getPixels(col, row, tileWidth, rows, pixels); } } } int index; int pixel = 0;; int k = 0; switch(sampleSize[0]) { case 1: if(useDataBuffer) { byte[] btmp = ((DataBufferByte)src.getDataBuffer()).getData(); MultiPixelPackedSampleModel mpp = (MultiPixelPackedSampleModel)src.getSampleModel(); int lineStride = mpp.getScanlineStride(); int inOffset = mpp.getOffset(col - src.getSampleModelTranslateX(), row - src.getSampleModelTranslateY()); if(lineStride == (int)bytesPerRow) { System.arraycopy(btmp, inOffset, bpixels, 0, (int)bytesPerRow*rows); } else { int outOffset = 0; for(int j = 0; j < rows; j++) { System.arraycopy(btmp, inOffset, bpixels, outOffset, (int)bytesPerRow); inOffset += lineStride; outOffset += (int)bytesPerRow; } } } else { index = 0; // For each of the rows in a strip for (int i=0; i 0) { pixel = 0; for (int j=0; j> 8); bpixels[ls++] = (byte)(value & 0x00ff); } if(compression == COMP_NONE) { output.write(bpixels, 0, size*2); } else if(compression == COMP_PACKBITS) { int numCompressedBytes = compressPackBits(bpixels, rows, (int)bytesPerRow, compressBuf); tileByteCounts[tileNum++] = numCompressedBytes; output.write(compressBuf, 0, numCompressedBytes); } else if(compression == COMP_DEFLATE) { int numCompressedBytes = deflate(deflater, bpixels, compressBuf); tileByteCounts[tileNum++] = numCompressedBytes; output.write(compressBuf, 0, numCompressedBytes); } break; case 32: if(dataType == DataBuffer.TYPE_INT) { int li = 0; for (int i = 0; i < size; i++) { int value = pixels[i]; bpixels[li++] = (byte)((value & 0xff000000) >> 24); bpixels[li++] = (byte)((value & 0x00ff0000) >> 16); bpixels[li++] = (byte)((value & 0x0000ff00) >> 8); bpixels[li++] = (byte)(value & 0x000000ff); } } else { // DataBuffer.TYPE_FLOAT int lf = 0; for (int i = 0; i < size; i++) { int value = Float.floatToIntBits(fpixels[i]); bpixels[lf++] = (byte)((value & 0xff000000) >> 24); bpixels[lf++] = (byte)((value & 0x00ff0000) >> 16); bpixels[lf++] = (byte)((value & 0x0000ff00) >> 8); bpixels[lf++] = (byte)(value & 0x000000ff); } } if(compression == COMP_NONE) { output.write(bpixels, 0, size*4); } else if(compression == COMP_PACKBITS) { int numCompressedBytes = compressPackBits(bpixels, rows, (int)bytesPerRow, compressBuf); tileByteCounts[tileNum++] = numCompressedBytes; output.write(compressBuf, 0, numCompressedBytes); } else if(compression == COMP_DEFLATE) { int numCompressedBytes = deflate(deflater, bpixels, compressBuf); tileByteCounts[tileNum++] = numCompressedBytes; output.write(compressBuf, 0, numCompressedBytes); } break; } } } if(compression == COMP_NONE) { // Write an extra byte for IFD word alignment if needed. if(skipByte) { output.write((byte)0); } } else { // Recompute the tile offsets the size of the compressed tiles. int totalBytes = 0; for (int i=1; i 4 bytes. int dirSize = 2 + numEntries*12 + 4; // Loop over fields adding the size of all values > 4 bytes. Iterator iter = fields.iterator(); while(iter.hasNext()) { // Get the field. TIFFField field = (TIFFField)iter.next(); // Determine the size of the field value. int valueSize = getValueSize(field); // Add any excess size. if(valueSize > 4) { dirSize += valueSize; } } return dirSize; } private void writeFileHeader() 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. writeLong(8); } private void writeDirectory(int thisIFDOffset, SortedSet fields, int nextIFDOffset) throws IOException { // 2 byte count of number of directory entries (fields) int numEntries = fields.size(); long offsetBeyondIFD = thisIFDOffset + 12 * numEntries + 4 + 2; ArrayList tooBig = new ArrayList(); // Write number of fields in the IFD writeUnsignedShort(numEntries); Iterator iter = fields.iterator(); while(iter.hasNext()) { // 12 byte field entry TIFFField TIFFField field = (TIFFField)iter.next(); // byte 0-1 Tag that identifies a field int tag = field.getTag(); writeUnsignedShort(tag); // byte 2-3 The field type int type = field.getType(); writeUnsignedShort(type); // bytes 4-7 the number of values of the indicated type except // ASCII-valued fields which require the total number of bytes. int count = field.getCount(); int valueSize = getValueSize(field); writeLong(type == TIFFField.TIFF_ASCII ? valueSize : count); // bytes 8 - 11 the value or value offset if (valueSize > 4) { // We need an offset as data won't fit into 4 bytes writeLong(offsetBeyondIFD); offsetBeyondIFD += valueSize; tooBig.add(field); } else { writeValuesAsFourBytes(field); } } // Address of next IFD writeLong(nextIFDOffset); // Write the tag values that did not fit into 4 bytes for (int i = 0; i < tooBig.size(); i++) { writeValues((TIFFField)tooBig.get(i)); } } /** * Determine the number of bytes in the value portion of the field. */ private static final int getValueSize(TIFFField field) { int type = field.getType(); int count = field.getCount(); int valueSize = 0; if(type == TIFFField.TIFF_ASCII) { for(int i = 0; i < count; i++) { byte[] stringBytes = field.getAsString(i).getBytes(); valueSize += stringBytes.length; if(stringBytes[stringBytes.length-1] != (byte)0) { valueSize++; } } } else { valueSize = count * sizeOfType[type]; } return valueSize; } 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(TIFFField field) throws IOException { int dataType = field.getType(); int count = field.getCount(); switch (dataType) { // 8 bits case TIFFField.TIFF_BYTE: case TIFFField.TIFF_SBYTE: case TIFFField.TIFF_UNDEFINED: byte bytes[] = field.getAsBytes(); for (int i=0; i> 32)); writeLong((int)(longBits & 0xffffffff)); } break; // unsigned rationals case TIFFField.TIFF_RATIONAL: long rationals[][] = field.getAsRationals(); for (int i=0; i>> 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)); } /** * Returns the current offset in the supplied OutputStream. * This method should only be used if compressing data. */ private long getOffset(OutputStream out) throws IOException { if(out instanceof ByteArrayOutputStream) { return ((ByteArrayOutputStream)out).size(); } else if(out instanceof SeekableOutputStream) { return ((SeekableOutputStream)out).getFilePointer(); } else { // Shouldn't happen. throw new IllegalStateException(); } } /** * Performs PackBits compression on a tile of data. */ private static int compressPackBits(byte[] data, int numRows, int bytesPerRow, byte[] compData) { int inOffset = 0; int outOffset = 0; for(int i = 0; i < numRows; i++) { outOffset = packBits(data, inOffset, bytesPerRow, compData, outOffset); inOffset += bytesPerRow; } return outOffset; } /** * Performs PackBits compression for a single buffer of data. * This should be called for each row of each tile. The returned * value is the offset into the output buffer after compression. */ private static int packBits(byte[] input, int inOffset, int inCount, byte[] output, int outOffset) { int inMax = inOffset + inCount - 1; int inMaxMinus1 = inMax - 1; while(inOffset <= inMax) { int run = 1; byte replicate = input[inOffset]; while(run < 127 && inOffset < inMax && input[inOffset] == input[inOffset+1]) { run++; inOffset++; } if(run > 1) { inOffset++; output[outOffset++] = (byte)(-(run - 1)); output[outOffset++] = replicate; } run = 0; int saveOffset = outOffset; while(run < 128 && ((inOffset < inMax && input[inOffset] != input[inOffset+1]) || (inOffset < inMaxMinus1 && input[inOffset] != input[inOffset+2]))) { run++; output[++outOffset] = input[inOffset++]; } if(run > 0) { output[saveOffset] = (byte)(run - 1); outOffset++; } if(inOffset == inMax) { if(run > 0 && run < 128) { output[saveOffset]++; output[outOffset++] = input[inOffset++]; } else { output[outOffset++] = (byte)0; output[outOffset++] = input[inOffset++]; } } } return outOffset; } private static int deflate(Deflater deflater, byte[] inflated, byte[] deflated) { deflater.setInput(inflated); deflater.finish(); int numCompressedBytes = deflater.deflate(deflated); deflater.reset(); return numCompressedBytes; } }