diff -r 098d54b4051d -r 68c0d866db5d jdk/src/java.desktop/share/classes/com/sun/imageio/plugins/tiff/TIFFImageWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/src/java.desktop/share/classes/com/sun/imageio/plugins/tiff/TIFFImageWriter.java Mon Nov 23 12:26:12 2015 -0800 @@ -0,0 +1,3610 @@ +/* + * Copyright (c) 2005, 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package com.sun.imageio.plugins.tiff; + +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_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.RenderedImage; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.imageio.IIOException; +import javax.imageio.IIOImage; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOInvalidTreeException; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.metadata.IIOMetadataFormatImpl; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageOutputStream; +import org.w3c.dom.Node; +import com.sun.imageio.plugins.common.ImageUtil; +import javax.imageio.plugins.tiff.BaselineTIFFTagSet; +import javax.imageio.plugins.tiff.ExifParentTIFFTagSet; +import javax.imageio.plugins.tiff.ExifTIFFTagSet; +import javax.imageio.plugins.tiff.TIFFField; +import javax.imageio.plugins.tiff.TIFFTag; +import javax.imageio.plugins.tiff.TIFFTagSet; +import com.sun.imageio.plugins.common.SimpleRenderedImage; +import com.sun.imageio.plugins.common.SingleTileRenderedImage; +import java.nio.charset.StandardCharsets; + +public class TIFFImageWriter extends ImageWriter { + + static final String EXIF_JPEG_COMPRESSION_TYPE = "Exif JPEG"; + + private static final int DEFAULT_BYTES_PER_STRIP = 8192; + + /** + * Supported TIFF compression types. + */ + static final String[] TIFFCompressionTypes = { + "CCITT RLE", + "CCITT T.4", + "CCITT T.6", + "LZW", + // "Old JPEG", + "JPEG", + "ZLib", + "PackBits", + "Deflate", + EXIF_JPEG_COMPRESSION_TYPE + }; + + // + // The lengths of the arrays 'compressionTypes', + // 'isCompressionLossless', and 'compressionNumbers' + // must be equal. + // + + /** + * Known TIFF compression types. + */ + static final String[] compressionTypes = { + "CCITT RLE", + "CCITT T.4", + "CCITT T.6", + "LZW", + "Old JPEG", + "JPEG", + "ZLib", + "PackBits", + "Deflate", + EXIF_JPEG_COMPRESSION_TYPE + }; + + /** + * Lossless flag for known compression types. + */ + static final boolean[] isCompressionLossless = { + true, // RLE + true, // T.4 + true, // T.6 + true, // LZW + false, // Old JPEG + false, // JPEG + true, // ZLib + true, // PackBits + true, // DEFLATE + false // Exif JPEG + }; + + /** + * Compression tag values for known compression types. + */ + static final int[] compressionNumbers = { + BaselineTIFFTagSet.COMPRESSION_CCITT_RLE, + BaselineTIFFTagSet.COMPRESSION_CCITT_T_4, + BaselineTIFFTagSet.COMPRESSION_CCITT_T_6, + BaselineTIFFTagSet.COMPRESSION_LZW, + BaselineTIFFTagSet.COMPRESSION_OLD_JPEG, + BaselineTIFFTagSet.COMPRESSION_JPEG, + BaselineTIFFTagSet.COMPRESSION_ZLIB, + BaselineTIFFTagSet.COMPRESSION_PACKBITS, + BaselineTIFFTagSet.COMPRESSION_DEFLATE, + BaselineTIFFTagSet.COMPRESSION_OLD_JPEG, // Exif JPEG + }; + + private ImageOutputStream stream; + private long headerPosition; + private RenderedImage image; + private ImageTypeSpecifier imageType; + private ByteOrder byteOrder; + private ImageWriteParam param; + private TIFFCompressor compressor; + private TIFFColorConverter colorConverter; + + private TIFFStreamMetadata streamMetadata; + private TIFFImageMetadata imageMetadata; + + private int sourceXOffset; + private int sourceYOffset; + private int sourceWidth; + private int sourceHeight; + private int[] sourceBands; + private int periodX; + private int periodY; + + private int bitDepth; // bits per channel + private int numBands; + private int tileWidth; + private int tileLength; + private int tilesAcross; + private int tilesDown; + + private int[] sampleSize = null; // Input sample size per band, in bits + private int scalingBitDepth = -1; // Output bit depth of the scaling tables + private boolean isRescaling = false; // Whether rescaling is needed. + + private boolean isBilevel; // Whether image is bilevel + private boolean isImageSimple; // Whether image can be copied into directly + private boolean isInverted; // Whether photometric inversion is required + + private boolean isTiled; // Whether the image is tiled (true) or stipped (false). + + private int nativePhotometricInterpretation; + private int photometricInterpretation; + + private char[] bitsPerSample; // Output sample size per band + private int sampleFormat = + BaselineTIFFTagSet.SAMPLE_FORMAT_UNDEFINED; // Output sample format + + // Tables for 1, 2, 4, or 8 bit output + private byte[][] scale = null; // 8 bit table + private byte[] scale0 = null; // equivalent to scale[0] + + // Tables for 16 bit output + private byte[][] scaleh = null; // High bytes of output + private byte[][] scalel = null; // Low bytes of output + + private int compression; + private int predictor; + + private int totalPixels; + private int pixelsDone; + + private long nextIFDPointerPos; + + // Next available space. + private long nextSpace = 0L; + + // Whether a sequence is being written. + private boolean isWritingSequence = false; + private boolean isInsertingEmpty = false; + private boolean isWritingEmpty = false; + + private int currentImage = 0; + + /** + * Converts a pixel's X coordinate into a horizontal tile index + * relative to a given tile grid layout specified by its X offset + * and tile width. + * + *

If tileWidth < 0, the results of this method + * are undefined. If tileWidth == 0, an + * ArithmeticException will be thrown. + * + * @throws ArithmeticException If tileWidth == 0. + */ + public static int XToTileX(int x, int tileGridXOffset, int tileWidth) { + x -= tileGridXOffset; + if (x < 0) { + x += 1 - tileWidth; // force round to -infinity (ceiling) + } + return x/tileWidth; + } + + /** + * Converts a pixel's Y coordinate into a vertical tile index + * relative to a given tile grid layout specified by its Y offset + * and tile height. + * + *

If tileHeight < 0, the results of this method + * are undefined. If tileHeight == 0, an + * ArithmeticException will be thrown. + * + * @throws ArithmeticException If tileHeight == 0. + */ + public static int YToTileY(int y, int tileGridYOffset, int tileHeight) { + y -= tileGridYOffset; + if (y < 0) { + y += 1 - tileHeight; // force round to -infinity (ceiling) + } + return y/tileHeight; + } + + public TIFFImageWriter(ImageWriterSpi originatingProvider) { + super(originatingProvider); + } + + public ImageWriteParam getDefaultWriteParam() { + return new TIFFImageWriteParam(getLocale()); + } + + 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; + + // + // The output is expected to be positioned at a TIFF header + // or at some arbitrary location which may or may not be + // the EOF. In the former case the writer should be able + // either to overwrite the existing sequence or append to it. + // + + // Set the position of the header and the next available space. + try { + headerPosition = this.stream.getStreamPosition(); + try { + // Read byte order and magic number. + byte[] b = new byte[4]; + stream.readFully(b); + + // Check bytes for TIFF header. + if((b[0] == (byte)0x49 && b[1] == (byte)0x49 && + b[2] == (byte)0x2a && b[3] == (byte)0x00) || + (b[0] == (byte)0x4d && b[1] == (byte)0x4d && + b[2] == (byte)0x00 && b[3] == (byte)0x2a)) { + // TIFF header. + this.nextSpace = stream.length(); + } else { + // Neither TIFF header nor EOF: overwrite. + this.nextSpace = headerPosition; + } + } catch(IOException io) { // thrown by readFully() + // At EOF or not at a TIFF header. + this.nextSpace = headerPosition; + } + stream.seek(headerPosition); + } catch(IOException ioe) { // thrown by getStreamPosition() + // Assume it's at zero. + this.nextSpace = headerPosition = 0L; + } + } else { + this.stream = null; + } + } + + public IIOMetadata + getDefaultStreamMetadata(ImageWriteParam param) { + return new TIFFStreamMetadata(); + } + + public IIOMetadata + getDefaultImageMetadata(ImageTypeSpecifier imageType, + ImageWriteParam param) { + + List tagSets = new ArrayList(1); + tagSets.add(BaselineTIFFTagSet.getInstance()); + TIFFImageMetadata imageMetadata = new TIFFImageMetadata(tagSets); + + if(imageType != null) { + TIFFImageMetadata im = + (TIFFImageMetadata)convertImageMetadata(imageMetadata, + imageType, + param); + if(im != null) { + imageMetadata = im; + } + } + + return imageMetadata; + } + + public IIOMetadata convertStreamMetadata(IIOMetadata inData, + ImageWriteParam param) { + // Check arguments. + if(inData == null) { + throw new NullPointerException("inData == null!"); + } + + // Note: param is irrelevant as it does not contain byte order. + + TIFFStreamMetadata outData = null; + if(inData instanceof TIFFStreamMetadata) { + outData = new TIFFStreamMetadata(); + outData.byteOrder = ((TIFFStreamMetadata)inData).byteOrder; + return outData; + } else if(Arrays.asList(inData.getMetadataFormatNames()).contains( + TIFFStreamMetadata.NATIVE_METADATA_FORMAT_NAME)) { + outData = new TIFFStreamMetadata(); + String format = TIFFStreamMetadata.NATIVE_METADATA_FORMAT_NAME; + try { + outData.mergeTree(format, inData.getAsTree(format)); + } catch(IIOInvalidTreeException e) { + return null; + } + } + + return outData; + } + + public IIOMetadata + convertImageMetadata(IIOMetadata inData, + ImageTypeSpecifier imageType, + ImageWriteParam param) { + // Check arguments. + if(inData == null) { + throw new NullPointerException("inData == null!"); + } + if(imageType == null) { + throw new NullPointerException("imageType == null!"); + } + + TIFFImageMetadata outData = null; + + // Obtain a TIFFImageMetadata object. + if(inData instanceof TIFFImageMetadata) { + // Create a new metadata object from a clone of the input IFD. + TIFFIFD inIFD = ((TIFFImageMetadata)inData).getRootIFD(); + outData = new TIFFImageMetadata(inIFD.getShallowClone()); + } else if(Arrays.asList(inData.getMetadataFormatNames()).contains( + TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME)) { + // Initialize from the native metadata form of the input tree. + try { + outData = convertNativeImageMetadata(inData); + } catch(IIOInvalidTreeException e) { + return null; + } + } else if(inData.isStandardMetadataFormatSupported()) { + // Initialize from the standard metadata form of the input tree. + try { + outData = convertStandardImageMetadata(inData); + } catch(IIOInvalidTreeException e) { + return null; + } + } + + // Update the metadata per the image type and param. + if(outData != null) { + TIFFImageWriter bogusWriter = + new TIFFImageWriter(this.originatingProvider); + bogusWriter.imageMetadata = outData; + bogusWriter.param = param; + SampleModel sm = imageType.getSampleModel(); + try { + bogusWriter.setupMetadata(imageType.getColorModel(), sm, + sm.getWidth(), sm.getHeight()); + return bogusWriter.imageMetadata; + } catch(IIOException e) { + return null; + } + } + + return outData; + } + + /** + * Converts a standard javax_imageio_1.0 tree to a + * TIFFImageMetadata object. + * + * @param inData The metadata object. + * @return a TIFFImageMetadata or null if + * the standard tree derived from the input object is null. + * @throws IllegalArgumentException if inData is + * null. + * @throws IllegalArgumentException if inData does not support + * the standard metadata format. + * @throws IIOInvalidTreeException if inData generates an + * invalid standard metadata tree. + */ + private TIFFImageMetadata convertStandardImageMetadata(IIOMetadata inData) + throws IIOInvalidTreeException { + + if(inData == null) { + throw new NullPointerException("inData == null!"); + } else if(!inData.isStandardMetadataFormatSupported()) { + throw new IllegalArgumentException + ("inData does not support standard metadata format!"); + } + + TIFFImageMetadata outData = null; + + String formatName = IIOMetadataFormatImpl.standardMetadataFormatName; + Node tree = inData.getAsTree(formatName); + if (tree != null) { + List tagSets = new ArrayList(1); + tagSets.add(BaselineTIFFTagSet.getInstance()); + outData = new TIFFImageMetadata(tagSets); + outData.setFromTree(formatName, tree); + } + + return outData; + } + + /** + * Converts a native + * javax_imageio_tiff_image_1.0 tree to a + * TIFFImageMetadata object. + * + * @param inData The metadata object. + * @return a TIFFImageMetadata or null if + * the native tree derived from the input object is null. + * @throws IllegalArgumentException if inData is + * null or does not support the native metadata format. + * @throws IIOInvalidTreeException if inData generates an + * invalid native metadata tree. + */ + private TIFFImageMetadata convertNativeImageMetadata(IIOMetadata inData) + throws IIOInvalidTreeException { + + if(inData == null) { + throw new NullPointerException("inData == null!"); + } else if(!Arrays.asList(inData.getMetadataFormatNames()).contains( + TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME)) { + throw new IllegalArgumentException + ("inData does not support native metadata format!"); + } + + TIFFImageMetadata outData = null; + + String formatName = TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME; + Node tree = inData.getAsTree(formatName); + if (tree != null) { + List tagSets = new ArrayList(1); + tagSets.add(BaselineTIFFTagSet.getInstance()); + outData = new TIFFImageMetadata(tagSets); + outData.setFromTree(formatName, tree); + } + + return outData; + } + + /** + * Sets up the output metadata adding, removing, and overriding fields + * as needed. The destination image dimensions are provided as parameters + * because these might differ from those of the source due to subsampling. + * + * @param cm The ColorModel of the image being written. + * @param sm The SampleModel of the image being written. + * @param destWidth The width of the written image after subsampling. + * @param destHeight The height of the written image after subsampling. + */ + void setupMetadata(ColorModel cm, SampleModel sm, + int destWidth, int destHeight) + throws IIOException { + // Get initial IFD from metadata + + // Always emit these fields: + // + // Override values from metadata: + // + // planarConfiguration -> chunky (planar not supported on output) + // + // Override values from metadata with image-derived values: + // + // bitsPerSample (if not bilivel) + // colorMap (if palette color) + // photometricInterpretation (derive from image) + // imageLength + // imageWidth + // + // rowsPerStrip \ / tileLength + // stripOffsets | OR | tileOffsets + // stripByteCounts / | tileByteCounts + // \ tileWidth + // + // + // Override values from metadata with write param values: + // + // compression + + // Use values from metadata if present for these fields, + // otherwise use defaults: + // + // resolutionUnit + // XResolution (take from metadata if present) + // YResolution + // rowsPerStrip + // sampleFormat + + TIFFIFD rootIFD = imageMetadata.getRootIFD(); + + BaselineTIFFTagSet base = BaselineTIFFTagSet.getInstance(); + + // If PlanarConfiguration field present, set value to chunky. + + TIFFField f = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION); + if(f != null && + f.getAsInt(0) != BaselineTIFFTagSet.PLANAR_CONFIGURATION_CHUNKY) { + TIFFField planarConfigurationField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION), + BaselineTIFFTagSet.PLANAR_CONFIGURATION_CHUNKY); + rootIFD.addTIFFField(planarConfigurationField); + } + + char[] extraSamples = null; + + this.photometricInterpretation = -1; + boolean forcePhotometricInterpretation = false; + + f = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION); + if (f != null) { + photometricInterpretation = f.getAsInt(0); + if(photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR && + !(cm instanceof IndexColorModel)) { + photometricInterpretation = -1; + } else { + forcePhotometricInterpretation = true; + } + } + + int[] sampleSize = sm.getSampleSize(); + + int numBands = sm.getNumBands(); + int numExtraSamples = 0; + + // Check that numBands > 1 here because TIFF requires that + // SamplesPerPixel = numBands + numExtraSamples and numBands + // cannot be zero. + if (numBands > 1 && cm != null && cm.hasAlpha()) { + --numBands; + numExtraSamples = 1; + extraSamples = new char[1]; + if (cm.isAlphaPremultiplied()) { + extraSamples[0] = + BaselineTIFFTagSet.EXTRA_SAMPLES_ASSOCIATED_ALPHA; + } else { + extraSamples[0] = + BaselineTIFFTagSet.EXTRA_SAMPLES_UNASSOCIATED_ALPHA; + } + } + + if (numBands == 3) { + this.nativePhotometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB; + if (photometricInterpretation == -1) { + photometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB; + } + } else if (sm.getNumBands() == 1 && cm instanceof IndexColorModel) { + IndexColorModel icm = (IndexColorModel)cm; + int r0 = icm.getRed(0); + int r1 = icm.getRed(1); + if (icm.getMapSize() == 2 && + (r0 == icm.getGreen(0)) && (r0 == icm.getBlue(0)) && + (r1 == icm.getGreen(1)) && (r1 == icm.getBlue(1)) && + (r0 == 0 || r0 == 255) && + (r1 == 0 || r1 == 255) && + (r0 != r1)) { + // Black/white image + + if (r0 == 0) { + nativePhotometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO; + } else { + nativePhotometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; + } + + + // If photometricInterpretation is already set to + // WhiteIsZero or BlackIsZero, leave it alone + if (photometricInterpretation != + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && + photometricInterpretation != + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) { + photometricInterpretation = + r0 == 0 ? + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO : + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; + } + } else { + nativePhotometricInterpretation = + photometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR; + } + } else { + if(cm != null) { + switch(cm.getColorSpace().getType()) { + case ColorSpace.TYPE_Lab: + nativePhotometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CIELAB; + break; + case ColorSpace.TYPE_YCbCr: + nativePhotometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR; + break; + case ColorSpace.TYPE_CMYK: + nativePhotometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CMYK; + break; + default: + nativePhotometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO; + } + } else { + nativePhotometricInterpretation = + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO; + } + if (photometricInterpretation == -1) { + photometricInterpretation = nativePhotometricInterpretation; + } + } + + // Emit compression tag + + int compressionMode = param.getCompressionMode(); + switch(compressionMode) { + case ImageWriteParam.MODE_EXPLICIT: + { + String compressionType = param.getCompressionType(); + if (compressionType == null) { + this.compression = BaselineTIFFTagSet.COMPRESSION_NONE; + } else { + // Determine corresponding compression tag value. + int len = compressionTypes.length; + for (int i = 0; i < len; i++) { + if (compressionType.equals(compressionTypes[i])) { + this.compression = compressionNumbers[i]; + } + } + } + } + break; + case ImageWriteParam.MODE_COPY_FROM_METADATA: + { + TIFFField compField = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_COMPRESSION); + if(compField != null) { + this.compression = compField.getAsInt(0); + } else { + this.compression = BaselineTIFFTagSet.COMPRESSION_NONE; + } + } + break; + default: + this.compression = BaselineTIFFTagSet.COMPRESSION_NONE; + } + + TIFFField predictorField = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PREDICTOR); + if (predictorField != null) { + this.predictor = predictorField.getAsInt(0); + + // We only support Horizontal Predictor for a bitDepth of 8 + if (sampleSize[0] != 8 || + // Check the value of the tag for validity + (predictor != BaselineTIFFTagSet.PREDICTOR_NONE && + predictor != + BaselineTIFFTagSet.PREDICTOR_HORIZONTAL_DIFFERENCING)) { + // Set to default + predictor = BaselineTIFFTagSet.PREDICTOR_NONE; + + // Emit this changed predictor value to metadata + TIFFField newPredictorField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_PREDICTOR), + predictor); + rootIFD.addTIFFField(newPredictorField); + } + } + + TIFFField compressionField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_COMPRESSION), + compression); + rootIFD.addTIFFField(compressionField); + + // Set Exif flag. Note that there is no way to determine definitively + // when an uncompressed thumbnail is being written as the Exif IFD + // pointer field is optional for thumbnails. + boolean isExif = false; + if(numBands == 3 && + sampleSize[0] == 8 && sampleSize[1] == 8 && sampleSize[2] == 8) { + // Three bands with 8 bits per sample. + if(rootIFD.getTIFFField(ExifParentTIFFTagSet.TAG_EXIF_IFD_POINTER) + != null) { + // Exif IFD pointer present. + if(compression == BaselineTIFFTagSet.COMPRESSION_NONE && + (photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB || + photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR)) { + // Uncompressed RGB or YCbCr. + isExif = true; + } else if(compression == + BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { + // Compressed. + isExif = true; + } + } else if(compressionMode == ImageWriteParam.MODE_EXPLICIT && + EXIF_JPEG_COMPRESSION_TYPE.equals + (param.getCompressionType())) { + // Exif IFD pointer absent but Exif JPEG compression set. + isExif = true; + } + } + + // Initialize JPEG interchange format flag which is used to + // indicate that the image is stored as a single JPEG stream. + // This flag is separated from the 'isExif' flag in case JPEG + // interchange format is eventually supported for non-Exif images. + boolean isJPEGInterchange = + isExif && compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG; + + this.compressor = null; + if (compression == BaselineTIFFTagSet.COMPRESSION_CCITT_RLE) { + compressor = new TIFFRLECompressor(); + + if (!forcePhotometricInterpretation) { + photometricInterpretation + = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; + } + } else if (compression + == BaselineTIFFTagSet.COMPRESSION_CCITT_T_4) { + compressor = new TIFFT4Compressor(); + + if (!forcePhotometricInterpretation) { + photometricInterpretation + = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; + } + } else if (compression + == BaselineTIFFTagSet.COMPRESSION_CCITT_T_6) { + compressor = new TIFFT6Compressor(); + + if (!forcePhotometricInterpretation) { + photometricInterpretation + = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO; + } + } else if (compression + == BaselineTIFFTagSet.COMPRESSION_LZW) { + compressor = new TIFFLZWCompressor(predictor); + } else if (compression + == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { + if (isExif) { + compressor = new TIFFExifJPEGCompressor(param); + } else { + throw new IIOException("Old JPEG compression not supported!"); + } + } else if (compression + == BaselineTIFFTagSet.COMPRESSION_JPEG) { + if (numBands == 3 && sampleSize[0] == 8 + && sampleSize[1] == 8 && sampleSize[2] == 8) { + photometricInterpretation + = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR; + } else if (numBands == 1 && sampleSize[0] == 8) { + photometricInterpretation + = BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO; + } else { + throw new IIOException("JPEG compression supported for 1- and 3-band byte images only!"); + } + compressor = new TIFFJPEGCompressor(param); + } else if (compression + == BaselineTIFFTagSet.COMPRESSION_ZLIB) { + compressor = new TIFFZLibCompressor(param, predictor); + } else if (compression + == BaselineTIFFTagSet.COMPRESSION_PACKBITS) { + compressor = new TIFFPackBitsCompressor(); + } else if (compression + == BaselineTIFFTagSet.COMPRESSION_DEFLATE) { + compressor = new TIFFDeflateCompressor(param, predictor); + } else { + // Determine inverse fill setting. + f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_FILL_ORDER); + boolean inverseFill = (f != null && f.getAsInt(0) == 2); + + if (inverseFill) { + compressor = new TIFFLSBCompressor(); + } else { + compressor = new TIFFNullCompressor(); + } + } + + + this.colorConverter = null; + if (cm != null + && cm.getColorSpace().getType() == ColorSpace.TYPE_RGB) { + // + // Perform color conversion only if image has RGB color space. + // + if (photometricInterpretation + == BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR + && compression + != BaselineTIFFTagSet.COMPRESSION_JPEG) { + // + // Convert RGB to YCbCr only if compression type is not + // JPEG in which case this is handled implicitly by the + // compressor. + // + colorConverter = new TIFFYCbCrColorConverter(imageMetadata); + } else if (photometricInterpretation + == BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CIELAB) { + colorConverter = new TIFFCIELabColorConverter(); + } + } + + // + // Cannot at this time do YCbCr subsampling so set the + // YCbCrSubsampling field value to [1, 1] and the YCbCrPositioning + // field value to "cosited". + // + if(photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR && + compression != + BaselineTIFFTagSet.COMPRESSION_JPEG) { + // Remove old subsampling and positioning fields. + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING); + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING); + + // Add unity chrominance subsampling factors. + rootIFD.addTIFFField + (new TIFFField + (base.getTag(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING), + TIFFTag.TIFF_SHORT, + 2, + new char[] {(char)1, (char)1})); + + // Add cosited positioning. + rootIFD.addTIFFField + (new TIFFField + (base.getTag(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING), + TIFFTag.TIFF_SHORT, + 1, + new char[] { + (char)BaselineTIFFTagSet.Y_CB_CR_POSITIONING_COSITED + })); + } + + TIFFField photometricInterpretationField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION), + photometricInterpretation); + rootIFD.addTIFFField(photometricInterpretationField); + + this.bitsPerSample = new char[numBands + numExtraSamples]; + this.bitDepth = 0; + for (int i = 0; i < numBands; i++) { + this.bitDepth = Math.max(bitDepth, sampleSize[i]); + } + if (bitDepth == 3) { + bitDepth = 4; + } else if (bitDepth > 4 && bitDepth < 8) { + bitDepth = 8; + } else if (bitDepth > 8 && bitDepth < 16) { + bitDepth = 16; + } else if (bitDepth > 16 && bitDepth < 32) { + bitDepth = 32; + } else if (bitDepth > 32) { + bitDepth = 64; + } + + for (int i = 0; i < bitsPerSample.length; i++) { + bitsPerSample[i] = (char)bitDepth; + } + + // Emit BitsPerSample. If the image is bilevel, emit if and only + // if already in the metadata and correct (count and value == 1). + if (bitsPerSample.length != 1 || bitsPerSample[0] != 1) { + TIFFField bitsPerSampleField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE), + TIFFTag.TIFF_SHORT, + bitsPerSample.length, + bitsPerSample); + rootIFD.addTIFFField(bitsPerSampleField); + } else { // bitsPerSample.length == 1 && bitsPerSample[0] == 1 + TIFFField bitsPerSampleField = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE); + if(bitsPerSampleField != null) { + int[] bps = bitsPerSampleField.getAsInts(); + if(bps == null || bps.length != 1 || bps[0] != 1) { + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE); + } + } + } + + // Prepare SampleFormat field. + f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_SAMPLE_FORMAT); + if(f == null && (bitDepth == 16 || bitDepth == 32 || bitDepth == 64)) { + // Set up default content for 16-, 32-, and 64-bit cases. + char sampleFormatValue; + int dataType = sm.getDataType(); + if(bitDepth == 16 && dataType == DataBuffer.TYPE_USHORT) { + sampleFormatValue = + (char)BaselineTIFFTagSet.SAMPLE_FORMAT_UNSIGNED_INTEGER; + } else if((bitDepth == 32 && dataType == DataBuffer.TYPE_FLOAT) || + (bitDepth == 64 && dataType == DataBuffer.TYPE_DOUBLE)) { + sampleFormatValue = + (char)BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT; + } else { + sampleFormatValue = + BaselineTIFFTagSet.SAMPLE_FORMAT_SIGNED_INTEGER; + } + this.sampleFormat = (int)sampleFormatValue; + char[] sampleFormatArray = new char[bitsPerSample.length]; + Arrays.fill(sampleFormatArray, sampleFormatValue); + + // Update the metadata. + TIFFTag sampleFormatTag = + base.getTag(BaselineTIFFTagSet.TAG_SAMPLE_FORMAT); + + TIFFField sampleFormatField = + new TIFFField(sampleFormatTag, TIFFTag.TIFF_SHORT, + sampleFormatArray.length, sampleFormatArray); + + rootIFD.addTIFFField(sampleFormatField); + } else if(f != null) { + // Get whatever was provided. + sampleFormat = f.getAsInt(0); + } else { + // Set default value for internal use only. + sampleFormat = BaselineTIFFTagSet.SAMPLE_FORMAT_UNDEFINED; + } + + if (extraSamples != null) { + TIFFField extraSamplesField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_EXTRA_SAMPLES), + TIFFTag.TIFF_SHORT, + extraSamples.length, + extraSamples); + rootIFD.addTIFFField(extraSamplesField); + } else { + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_EXTRA_SAMPLES); + } + + TIFFField samplesPerPixelField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_SAMPLES_PER_PIXEL), + bitsPerSample.length); + rootIFD.addTIFFField(samplesPerPixelField); + + // Emit ColorMap if image is of palette color type + if (photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR && + cm instanceof IndexColorModel) { + char[] colorMap = new char[3*(1 << bitsPerSample[0])]; + + IndexColorModel icm = (IndexColorModel)cm; + + // mapSize is determined by BitsPerSample, not by incoming ICM. + int mapSize = 1 << bitsPerSample[0]; + int indexBound = Math.min(mapSize, icm.getMapSize()); + for (int i = 0; i < indexBound; i++) { + colorMap[i] = (char)((icm.getRed(i)*65535)/255); + colorMap[mapSize + i] = (char)((icm.getGreen(i)*65535)/255); + colorMap[2*mapSize + i] = (char)((icm.getBlue(i)*65535)/255); + } + + TIFFField colorMapField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_COLOR_MAP), + TIFFTag.TIFF_SHORT, + colorMap.length, + colorMap); + rootIFD.addTIFFField(colorMapField); + } else { + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_COLOR_MAP); + } + + // Emit ICCProfile if there is no ICCProfile field already in the + // metadata and the ColorSpace is non-standard ICC. + if(cm != null && + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_ICC_PROFILE) == null && + ImageUtil.isNonStandardICCColorSpace(cm.getColorSpace())) { + ICC_ColorSpace iccColorSpace = (ICC_ColorSpace)cm.getColorSpace(); + byte[] iccProfileData = iccColorSpace.getProfile().getData(); + TIFFField iccProfileField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_ICC_PROFILE), + TIFFTag.TIFF_UNDEFINED, + iccProfileData.length, + iccProfileData); + rootIFD.addTIFFField(iccProfileField); + } + + // Always emit XResolution and YResolution. + + TIFFField XResolutionField = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_X_RESOLUTION); + TIFFField YResolutionField = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_Y_RESOLUTION); + + if(XResolutionField == null && YResolutionField == null) { + long[][] resRational = new long[1][2]; + resRational[0] = new long[2]; + + TIFFField ResolutionUnitField = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_RESOLUTION_UNIT); + + // Don't force dimensionless if one of the other dimensional + // quantities is present. + if(ResolutionUnitField == null && + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_X_POSITION) == null && + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_Y_POSITION) == null) { + // Set resolution to unit and units to dimensionless. + resRational[0][0] = 1; + resRational[0][1] = 1; + + ResolutionUnitField = + new TIFFField(rootIFD.getTag + (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT), + BaselineTIFFTagSet.RESOLUTION_UNIT_NONE); + rootIFD.addTIFFField(ResolutionUnitField); + } else { + // Set resolution to a value which would make the maximum + // image dimension equal to 4 inches as arbitrarily stated + // in the description of ResolutionUnit in the TIFF 6.0 + // specification. If the ResolutionUnit field specifies + // "none" then set the resolution to unity (1/1). + int resolutionUnit = ResolutionUnitField != null ? + ResolutionUnitField.getAsInt(0) : + BaselineTIFFTagSet.RESOLUTION_UNIT_INCH; + int maxDimension = Math.max(destWidth, destHeight); + switch(resolutionUnit) { + case BaselineTIFFTagSet.RESOLUTION_UNIT_INCH: + resRational[0][0] = maxDimension; + resRational[0][1] = 4; + break; + case BaselineTIFFTagSet.RESOLUTION_UNIT_CENTIMETER: + resRational[0][0] = 100L*maxDimension; // divide out 100 + resRational[0][1] = 4*254; // 2.54 cm/inch * 100 + break; + default: + resRational[0][0] = 1; + resRational[0][1] = 1; + } + } + + XResolutionField = + new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_X_RESOLUTION), + TIFFTag.TIFF_RATIONAL, + 1, + resRational); + rootIFD.addTIFFField(XResolutionField); + + YResolutionField = + new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_Y_RESOLUTION), + TIFFTag.TIFF_RATIONAL, + 1, + resRational); + rootIFD.addTIFFField(YResolutionField); + } else if(XResolutionField == null && YResolutionField != null) { + // Set XResolution to YResolution. + long[] yResolution = + YResolutionField.getAsRational(0).clone(); + XResolutionField = + new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_X_RESOLUTION), + TIFFTag.TIFF_RATIONAL, + 1, + yResolution); + rootIFD.addTIFFField(XResolutionField); + } else if(XResolutionField != null && YResolutionField == null) { + // Set YResolution to XResolution. + long[] xResolution = + XResolutionField.getAsRational(0).clone(); + YResolutionField = + new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_Y_RESOLUTION), + TIFFTag.TIFF_RATIONAL, + 1, + xResolution); + rootIFD.addTIFFField(YResolutionField); + } + + // Set mandatory fields, overriding metadata passed in + + int width = destWidth; + TIFFField imageWidthField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_IMAGE_WIDTH), + width); + rootIFD.addTIFFField(imageWidthField); + + int height = destHeight; + TIFFField imageLengthField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_IMAGE_LENGTH), + height); + rootIFD.addTIFFField(imageLengthField); + + // Determine rowsPerStrip + + int rowsPerStrip; + + TIFFField rowsPerStripField = + rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP); + if (rowsPerStripField != null) { + rowsPerStrip = rowsPerStripField.getAsInt(0); + if(rowsPerStrip < 0) { + rowsPerStrip = height; + } + } else { + int bitsPerPixel = bitDepth*(numBands + numExtraSamples); + int bytesPerRow = (bitsPerPixel*width + 7)/8; + rowsPerStrip = + Math.max(Math.max(DEFAULT_BYTES_PER_STRIP/bytesPerRow, 1), 8); + } + rowsPerStrip = Math.min(rowsPerStrip, height); + + // Tiling flag. + boolean useTiling = false; + + // Analyze tiling parameters + int tilingMode = param.getTilingMode(); + if (tilingMode == ImageWriteParam.MODE_DISABLED || + tilingMode == ImageWriteParam.MODE_DEFAULT) { + this.tileWidth = width; + this.tileLength = rowsPerStrip; + useTiling = false; + } else if (tilingMode == ImageWriteParam.MODE_EXPLICIT) { + tileWidth = param.getTileWidth(); + tileLength = param.getTileHeight(); + useTiling = true; + } else if (tilingMode == ImageWriteParam.MODE_COPY_FROM_METADATA) { + f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_WIDTH); + if (f == null) { + tileWidth = width; + useTiling = false; + } else { + tileWidth = f.getAsInt(0); + useTiling = true; + } + + f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_LENGTH); + if (f == null) { + tileLength = rowsPerStrip; + } else { + tileLength = f.getAsInt(0); + useTiling = true; + } + } else { + throw new IIOException("Illegal value of tilingMode!"); + } + + if(compression == BaselineTIFFTagSet.COMPRESSION_JPEG) { + // Reset tile size per TTN2 spec for JPEG compression. + int subX; + int subY; + if(numBands == 1) { + subX = subY = 1; + } else { + subX = subY = TIFFJPEGCompressor.CHROMA_SUBSAMPLING; + } + if(useTiling) { + int MCUMultipleX = 8*subX; + int MCUMultipleY = 8*subY; + tileWidth = + Math.max(MCUMultipleX*((tileWidth + + MCUMultipleX/2)/MCUMultipleX), + MCUMultipleX); + tileLength = + Math.max(MCUMultipleY*((tileLength + + MCUMultipleY/2)/MCUMultipleY), + MCUMultipleY); + } else if(rowsPerStrip < height) { + int MCUMultiple = 8*Math.max(subX, subY); + rowsPerStrip = tileLength = + Math.max(MCUMultiple*((tileLength + + MCUMultiple/2)/MCUMultiple), + MCUMultiple); + } + + // The written image may be unreadable if these fields are present. + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + + // Also remove fields related to the old JPEG encoding scheme + // which may be misleading when the compression type is JPEG. + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_PROC); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_RESTART_INTERVAL); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_LOSSLESS_PREDICTORS); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_POINT_TRANSFORMS); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_Q_TABLES); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_DC_TABLES); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_AC_TABLES); + } else if(isJPEGInterchange) { + // Force tile size to equal image size. + tileWidth = width; + tileLength = height; + } else if(useTiling) { + // Round tile size to multiple of 16 per TIFF 6.0 specification + // (see pages 67-68 of version 6.0.1 from Adobe). + int tileWidthRemainder = tileWidth % 16; + if(tileWidthRemainder != 0) { + // Round to nearest multiple of 16 not less than 16. + tileWidth = Math.max(16*((tileWidth + 8)/16), 16); + processWarningOccurred(currentImage, + "Tile width rounded to multiple of 16."); + } + + int tileLengthRemainder = tileLength % 16; + if(tileLengthRemainder != 0) { + // Round to nearest multiple of 16 not less than 16. + tileLength = Math.max(16*((tileLength + 8)/16), 16); + processWarningOccurred(currentImage, + "Tile height rounded to multiple of 16."); + } + } + + this.tilesAcross = (width + tileWidth - 1)/tileWidth; + this.tilesDown = (height + tileLength - 1)/tileLength; + + if (!useTiling) { + this.isTiled = false; + + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_WIDTH); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_LENGTH); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_OFFSETS); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_BYTE_COUNTS); + + rowsPerStripField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP), + rowsPerStrip); + rootIFD.addTIFFField(rowsPerStripField); + + TIFFField stripOffsetsField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_STRIP_OFFSETS), + TIFFTag.TIFF_LONG, + tilesDown); + rootIFD.addTIFFField(stripOffsetsField); + + TIFFField stripByteCountsField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS), + TIFFTag.TIFF_LONG, + tilesDown); + rootIFD.addTIFFField(stripByteCountsField); + } else { + this.isTiled = true; + + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS); + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS); + + TIFFField tileWidthField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_TILE_WIDTH), + tileWidth); + rootIFD.addTIFFField(tileWidthField); + + TIFFField tileLengthField = + new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_TILE_LENGTH), + tileLength); + rootIFD.addTIFFField(tileLengthField); + + TIFFField tileOffsetsField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_TILE_OFFSETS), + TIFFTag.TIFF_LONG, + tilesDown*tilesAcross); + rootIFD.addTIFFField(tileOffsetsField); + + TIFFField tileByteCountsField = + new TIFFField( + base.getTag(BaselineTIFFTagSet.TAG_TILE_BYTE_COUNTS), + TIFFTag.TIFF_LONG, + tilesDown*tilesAcross); + rootIFD.addTIFFField(tileByteCountsField); + } + + if(isExif) { + // + // Ensure presence of mandatory fields and absence of prohibited + // fields and those that duplicate information in JPEG marker + // segments per tables 14-18 of the Exif 2.2 specification. + // + + // If an empty image is being written or inserted then infer + // that the primary IFD is being set up. + boolean isPrimaryIFD = isEncodingEmpty(); + + // Handle TIFF fields in order of increasing tag number. + if(compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { + // ImageWidth + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_IMAGE_WIDTH); + + // ImageLength + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_IMAGE_LENGTH); + + // BitsPerSample + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE); + // Compression + if(isPrimaryIFD) { + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_COMPRESSION); + } + + // PhotometricInterpretation + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION); + + // StripOffsets + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS); + + // SamplesPerPixel + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_SAMPLES_PER_PIXEL); + + // RowsPerStrip + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP); + + // StripByteCounts + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS); + // XResolution and YResolution are handled above for all TIFFs. + + // PlanarConfiguration + rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION); + + // ResolutionUnit + if(rootIFD.getTIFFField + (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT) == null) { + f = new TIFFField(base.getTag + (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT), + BaselineTIFFTagSet.RESOLUTION_UNIT_INCH); + rootIFD.addTIFFField(f); + } + + if(isPrimaryIFD) { + // JPEGInterchangeFormat + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT); + + // JPEGInterchangeFormatLength + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + + // YCbCrSubsampling + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING); + + // YCbCrPositioning + if(rootIFD.getTIFFField + (BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING) == null) { + f = new TIFFField + (base.getTag + (BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING), + TIFFTag.TIFF_SHORT, + 1, + new char[] { + (char)BaselineTIFFTagSet.Y_CB_CR_POSITIONING_CENTERED + }); + rootIFD.addTIFFField(f); + } + } else { // Thumbnail IFD + // JPEGInterchangeFormat + f = new TIFFField + (base.getTag + (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT), + TIFFTag.TIFF_LONG, + 1); + rootIFD.addTIFFField(f); + + // JPEGInterchangeFormatLength + f = new TIFFField + (base.getTag + (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH), + TIFFTag.TIFF_LONG, + 1); + rootIFD.addTIFFField(f); + + // YCbCrSubsampling + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING); + } + } else { // Uncompressed + // ImageWidth through PlanarConfiguration are set above. + // XResolution and YResolution are handled above for all TIFFs. + + // ResolutionUnit + if(rootIFD.getTIFFField + (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT) == null) { + f = new TIFFField(base.getTag + (BaselineTIFFTagSet.TAG_RESOLUTION_UNIT), + BaselineTIFFTagSet.RESOLUTION_UNIT_INCH); + rootIFD.addTIFFField(f); + } + + + // JPEGInterchangeFormat + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT); + + // JPEGInterchangeFormatLength + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + + if(photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB) { + // YCbCrCoefficients + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_Y_CB_CR_COEFFICIENTS); + + // YCbCrSubsampling + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING); + + // YCbCrPositioning + rootIFD.removeTIFFField + (BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING); + } + } + + // Get Exif tags. + TIFFTagSet exifTags = ExifTIFFTagSet.getInstance(); + + // Retrieve or create the Exif IFD. + TIFFIFD exifIFD = null; + f = rootIFD.getTIFFField + (ExifParentTIFFTagSet.TAG_EXIF_IFD_POINTER); + if(f != null && f.hasDirectory()) { + // Retrieve the Exif IFD. + exifIFD = (TIFFIFD)f.getDirectory(); + } else if(isPrimaryIFD) { + // Create the Exif IFD. + List exifTagSets = new ArrayList(1); + exifTagSets.add(exifTags); + exifIFD = new TIFFIFD(exifTagSets); + + // Add it to the root IFD. + TIFFTagSet tagSet = ExifParentTIFFTagSet.getInstance(); + TIFFTag exifIFDTag = + tagSet.getTag(ExifParentTIFFTagSet.TAG_EXIF_IFD_POINTER); + rootIFD.addTIFFField(new TIFFField(exifIFDTag, + TIFFTag.TIFF_LONG, + 1L, + exifIFD)); + } + + if(exifIFD != null) { + // Handle Exif private fields in order of increasing + // tag number. + + // ExifVersion + if(exifIFD.getTIFFField + (ExifTIFFTagSet.TAG_EXIF_VERSION) == null) { + f = new TIFFField + (exifTags.getTag(ExifTIFFTagSet.TAG_EXIF_VERSION), + TIFFTag.TIFF_UNDEFINED, + 4, + ExifTIFFTagSet.EXIF_VERSION_2_2.getBytes(StandardCharsets.US_ASCII)); + exifIFD.addTIFFField(f); + } + + if(compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { + // ComponentsConfiguration + if(exifIFD.getTIFFField + (ExifTIFFTagSet.TAG_COMPONENTS_CONFIGURATION) == null) { + f = new TIFFField + (exifTags.getTag + (ExifTIFFTagSet.TAG_COMPONENTS_CONFIGURATION), + TIFFTag.TIFF_UNDEFINED, + 4, + new byte[] { + (byte)ExifTIFFTagSet.COMPONENTS_CONFIGURATION_Y, + (byte)ExifTIFFTagSet.COMPONENTS_CONFIGURATION_CB, + (byte)ExifTIFFTagSet.COMPONENTS_CONFIGURATION_CR, + (byte)0 + }); + exifIFD.addTIFFField(f); + } + } else { + // ComponentsConfiguration + exifIFD.removeTIFFField + (ExifTIFFTagSet.TAG_COMPONENTS_CONFIGURATION); + + // CompressedBitsPerPixel + exifIFD.removeTIFFField + (ExifTIFFTagSet.TAG_COMPRESSED_BITS_PER_PIXEL); + } + + // FlashpixVersion + if(exifIFD.getTIFFField + (ExifTIFFTagSet.TAG_FLASHPIX_VERSION) == null) { + f = new TIFFField + (exifTags.getTag(ExifTIFFTagSet.TAG_FLASHPIX_VERSION), + TIFFTag.TIFF_UNDEFINED, + 4, + new byte[] {(byte)'0', (byte)'1', (byte)'0', (byte)'0'}); + exifIFD.addTIFFField(f); + } + + // ColorSpace + if(exifIFD.getTIFFField + (ExifTIFFTagSet.TAG_COLOR_SPACE) == null) { + f = new TIFFField + (exifTags.getTag(ExifTIFFTagSet.TAG_COLOR_SPACE), + TIFFTag.TIFF_SHORT, + 1, + new char[] { + (char)ExifTIFFTagSet.COLOR_SPACE_SRGB + }); + exifIFD.addTIFFField(f); + } + + if(compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) { + // PixelXDimension + if(exifIFD.getTIFFField + (ExifTIFFTagSet.TAG_PIXEL_X_DIMENSION) == null) { + f = new TIFFField + (exifTags.getTag(ExifTIFFTagSet.TAG_PIXEL_X_DIMENSION), + width); + exifIFD.addTIFFField(f); + } + + // PixelYDimension + if(exifIFD.getTIFFField + (ExifTIFFTagSet.TAG_PIXEL_Y_DIMENSION) == null) { + f = new TIFFField + (exifTags.getTag(ExifTIFFTagSet.TAG_PIXEL_Y_DIMENSION), + height); + exifIFD.addTIFFField(f); + } + } else { + exifIFD.removeTIFFField + (ExifTIFFTagSet.TAG_INTEROPERABILITY_IFD_POINTER); + } + } + + } // if(isExif) + } + + ImageTypeSpecifier getImageType() { + return imageType; + } + + /** + @param tileRect The area to be written which might be outside the image. + */ + private int writeTile(Rectangle tileRect, TIFFCompressor compressor) + throws IOException { + // Determine the rectangle which will actually be written + // and set the padding flag. Padding will occur only when the + // image is written as a tiled TIFF and the tile bounds are not + // contained within the image bounds. + Rectangle activeRect; + boolean isPadded; + Rectangle imageBounds = + new Rectangle(image.getMinX(), image.getMinY(), + image.getWidth(), image.getHeight()); + if(!isTiled) { + // Stripped + activeRect = tileRect.intersection(imageBounds); + tileRect = activeRect; + isPadded = false; + } else if(imageBounds.contains(tileRect)) { + // Tiled, tile within image bounds + activeRect = tileRect; + isPadded = false; + } else { + // Tiled, tile not within image bounds + activeRect = imageBounds.intersection(tileRect); + isPadded = true; + } + + // Return early if empty intersection. + if(activeRect.isEmpty()) { + return 0; + } + + int minX = tileRect.x; + int minY = tileRect.y; + int width = tileRect.width; + int height = tileRect.height; + + if(isImageSimple) { + + SampleModel sm = image.getSampleModel(); + + // Read only data from the active rectangle. + Raster raster = image.getData(activeRect); + + // If padding is required, create a larger Raster and fill + // it from the active rectangle. + if(isPadded) { + WritableRaster wr = + raster.createCompatibleWritableRaster(minX, minY, + width, height); + wr.setRect(raster); + raster = wr; + } + + if(isBilevel) { + byte[] buf = ImageUtil.getPackedBinaryData(raster, + tileRect); + + if(isInverted) { + DataBuffer dbb = raster.getDataBuffer(); + if(dbb instanceof DataBufferByte && + buf == ((DataBufferByte)dbb).getData()) { + byte[] bbuf = new byte[buf.length]; + int len = buf.length; + for(int i = 0; i < len; i++) { + bbuf[i] = (byte)(buf[i] ^ 0xff); + } + buf = bbuf; + } else { + int len = buf.length; + for(int i = 0; i < len; i++) { + buf[i] ^= 0xff; + } + } + } + + return compressor.encode(buf, 0, + width, height, sampleSize, + (tileRect.width + 7)/8); + } else if(bitDepth == 8 && + sm.getDataType() == DataBuffer.TYPE_BYTE) { + ComponentSampleModel csm = + (ComponentSampleModel)raster.getSampleModel(); + + byte[] buf = + ((DataBufferByte)raster.getDataBuffer()).getData(); + + int off = + csm.getOffset(minX - + raster.getSampleModelTranslateX(), + minY - + raster.getSampleModelTranslateY()); + + return compressor.encode(buf, off, + width, height, sampleSize, + csm.getScanlineStride()); + } + } + + // Set offsets and skips based on source subsampling factors + int xOffset = minX; + int xSkip = periodX; + int yOffset = minY; + int ySkip = periodY; + + // Early exit if no data for this pass + int hpixels = (width + xSkip - 1)/xSkip; + int vpixels = (height + ySkip - 1)/ySkip; + if (hpixels == 0 || vpixels == 0) { + return 0; + } + + // Convert X offset and skip from pixels to samples + xOffset *= numBands; + xSkip *= numBands; + + // Initialize sizes + int samplesPerByte = 8/bitDepth; + int numSamples = width*numBands; + int bytesPerRow = hpixels*numBands; + + // Update number of bytes per row. + if (bitDepth < 8) { + bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte; + } else if (bitDepth == 16) { + bytesPerRow *= 2; + } else if (bitDepth == 32) { + bytesPerRow *= 4; + } else if (bitDepth == 64) { + bytesPerRow *= 8; + } + + // Create row buffers + int[] samples = null; + float[] fsamples = null; + double[] dsamples = null; + if(sampleFormat == BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { + if (bitDepth == 32) { + fsamples = new float[numSamples]; + } else { + dsamples = new double[numSamples]; + } + } else { + samples = new int[numSamples]; + } + + // Create tile buffer + byte[] currTile = new byte[bytesPerRow*vpixels]; + + // Sub-optimal case: shy of "isImageSimple" only by virtue of + // not being contiguous. + if(!isInverted && // no inversion + !isRescaling && // no value rescaling + sourceBands == null && // no subbanding + periodX == 1 && periodY == 1 && // no subsampling + colorConverter == null) { + + SampleModel sm = image.getSampleModel(); + + if(sm instanceof ComponentSampleModel && // component + bitDepth == 8 && // 8 bits/sample + sm.getDataType() == DataBuffer.TYPE_BYTE) { // byte type + + // Read only data from the active rectangle. + Raster raster = image.getData(activeRect); + + // If padding is required, create a larger Raster and fill + // it from the active rectangle. + if(isPadded) { + WritableRaster wr = + raster.createCompatibleWritableRaster(minX, minY, + width, height); + wr.setRect(raster); + raster = wr; + } + + // Get SampleModel info. + ComponentSampleModel csm = + (ComponentSampleModel)raster.getSampleModel(); + int[] bankIndices = csm.getBankIndices(); + byte[][] bankData = + ((DataBufferByte)raster.getDataBuffer()).getBankData(); + int lineStride = csm.getScanlineStride(); + int pixelStride = csm.getPixelStride(); + + // Copy the data into a contiguous pixel interleaved buffer. + for(int k = 0; k < numBands; k++) { + byte[] bandData = bankData[bankIndices[k]]; + int lineOffset = + csm.getOffset(raster.getMinX() - + raster.getSampleModelTranslateX(), + raster.getMinY() - + raster.getSampleModelTranslateY(), k); + int idx = k; + for(int j = 0; j < vpixels; j++) { + int offset = lineOffset; + for(int i = 0; i < hpixels; i++) { + currTile[idx] = bandData[offset]; + idx += numBands; + offset += pixelStride; + } + lineOffset += lineStride; + } + } + + // Compressor and return. + return compressor.encode(currTile, 0, + width, height, sampleSize, + width*numBands); + } + } + + int tcount = 0; + + // Save active rectangle variables. + int activeMinX = activeRect.x; + int activeMinY = activeRect.y; + int activeMaxY = activeMinY + activeRect.height - 1; + int activeWidth = activeRect.width; + + // Set a SampleModel for use in padding. + SampleModel rowSampleModel = null; + if(isPadded) { + rowSampleModel = + image.getSampleModel().createCompatibleSampleModel(width, 1); + } + + for (int row = yOffset; row < yOffset + height; row += ySkip) { + Raster ras = null; + if(isPadded) { + // Create a raster for the entire row. + WritableRaster wr = + Raster.createWritableRaster(rowSampleModel, + new Point(minX, row)); + + // Populate the raster from the active sub-row, if any. + if(row >= activeMinY && row <= activeMaxY) { + Rectangle rect = + new Rectangle(activeMinX, row, activeWidth, 1); + ras = image.getData(rect); + wr.setRect(ras); + } + + // Update the raster variable. + ras = wr; + } else { + Rectangle rect = new Rectangle(minX, row, width, 1); + ras = image.getData(rect); + } + if (sourceBands != null) { + ras = ras.createChild(minX, row, width, 1, minX, row, + sourceBands); + } + + if(sampleFormat == + BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { + if (fsamples != null) { + ras.getPixels(minX, row, width, 1, fsamples); + } else { + ras.getPixels(minX, row, width, 1, dsamples); + } + } else { + ras.getPixels(minX, row, width, 1, samples); + + if ((nativePhotometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && + photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) || + (nativePhotometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO && + photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO)) { + int bitMask = (1 << bitDepth) - 1; + for (int s = 0; s < numSamples; s++) { + samples[s] ^= bitMask; + } + } + } + + if (colorConverter != null) { + int idx = 0; + float[] result = new float[3]; + + if(sampleFormat == + BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { + if (bitDepth == 32) { + for (int i = 0; i < width; i++) { + float r = fsamples[idx]; + float g = fsamples[idx + 1]; + float b = fsamples[idx + 2]; + + colorConverter.fromRGB(r, g, b, result); + + fsamples[idx] = result[0]; + fsamples[idx + 1] = result[1]; + fsamples[idx + 2] = result[2]; + + idx += 3; + } + } else { + for (int i = 0; i < width; i++) { + // Note: Possible loss of precision. + float r = (float)dsamples[idx]; + float g = (float)dsamples[idx + 1]; + float b = (float)dsamples[idx + 2]; + + colorConverter.fromRGB(r, g, b, result); + + dsamples[idx] = result[0]; + dsamples[idx + 1] = result[1]; + dsamples[idx + 2] = result[2]; + + idx += 3; + } + } + } else { + for (int i = 0; i < width; i++) { + float r = (float)samples[idx]; + float g = (float)samples[idx + 1]; + float b = (float)samples[idx + 2]; + + colorConverter.fromRGB(r, g, b, result); + + samples[idx] = (int)(result[0]); + samples[idx + 1] = (int)(result[1]); + samples[idx + 2] = (int)(result[2]); + + idx += 3; + } + } + } + + int tmp = 0; + int pos = 0; + + switch (bitDepth) { + case 1: case 2: case 4: + // Image can only have a single band + + if(isRescaling) { + for (int s = 0; s < numSamples; s += xSkip) { + byte val = scale0[samples[s]]; + tmp = (tmp << bitDepth) | val; + + if (++pos == samplesPerByte) { + currTile[tcount++] = (byte)tmp; + tmp = 0; + pos = 0; + } + } + } else { + for (int s = 0; s < numSamples; s += xSkip) { + byte val = (byte)samples[s]; + tmp = (tmp << bitDepth) | val; + + if (++pos == samplesPerByte) { + currTile[tcount++] = (byte)tmp; + tmp = 0; + pos = 0; + } + } + } + + // Left shift the last byte + if (pos != 0) { + tmp <<= ((8/bitDepth) - pos)*bitDepth; + currTile[tcount++] = (byte)tmp; + } + break; + + case 8: + if (numBands == 1) { + if(isRescaling) { + for (int s = 0; s < numSamples; s += xSkip) { + currTile[tcount++] = scale0[samples[s]]; + } + } else { + for (int s = 0; s < numSamples; s += xSkip) { + currTile[tcount++] = (byte)samples[s]; + } + } + } else { + if(isRescaling) { + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + currTile[tcount++] = scale[b][samples[s + b]]; + } + } + } else { + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + currTile[tcount++] = (byte)samples[s + b]; + } + } + } + } + break; + + case 16: + if(isRescaling) { + if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + int sample = samples[s + b]; + currTile[tcount++] = scaleh[b][sample]; + currTile[tcount++] = scalel[b][sample]; + } + } + } else { // ByteOrder.LITLE_ENDIAN + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + int sample = samples[s + b]; + currTile[tcount++] = scalel[b][sample]; + currTile[tcount++] = scaleh[b][sample]; + } + } + } + } else { + if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + int sample = samples[s + b]; + currTile[tcount++] = + (byte)((sample >>> 8) & 0xff); + currTile[tcount++] = + (byte)(sample & 0xff); + } + } + } else { // ByteOrder.LITLE_ENDIAN + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + int sample = samples[s + b]; + currTile[tcount++] = + (byte)(sample & 0xff); + currTile[tcount++] = + (byte)((sample >>> 8) & 0xff); + } + } + } + } + break; + + case 32: + if(sampleFormat == + BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { + if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + float fsample = fsamples[s + b]; + int isample = Float.floatToIntBits(fsample); + currTile[tcount++] = + (byte)((isample & 0xff000000) >> 24); + currTile[tcount++] = + (byte)((isample & 0x00ff0000) >> 16); + currTile[tcount++] = + (byte)((isample & 0x0000ff00) >> 8); + currTile[tcount++] = + (byte)(isample & 0x000000ff); + } + } + } else { // ByteOrder.LITLE_ENDIAN + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + float fsample = fsamples[s + b]; + int isample = Float.floatToIntBits(fsample); + currTile[tcount++] = + (byte)(isample & 0x000000ff); + currTile[tcount++] = + (byte)((isample & 0x0000ff00) >> 8); + currTile[tcount++] = + (byte)((isample & 0x00ff0000) >> 16); + currTile[tcount++] = + (byte)((isample & 0xff000000) >> 24); + } + } + } + } else { + if(isRescaling) { + long[] maxIn = new long[numBands]; + long[] halfIn = new long[numBands]; + long maxOut = (1L << (long)bitDepth) - 1L; + + for (int b = 0; b < numBands; b++) { + maxIn[b] = ((1L << (long)sampleSize[b]) - 1L); + halfIn[b] = maxIn[b]/2; + } + + if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + long sampleOut = + (samples[s + b]*maxOut + halfIn[b])/ + maxIn[b]; + currTile[tcount++] = + (byte)((sampleOut & 0xff000000) >> 24); + currTile[tcount++] = + (byte)((sampleOut & 0x00ff0000) >> 16); + currTile[tcount++] = + (byte)((sampleOut & 0x0000ff00) >> 8); + currTile[tcount++] = + (byte)(sampleOut & 0x000000ff); + } + } + } else { // ByteOrder.LITLE_ENDIAN + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + long sampleOut = + (samples[s + b]*maxOut + halfIn[b])/ + maxIn[b]; + currTile[tcount++] = + (byte)(sampleOut & 0x000000ff); + currTile[tcount++] = + (byte)((sampleOut & 0x0000ff00) >> 8); + currTile[tcount++] = + (byte)((sampleOut & 0x00ff0000) >> 16); + currTile[tcount++] = + (byte)((sampleOut & 0xff000000) >> 24); + } + } + } + } else { + if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + int isample = samples[s + b]; + currTile[tcount++] = + (byte)((isample & 0xff000000) >> 24); + currTile[tcount++] = + (byte)((isample & 0x00ff0000) >> 16); + currTile[tcount++] = + (byte)((isample & 0x0000ff00) >> 8); + currTile[tcount++] = + (byte)(isample & 0x000000ff); + } + } + } else { // ByteOrder.LITLE_ENDIAN + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + int isample = samples[s + b]; + currTile[tcount++] = + (byte)(isample & 0x000000ff); + currTile[tcount++] = + (byte)((isample & 0x0000ff00) >> 8); + currTile[tcount++] = + (byte)((isample & 0x00ff0000) >> 16); + currTile[tcount++] = + (byte)((isample & 0xff000000) >> 24); + } + } + } + } + } + break; + + case 64: + if(sampleFormat == + BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT) { + if(stream.getByteOrder() == ByteOrder.BIG_ENDIAN) { + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + double dsample = dsamples[s + b]; + long lsample = Double.doubleToLongBits(dsample); + currTile[tcount++] = + (byte)((lsample & 0xff00000000000000L) >> 56); + currTile[tcount++] = + (byte)((lsample & 0x00ff000000000000L) >> 48); + currTile[tcount++] = + (byte)((lsample & 0x0000ff0000000000L) >> 40); + currTile[tcount++] = + (byte)((lsample & 0x000000ff00000000L) >> 32); + currTile[tcount++] = + (byte)((lsample & 0x00000000ff000000L) >> 24); + currTile[tcount++] = + (byte)((lsample & 0x0000000000ff0000L) >> 16); + currTile[tcount++] = + (byte)((lsample & 0x000000000000ff00L) >> 8); + currTile[tcount++] = + (byte)(lsample & 0x00000000000000ffL); + } + } + } else { // ByteOrder.LITLE_ENDIAN + for (int s = 0; s < numSamples; s += xSkip) { + for (int b = 0; b < numBands; b++) { + double dsample = dsamples[s + b]; + long lsample = Double.doubleToLongBits(dsample); + currTile[tcount++] = + (byte)(lsample & 0x00000000000000ffL); + currTile[tcount++] = + (byte)((lsample & 0x000000000000ff00L) >> 8); + currTile[tcount++] = + (byte)((lsample & 0x0000000000ff0000L) >> 16); + currTile[tcount++] = + (byte)((lsample & 0x00000000ff000000L) >> 24); + currTile[tcount++] = + (byte)((lsample & 0x000000ff00000000L) >> 32); + currTile[tcount++] = + (byte)((lsample & 0x0000ff0000000000L) >> 40); + currTile[tcount++] = + (byte)((lsample & 0x00ff000000000000L) >> 48); + currTile[tcount++] = + (byte)((lsample & 0xff00000000000000L) >> 56); + } + } + } + } + break; + } + } + + int[] bitsPerSample = new int[numBands]; + for (int i = 0; i < bitsPerSample.length; i++) { + bitsPerSample[i] = bitDepth; + } + + int byteCount = compressor.encode(currTile, 0, + hpixels, vpixels, + bitsPerSample, + bytesPerRow); + return byteCount; + } + + // Check two int arrays for value equality, always returns false + // if either array is null + private boolean equals(int[] s0, int[] s1) { + if (s0 == null || s1 == null) { + return false; + } + if (s0.length != s1.length) { + return false; + } + for (int i = 0; i < s0.length; i++) { + if (s0[i] != s1[i]) { + return false; + } + } + return true; + } + + // Initialize the scale/scale0 or scaleh/scalel arrays to + // hold the results of scaling an input value to the desired + // output bit depth + private void initializeScaleTables(int[] sampleSize) { + // Save the sample size in the instance variable. + + // If the existing tables are still valid, just return. + if (bitDepth == scalingBitDepth && + equals(sampleSize, this.sampleSize)) { + return; + } + + // Reset scaling variables. + isRescaling = false; + scalingBitDepth = -1; + scale = scalel = scaleh = null; + scale0 = null; + + // Set global sample size to parameter. + this.sampleSize = sampleSize; + + // Check whether rescaling is called for. + if(bitDepth <= 16) { + for(int b = 0; b < numBands; b++) { + if(sampleSize[b] != bitDepth) { + isRescaling = true; + break; + } + } + } + + // If not rescaling then return after saving the sample size. + if(!isRescaling) { + return; + } + + // Compute new tables + this.scalingBitDepth = bitDepth; + int maxOutSample = (1 << bitDepth) - 1; + if (bitDepth <= 8) { + scale = new byte[numBands][]; + for (int b = 0; b < numBands; b++) { + int maxInSample = (1 << sampleSize[b]) - 1; + int halfMaxInSample = maxInSample/2; + scale[b] = new byte[maxInSample + 1]; + for (int s = 0; s <= maxInSample; s++) { + scale[b][s] = + (byte)((s*maxOutSample + halfMaxInSample)/maxInSample); + } + } + scale0 = scale[0]; + scaleh = scalel = null; + } else if(bitDepth <= 16) { + // Divide scaling table into high and low bytes + scaleh = new byte[numBands][]; + scalel = new byte[numBands][]; + + for (int b = 0; b < numBands; b++) { + int maxInSample = (1 << sampleSize[b]) - 1; + int halfMaxInSample = maxInSample/2; + scaleh[b] = new byte[maxInSample + 1]; + scalel[b] = new byte[maxInSample + 1]; + for (int s = 0; s <= maxInSample; s++) { + int val = (s*maxOutSample + halfMaxInSample)/maxInSample; + scaleh[b][s] = (byte)(val >> 8); + scalel[b][s] = (byte)(val & 0xff); + } + } + scale = null; + scale0 = null; + } + } + + public void write(IIOMetadata sm, + IIOImage iioimage, + ImageWriteParam p) throws IOException { + write(sm, iioimage, p, true, true); + } + + private void writeHeader() throws IOException { + if (streamMetadata != null) { + this.byteOrder = streamMetadata.byteOrder; + } else { + this.byteOrder = ByteOrder.BIG_ENDIAN; + } + + stream.setByteOrder(byteOrder); + if (byteOrder == ByteOrder.BIG_ENDIAN) { + stream.writeShort(0x4d4d); + } else { + stream.writeShort(0x4949); + } + + stream.writeShort(42); // Magic number + stream.writeInt(0); // Offset of first IFD (0 == none) + + nextSpace = stream.getStreamPosition(); + headerPosition = nextSpace - 8; + } + + private void write(IIOMetadata sm, + IIOImage iioimage, + ImageWriteParam p, + boolean writeHeader, + boolean writeData) throws IOException { + if (stream == null) { + throw new IllegalStateException("output == null!"); + } + if (iioimage == null) { + throw new NullPointerException("image == null!"); + } + if(iioimage.hasRaster() && !canWriteRasters()) { + throw new UnsupportedOperationException + ("TIFF ImageWriter cannot write Rasters!"); + } + + this.image = iioimage.getRenderedImage(); + SampleModel sampleModel = image.getSampleModel(); + + this.sourceXOffset = image.getMinX(); + this.sourceYOffset = image.getMinY(); + this.sourceWidth = image.getWidth(); + this.sourceHeight = image.getHeight(); + + Rectangle imageBounds = new Rectangle(sourceXOffset, + sourceYOffset, + sourceWidth, + sourceHeight); + + ColorModel colorModel = null; + if (p == null) { + this.param = getDefaultWriteParam(); + this.sourceBands = null; + this.periodX = 1; + this.periodY = 1; + this.numBands = sampleModel.getNumBands(); + colorModel = image.getColorModel(); + } else { + this.param = p; + + // Get source region and subsampling factors + Rectangle sourceRegion = param.getSourceRegion(); + if (sourceRegion != null) { + // Clip to actual image bounds + sourceRegion = sourceRegion.intersection(imageBounds); + + sourceXOffset = sourceRegion.x; + sourceYOffset = sourceRegion.y; + sourceWidth = sourceRegion.width; + sourceHeight = sourceRegion.height; + } + + // Adjust for subsampling offsets + int gridX = param.getSubsamplingXOffset(); + int gridY = param.getSubsamplingYOffset(); + this.sourceXOffset += gridX; + this.sourceYOffset += gridY; + this.sourceWidth -= gridX; + this.sourceHeight -= gridY; + + // Get subsampling factors + this.periodX = param.getSourceXSubsampling(); + this.periodY = param.getSourceYSubsampling(); + + int[] sBands = param.getSourceBands(); + if (sBands != null) { + sourceBands = sBands; + this.numBands = sourceBands.length; + } else { + this.numBands = sampleModel.getNumBands(); + } + + ImageTypeSpecifier destType = p.getDestinationType(); + if(destType != null) { + ColorModel cm = destType.getColorModel(); + if(cm.getNumComponents() == numBands) { + colorModel = cm; + } + } + + if(colorModel == null) { + colorModel = image.getColorModel(); + } + } + + this.imageType = new ImageTypeSpecifier(colorModel, sampleModel); + + ImageUtil.canEncodeImage(this, this.imageType); + + // Compute output dimensions + int destWidth = (sourceWidth + periodX - 1)/periodX; + int destHeight = (sourceHeight + periodY - 1)/periodY; + if (destWidth <= 0 || destHeight <= 0) { + throw new IllegalArgumentException("Empty source region!"); + } + + clearAbortRequest(); + processImageStarted(0); + + // Optionally write the header. + if (writeHeader) { + // Clear previous stream metadata. + this.streamMetadata = null; + + // Try to convert non-null input stream metadata. + if (sm != null) { + this.streamMetadata = + (TIFFStreamMetadata)convertStreamMetadata(sm, param); + } + + // Set to default if not converted. + if(this.streamMetadata == null) { + this.streamMetadata = + (TIFFStreamMetadata)getDefaultStreamMetadata(param); + } + + // Write the header. + writeHeader(); + + // Seek to the position of the IFD pointer in the header. + stream.seek(headerPosition + 4); + + // Ensure IFD is written on a word boundary + nextSpace = (nextSpace + 3) & ~0x3; + + // Write the pointer to the first IFD after the header. + stream.writeInt((int)nextSpace); + } + + // Write out the IFD and any sub IFDs, followed by a zero + + // Clear previous image metadata. + this.imageMetadata = null; + + // Initialize the metadata object. + IIOMetadata im = iioimage.getMetadata(); + if(im != null) { + if (im instanceof TIFFImageMetadata) { + // Clone the one passed in. + this.imageMetadata = ((TIFFImageMetadata)im).getShallowClone(); + } else if(Arrays.asList(im.getMetadataFormatNames()).contains( + TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME)) { + this.imageMetadata = convertNativeImageMetadata(im); + } else if(im.isStandardMetadataFormatSupported()) { + // Convert standard metadata. + this.imageMetadata = convertStandardImageMetadata(im); + } + if (this.imageMetadata == null) { + processWarningOccurred(currentImage, + "Could not initialize image metadata"); + } + } + + // Use default metadata if still null. + if(this.imageMetadata == null) { + this.imageMetadata = + (TIFFImageMetadata)getDefaultImageMetadata(this.imageType, + this.param); + } + + // Set or overwrite mandatory fields in the root IFD + setupMetadata(colorModel, sampleModel, destWidth, destHeight); + + // Set compressor fields. + compressor.setWriter(this); + // Metadata needs to be set on the compressor before the IFD is + // written as the compressor could modify the metadata. + compressor.setMetadata(imageMetadata); + compressor.setStream(stream); + + // Initialize scaling tables for this image + sampleSize = sampleModel.getSampleSize(); + initializeScaleTables(sampleModel.getSampleSize()); + + // Determine whether bilevel. + this.isBilevel = ImageUtil.isBinary(this.image.getSampleModel()); + + // Check for photometric inversion. + this.isInverted = + (nativePhotometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && + photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) || + (nativePhotometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO && + photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO); + + // Analyze image data suitability for direct copy. + this.isImageSimple = + (isBilevel || + (!isInverted && ImageUtil.imageIsContiguous(this.image))) && + !isRescaling && // no value rescaling + sourceBands == null && // no subbanding + periodX == 1 && periodY == 1 && // no subsampling + colorConverter == null; + + TIFFIFD rootIFD = imageMetadata.getRootIFD(); + + rootIFD.writeToStream(stream); + + this.nextIFDPointerPos = stream.getStreamPosition(); + stream.writeInt(0); + + // Seek to end of IFD data + long lastIFDPosition = rootIFD.getLastPosition(); + stream.seek(lastIFDPosition); + if(lastIFDPosition > this.nextSpace) { + this.nextSpace = lastIFDPosition; + } + + // If not writing the image data, i.e., if writing or inserting an + // empty image, return. + if(!writeData) { + return; + } + + // Get positions of fields within the IFD to update as we write + // each strip or tile + long stripOrTileByteCountsPosition = + rootIFD.getStripOrTileByteCountsPosition(); + long stripOrTileOffsetsPosition = + rootIFD.getStripOrTileOffsetsPosition(); + + // Compute total number of pixels for progress notification + this.totalPixels = tileWidth*tileLength*tilesDown*tilesAcross; + this.pixelsDone = 0; + + // Write the image, a strip or tile at a time + for (int tj = 0; tj < tilesDown; tj++) { + for (int ti = 0; ti < tilesAcross; ti++) { + long pos = stream.getStreamPosition(); + + // Write the (possibly compressed) tile data + + Rectangle tileRect = + new Rectangle(sourceXOffset + ti*tileWidth*periodX, + sourceYOffset + tj*tileLength*periodY, + tileWidth*periodX, + tileLength*periodY); + + try { + int byteCount = writeTile(tileRect, compressor); + + if(pos + byteCount > nextSpace) { + nextSpace = pos + byteCount; + } + + pixelsDone += tileRect.width*tileRect.height; + processImageProgress(100.0F*pixelsDone/totalPixels); + + // Fill in the offset and byte count for the file + stream.mark(); + stream.seek(stripOrTileOffsetsPosition); + stream.writeInt((int)pos); + stripOrTileOffsetsPosition += 4; + + stream.seek(stripOrTileByteCountsPosition); + stream.writeInt(byteCount); + stripOrTileByteCountsPosition += 4; + stream.reset(); + } catch (IOException e) { + throw new IIOException("I/O error writing TIFF file!", e); + } + + if (abortRequested()) { + processWriteAborted(); + return; + } + } + } + + processImageComplete(); + currentImage++; + } + + public boolean canWriteSequence() { + return true; + } + + public void prepareWriteSequence(IIOMetadata streamMetadata) + throws IOException { + if (getOutput() == null) { + throw new IllegalStateException("getOutput() == null!"); + } + + // Set up stream metadata. + if (streamMetadata != null) { + streamMetadata = convertStreamMetadata(streamMetadata, null); + } + if(streamMetadata == null) { + streamMetadata = getDefaultStreamMetadata(null); + } + this.streamMetadata = (TIFFStreamMetadata)streamMetadata; + + // Write the header. + writeHeader(); + + // Set the sequence flag. + this.isWritingSequence = true; + } + + public void writeToSequence(IIOImage image, ImageWriteParam param) + throws IOException { + // Check sequence flag. + if(!this.isWritingSequence) { + throw new IllegalStateException + ("prepareWriteSequence() has not been called!"); + } + + // Append image. + writeInsert(-1, image, param); + } + + public void endWriteSequence() throws IOException { + // Check output. + if (getOutput() == null) { + throw new IllegalStateException("getOutput() == null!"); + } + + // Check sequence flag. + if(!isWritingSequence) { + throw new IllegalStateException + ("prepareWriteSequence() has not been called!"); + } + + // Unset sequence flag. + this.isWritingSequence = false; + + // Position the stream at the end, not at the next IFD pointer position. + long streamLength = this.stream.length(); + if (streamLength != -1) { + stream.seek(streamLength); + } + } + + public boolean canInsertImage(int imageIndex) throws IOException { + if (getOutput() == null) { + throw new IllegalStateException("getOutput() == null!"); + } + + // Mark position as locateIFD() will seek to IFD at imageIndex. + stream.mark(); + + // locateIFD() will throw an IndexOutOfBoundsException if + // imageIndex is < -1 or is too big thereby satisfying the spec. + long[] ifdpos = new long[1]; + long[] ifd = new long[1]; + locateIFD(imageIndex, ifdpos, ifd); + + // Reset to position before locateIFD(). + stream.reset(); + + return true; + } + + // Locate start of IFD for image. + // Throws IIOException if not at a TIFF header and + // IndexOutOfBoundsException if imageIndex is < -1 or is too big. + private void locateIFD(int imageIndex, long[] ifdpos, long[] ifd) + throws IOException { + + if(imageIndex < -1) { + throw new IndexOutOfBoundsException("imageIndex < -1!"); + } + + long startPos = stream.getStreamPosition(); + + stream.seek(headerPosition); + int byteOrder = stream.readUnsignedShort(); + if (byteOrder == 0x4d4d) { + stream.setByteOrder(ByteOrder.BIG_ENDIAN); + } else if (byteOrder == 0x4949) { + stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } else { + stream.seek(startPos); + throw new IIOException("Illegal byte order"); + } + if (stream.readUnsignedShort() != 42) { + stream.seek(startPos); + throw new IIOException("Illegal magic number"); + } + + ifdpos[0] = stream.getStreamPosition(); + ifd[0] = stream.readUnsignedInt(); + if (ifd[0] == 0) { + // imageIndex has to be >= -1 due to check above. + if(imageIndex > 0) { + stream.seek(startPos); + throw new IndexOutOfBoundsException + ("imageIndex is greater than the largest available index!"); + } + return; + } + stream.seek(ifd[0]); + + for (int i = 0; imageIndex == -1 || i < imageIndex; i++) { + int numFields; + try { + numFields = stream.readShort(); + } catch (EOFException eof) { + stream.seek(startPos); + ifd[0] = 0; + return; + } + + stream.skipBytes(12*numFields); + + ifdpos[0] = stream.getStreamPosition(); + ifd[0] = stream.readUnsignedInt(); + if (ifd[0] == 0) { + if (imageIndex != -1 && i < imageIndex - 1) { + stream.seek(startPos); + throw new IndexOutOfBoundsException( + "imageIndex is greater than the largest available index!"); + } + break; + } + stream.seek(ifd[0]); + } + } + + public void writeInsert(int imageIndex, + IIOImage image, + ImageWriteParam param) throws IOException { + int currentImageCached = currentImage; + try { + insert(imageIndex, image, param, true); + } catch (Exception e) { + throw e; + } finally { + currentImage = currentImageCached; + } + } + + private void insert(int imageIndex, + IIOImage image, + ImageWriteParam param, + boolean writeData) throws IOException { + if (stream == null) { + throw new IllegalStateException("Output not set!"); + } + if (image == null) { + throw new NullPointerException("image == null!"); + } + + // Locate the position of the old IFD (ifd) and the location + // of the pointer to that position (ifdpos). + long[] ifdpos = new long[1]; + long[] ifd = new long[1]; + + // locateIFD() will throw an IndexOutOfBoundsException if + // imageIndex is < -1 or is too big thereby satisfying the spec. + locateIFD(imageIndex, ifdpos, ifd); + + // Seek to the position containing the pointer to the old IFD. + stream.seek(ifdpos[0]); + + // Update next space pointer in anticipation of next write. + if(ifdpos[0] + 4 > nextSpace) { + nextSpace = ifdpos[0] + 4; + } + + // Ensure IFD is written on a word boundary + nextSpace = (nextSpace + 3) & ~0x3; + + // Update the value to point to the next available space. + stream.writeInt((int)nextSpace); + + // Seek to the next available space. + stream.seek(nextSpace); + + // Write the image (IFD and data). + write(null, image, param, false, writeData); + + // Seek to the position containing the pointer in the new IFD. + stream.seek(nextIFDPointerPos); + + // Update the new IFD to point to the old IFD. + stream.writeInt((int)ifd[0]); + // Don't need to update nextSpace here as already done in write(). + } + + // ----- BEGIN insert/writeEmpty methods ----- + + private boolean isEncodingEmpty() { + return isInsertingEmpty || isWritingEmpty; + } + + public boolean canInsertEmpty(int imageIndex) throws IOException { + return canInsertImage(imageIndex); + } + + public boolean canWriteEmpty() throws IOException { + if (getOutput() == null) { + throw new IllegalStateException("getOutput() == null!"); + } + return true; + } + + // Check state and parameters for writing or inserting empty images. + private void checkParamsEmpty(ImageTypeSpecifier imageType, + int width, + int height, + List thumbnails) { + if (getOutput() == null) { + throw new IllegalStateException("getOutput() == null!"); + } + + if(imageType == null) { + throw new NullPointerException("imageType == null!"); + } + + if(width < 1 || height < 1) { + throw new IllegalArgumentException("width < 1 || height < 1!"); + } + + if(thumbnails != null) { + int numThumbs = thumbnails.size(); + for(int i = 0; i < numThumbs; i++) { + Object thumb = thumbnails.get(i); + if(thumb == null || !(thumb instanceof BufferedImage)) { + throw new IllegalArgumentException + ("thumbnails contains null references or objects other than BufferedImages!"); + } + } + } + + if(this.isInsertingEmpty) { + throw new IllegalStateException + ("Previous call to prepareInsertEmpty() without corresponding call to endInsertEmpty()!"); + } + + if(this.isWritingEmpty) { + throw new IllegalStateException + ("Previous call to prepareWriteEmpty() without corresponding call to endWriteEmpty()!"); + } + } + + public void prepareInsertEmpty(int imageIndex, + ImageTypeSpecifier imageType, + int width, + int height, + IIOMetadata imageMetadata, + List thumbnails, + ImageWriteParam param) throws IOException { + checkParamsEmpty(imageType, width, height, thumbnails); + + this.isInsertingEmpty = true; + + SampleModel emptySM = imageType.getSampleModel(); + RenderedImage emptyImage = + new EmptyImage(0, 0, width, height, + 0, 0, emptySM.getWidth(), emptySM.getHeight(), + emptySM, imageType.getColorModel()); + + insert(imageIndex, new IIOImage(emptyImage, null, imageMetadata), + param, false); + } + + public void prepareWriteEmpty(IIOMetadata streamMetadata, + ImageTypeSpecifier imageType, + int width, + int height, + IIOMetadata imageMetadata, + List thumbnails, + ImageWriteParam param) throws IOException { + checkParamsEmpty(imageType, width, height, thumbnails); + + this.isWritingEmpty = true; + + SampleModel emptySM = imageType.getSampleModel(); + RenderedImage emptyImage = + new EmptyImage(0, 0, width, height, + 0, 0, emptySM.getWidth(), emptySM.getHeight(), + emptySM, imageType.getColorModel()); + + write(streamMetadata, new IIOImage(emptyImage, null, imageMetadata), + param, true, false); + } + + public void endInsertEmpty() throws IOException { + if (getOutput() == null) { + throw new IllegalStateException("getOutput() == null!"); + } + + if(!this.isInsertingEmpty) { + throw new IllegalStateException + ("No previous call to prepareInsertEmpty()!"); + } + + if(this.isWritingEmpty) { + throw new IllegalStateException + ("Previous call to prepareWriteEmpty() without corresponding call to endWriteEmpty()!"); + } + + if (inReplacePixelsNest) { + throw new IllegalStateException + ("In nested call to prepareReplacePixels!"); + } + + this.isInsertingEmpty = false; + } + + public void endWriteEmpty() throws IOException { + if (getOutput() == null) { + throw new IllegalStateException("getOutput() == null!"); + } + + if(!this.isWritingEmpty) { + throw new IllegalStateException + ("No previous call to prepareWriteEmpty()!"); + } + + if(this.isInsertingEmpty) { + throw new IllegalStateException + ("Previous call to prepareInsertEmpty() without corresponding call to endInsertEmpty()!"); + } + + if (inReplacePixelsNest) { + throw new IllegalStateException + ("In nested call to prepareReplacePixels!"); + } + + this.isWritingEmpty = false; + } + + // ----- END insert/writeEmpty methods ----- + + // ----- BEGIN replacePixels methods ----- + + private TIFFIFD readIFD(int imageIndex) throws IOException { + if (stream == null) { + throw new IllegalStateException("Output not set!"); + } + if (imageIndex < 0) { + throw new IndexOutOfBoundsException("imageIndex < 0!"); + } + + stream.mark(); + long[] ifdpos = new long[1]; + long[] ifd = new long[1]; + locateIFD(imageIndex, ifdpos, ifd); + if (ifd[0] == 0) { + stream.reset(); + throw new IndexOutOfBoundsException + ("imageIndex out of bounds!"); + } + + List tagSets = new ArrayList(1); + tagSets.add(BaselineTIFFTagSet.getInstance()); + TIFFIFD rootIFD = new TIFFIFD(tagSets); + rootIFD.initialize(stream, true, true); + stream.reset(); + + return rootIFD; + } + + public boolean canReplacePixels(int imageIndex) throws IOException { + if (getOutput() == null) { + throw new IllegalStateException("getOutput() == null!"); + } + + TIFFIFD rootIFD = readIFD(imageIndex); + TIFFField f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_COMPRESSION); + int compression = f.getAsInt(0); + + return compression == BaselineTIFFTagSet.COMPRESSION_NONE; + } + + private Object replacePixelsLock = new Object(); + + private int replacePixelsIndex = -1; + private TIFFImageMetadata replacePixelsMetadata = null; + private long[] replacePixelsTileOffsets = null; + private long[] replacePixelsByteCounts = null; + private long replacePixelsOffsetsPosition = 0L; + private long replacePixelsByteCountsPosition = 0L; + private Rectangle replacePixelsRegion = null; + private boolean inReplacePixelsNest = false; + + private TIFFImageReader reader = null; + + public void prepareReplacePixels(int imageIndex, + Rectangle region) throws IOException { + synchronized(replacePixelsLock) { + // Check state and parameters vis-a-vis ImageWriter specification. + if (stream == null) { + throw new IllegalStateException("Output not set!"); + } + if (region == null) { + throw new NullPointerException("region == null!"); + } + if (region.getWidth() < 1) { + throw new IllegalArgumentException("region.getWidth() < 1!"); + } + if (region.getHeight() < 1) { + throw new IllegalArgumentException("region.getHeight() < 1!"); + } + if (inReplacePixelsNest) { + throw new IllegalStateException + ("In nested call to prepareReplacePixels!"); + } + + // Read the IFD for the pixel replacement index. + TIFFIFD replacePixelsIFD = readIFD(imageIndex); + + // Ensure that compression is "none". + TIFFField f = + replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_COMPRESSION); + int compression = f.getAsInt(0); + if (compression != BaselineTIFFTagSet.COMPRESSION_NONE) { + throw new UnsupportedOperationException + ("canReplacePixels(imageIndex) == false!"); + } + + // Get the image dimensions. + f = + replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_IMAGE_WIDTH); + if(f == null) { + throw new IIOException("Cannot read ImageWidth field."); + } + int w = f.getAsInt(0); + + f = + replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_IMAGE_LENGTH); + if(f == null) { + throw new IIOException("Cannot read ImageHeight field."); + } + int h = f.getAsInt(0); + + // Create image bounds. + Rectangle bounds = new Rectangle(0, 0, w, h); + + // Intersect region with bounds. + region = region.intersection(bounds); + + // Check for empty intersection. + if(region.isEmpty()) { + throw new IIOException("Region does not intersect image bounds"); + } + + // Save the region. + replacePixelsRegion = region; + + // Get the tile offsets. + f = replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_OFFSETS); + if(f == null) { + f = replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS); + } + replacePixelsTileOffsets = f.getAsLongs(); + + // Get the byte counts. + f = replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_BYTE_COUNTS); + if(f == null) { + f = replacePixelsIFD.getTIFFField(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS); + } + replacePixelsByteCounts = f.getAsLongs(); + + replacePixelsOffsetsPosition = + replacePixelsIFD.getStripOrTileOffsetsPosition(); + replacePixelsByteCountsPosition = + replacePixelsIFD.getStripOrTileByteCountsPosition(); + + // Get the image metadata. + replacePixelsMetadata = new TIFFImageMetadata(replacePixelsIFD); + + // Save the image index. + replacePixelsIndex = imageIndex; + + // Set the pixel replacement flag. + inReplacePixelsNest = true; + } + } + + private Raster subsample(Raster raster, int[] sourceBands, + int subOriginX, int subOriginY, + int subPeriodX, int subPeriodY, + int dstOffsetX, int dstOffsetY, + Rectangle target) { + + int x = raster.getMinX(); + int y = raster.getMinY(); + int w = raster.getWidth(); + int h = raster.getHeight(); + int b = raster.getSampleModel().getNumBands(); + int t = raster.getSampleModel().getDataType(); + + int outMinX = XToTileX(x, subOriginX, subPeriodX) + dstOffsetX; + int outMinY = YToTileY(y, subOriginY, subPeriodY) + dstOffsetY; + int outMaxX = XToTileX(x + w - 1, subOriginX, subPeriodX) + dstOffsetX; + int outMaxY = YToTileY(y + h - 1, subOriginY, subPeriodY) + dstOffsetY; + int outWidth = outMaxX - outMinX + 1; + int outHeight = outMaxY - outMinY + 1; + + if(outWidth <= 0 || outHeight <= 0) return null; + + int inMinX = (outMinX - dstOffsetX)*subPeriodX + subOriginX; + int inMaxX = (outMaxX - dstOffsetX)*subPeriodX + subOriginX; + int inWidth = inMaxX - inMinX + 1; + int inMinY = (outMinY - dstOffsetY)*subPeriodY + subOriginY; + int inMaxY = (outMaxY - dstOffsetY)*subPeriodY + subOriginY; + int inHeight = inMaxY - inMinY + 1; + + WritableRaster wr = + raster.createCompatibleWritableRaster(outMinX, outMinY, + outWidth, outHeight); + + int jMax = inMinY + inHeight; + + if(t == DataBuffer.TYPE_FLOAT) { + float[] fsamples = new float[inWidth]; + float[] fsubsamples = new float[outWidth]; + + for(int k = 0; k < b; k++) { + int outY = outMinY; + for(int j = inMinY; j < jMax; j += subPeriodY) { + raster.getSamples(inMinX, j, inWidth, 1, k, fsamples); + int s = 0; + for(int i = 0; i < inWidth; i += subPeriodX) { + fsubsamples[s++] = fsamples[i]; + } + wr.setSamples(outMinX, outY++, outWidth, 1, k, + fsubsamples); + } + } + } else if (t == DataBuffer.TYPE_DOUBLE) { + double[] dsamples = new double[inWidth]; + double[] dsubsamples = new double[outWidth]; + + for(int k = 0; k < b; k++) { + int outY = outMinY; + for(int j = inMinY; j < jMax; j += subPeriodY) { + raster.getSamples(inMinX, j, inWidth, 1, k, dsamples); + int s = 0; + for(int i = 0; i < inWidth; i += subPeriodX) { + dsubsamples[s++] = dsamples[i]; + } + wr.setSamples(outMinX, outY++, outWidth, 1, k, + dsubsamples); + } + } + } else { + int[] samples = new int[inWidth]; + int[] subsamples = new int[outWidth]; + + for(int k = 0; k < b; k++) { + int outY = outMinY; + for(int j = inMinY; j < jMax; j += subPeriodY) { + raster.getSamples(inMinX, j, inWidth, 1, k, samples); + int s = 0; + for(int i = 0; i < inWidth; i += subPeriodX) { + subsamples[s++] = samples[i]; + } + wr.setSamples(outMinX, outY++, outWidth, 1, k, + subsamples); + } + } + } + + return wr.createChild(outMinX, outMinY, + target.width, target.height, + target.x, target.y, + sourceBands); + } + + public void replacePixels(RenderedImage image, ImageWriteParam param) + throws IOException { + + synchronized(replacePixelsLock) { + // Check state and parameters vis-a-vis ImageWriter specification. + if (stream == null) { + throw new IllegalStateException("stream == null!"); + } + + if (image == null) { + throw new NullPointerException("image == null!"); + } + + if (!inReplacePixelsNest) { + throw new IllegalStateException + ("No previous call to prepareReplacePixels!"); + } + + // Subsampling values. + int stepX = 1, stepY = 1, gridX = 0, gridY = 0; + + // Initialize the ImageWriteParam. + if (param == null) { + // Use the default. + param = getDefaultWriteParam(); + } else { + // Make a copy of the ImageWriteParam. + ImageWriteParam paramCopy = getDefaultWriteParam(); + + // Force uncompressed. + paramCopy.setCompressionMode(ImageWriteParam.MODE_DISABLED); + + // Force tiling to remain as in the already written image. + paramCopy.setTilingMode(ImageWriteParam.MODE_COPY_FROM_METADATA); + + // Retain source and destination region and band settings. + paramCopy.setDestinationOffset(param.getDestinationOffset()); + paramCopy.setSourceBands(param.getSourceBands()); + paramCopy.setSourceRegion(param.getSourceRegion()); + + // Save original subsampling values for subsampling the + // replacement data - not the data re-read from the image. + stepX = param.getSourceXSubsampling(); + stepY = param.getSourceYSubsampling(); + gridX = param.getSubsamplingXOffset(); + gridY = param.getSubsamplingYOffset(); + + // Replace the param. + param = paramCopy; + } + + // Check band count and bit depth compatibility. + TIFFField f = + replacePixelsMetadata.getTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE); + if(f == null) { + throw new IIOException + ("Cannot read destination BitsPerSample"); + } + int[] dstBitsPerSample = f.getAsInts(); + int[] srcBitsPerSample = image.getSampleModel().getSampleSize(); + int[] sourceBands = param.getSourceBands(); + if(sourceBands != null) { + if(sourceBands.length != dstBitsPerSample.length) { + throw new IIOException + ("Source and destination have different SamplesPerPixel"); + } + for(int i = 0; i < sourceBands.length; i++) { + if(dstBitsPerSample[i] != + srcBitsPerSample[sourceBands[i]]) { + throw new IIOException + ("Source and destination have different BitsPerSample"); + } + } + } else { + int srcNumBands = image.getSampleModel().getNumBands(); + if(srcNumBands != dstBitsPerSample.length) { + throw new IIOException + ("Source and destination have different SamplesPerPixel"); + } + for(int i = 0; i < srcNumBands; i++) { + if(dstBitsPerSample[i] != srcBitsPerSample[i]) { + throw new IIOException + ("Source and destination have different BitsPerSample"); + } + } + } + + // Get the source image bounds. + Rectangle srcImageBounds = + new Rectangle(image.getMinX(), image.getMinY(), + image.getWidth(), image.getHeight()); + + // Initialize the source rect. + Rectangle srcRect = param.getSourceRegion(); + if(srcRect == null) { + srcRect = srcImageBounds; + } + + // Set subsampling grid parameters. + int subPeriodX = stepX; + int subPeriodY = stepY; + int subOriginX = gridX + srcRect.x; + int subOriginY = gridY + srcRect.y; + + // Intersect with the source bounds. + if(!srcRect.equals(srcImageBounds)) { + srcRect = srcRect.intersection(srcImageBounds); + if(srcRect.isEmpty()) { + throw new IllegalArgumentException + ("Source region does not intersect source image!"); + } + } + + // Get the destination offset. + Point dstOffset = param.getDestinationOffset(); + + // Forward map source rectangle to determine destination width. + int dMinX = XToTileX(srcRect.x, subOriginX, subPeriodX) + + dstOffset.x; + int dMinY = YToTileY(srcRect.y, subOriginY, subPeriodY) + + dstOffset.y; + int dMaxX = XToTileX(srcRect.x + srcRect.width, + subOriginX, subPeriodX) + dstOffset.x; + int dMaxY = YToTileY(srcRect.y + srcRect.height, + subOriginY, subPeriodY) + dstOffset.y; + + // Initialize the destination rectangle. + Rectangle dstRect = + new Rectangle(dstOffset.x, dstOffset.y, + dMaxX - dMinX, dMaxY - dMinY); + + // Intersect with the replacement region. + dstRect = dstRect.intersection(replacePixelsRegion); + if(dstRect.isEmpty()) { + throw new IllegalArgumentException + ("Forward mapped source region does not intersect destination region!"); + } + + // Backward map to the active source region. + int activeSrcMinX = (dstRect.x - dstOffset.x)*subPeriodX + + subOriginX; + int sxmax = + (dstRect.x + dstRect.width - 1 - dstOffset.x)*subPeriodX + + subOriginX; + int activeSrcWidth = sxmax - activeSrcMinX + 1; + + int activeSrcMinY = (dstRect.y - dstOffset.y)*subPeriodY + + subOriginY; + int symax = + (dstRect.y + dstRect.height - 1 - dstOffset.y)*subPeriodY + + subOriginY; + int activeSrcHeight = symax - activeSrcMinY + 1; + Rectangle activeSrcRect = + new Rectangle(activeSrcMinX, activeSrcMinY, + activeSrcWidth, activeSrcHeight); + if(activeSrcRect.intersection(srcImageBounds).isEmpty()) { + throw new IllegalArgumentException + ("Backward mapped destination region does not intersect source image!"); + } + + if(reader == null) { + reader = new TIFFImageReader(new TIFFImageReaderSpi()); + } else { + reader.reset(); + } + + stream.mark(); + + try { + stream.seek(headerPosition); + reader.setInput(stream); + + this.imageMetadata = replacePixelsMetadata; + this.param = param; + SampleModel sm = image.getSampleModel(); + ColorModel cm = image.getColorModel(); + this.numBands = sm.getNumBands(); + this.imageType = new ImageTypeSpecifier(image); + this.periodX = param.getSourceXSubsampling(); + this.periodY = param.getSourceYSubsampling(); + this.sourceBands = null; + int[] sBands = param.getSourceBands(); + if (sBands != null) { + this.sourceBands = sBands; + this.numBands = sourceBands.length; + } + setupMetadata(cm, sm, + reader.getWidth(replacePixelsIndex), + reader.getHeight(replacePixelsIndex)); + int[] scaleSampleSize = sm.getSampleSize(); + initializeScaleTables(scaleSampleSize); + + // Determine whether bilevel. + this.isBilevel = ImageUtil.isBinary(image.getSampleModel()); + + // Check for photometric inversion. + this.isInverted = + (nativePhotometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO && + photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) || + (nativePhotometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO && + photometricInterpretation == + BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO); + + // Analyze image data suitability for direct copy. + this.isImageSimple = + (isBilevel || + (!isInverted && ImageUtil.imageIsContiguous(image))) && + !isRescaling && // no value rescaling + sourceBands == null && // no subbanding + periodX == 1 && periodY == 1 && // no subsampling + colorConverter == null; + + int minTileX = XToTileX(dstRect.x, 0, tileWidth); + int minTileY = YToTileY(dstRect.y, 0, tileLength); + int maxTileX = XToTileX(dstRect.x + dstRect.width - 1, + 0, tileWidth); + int maxTileY = YToTileY(dstRect.y + dstRect.height - 1, + 0, tileLength); + + TIFFCompressor encoder = new TIFFNullCompressor(); + encoder.setWriter(this); + encoder.setStream(stream); + encoder.setMetadata(this.imageMetadata); + + Rectangle tileRect = new Rectangle(); + for(int ty = minTileY; ty <= maxTileY; ty++) { + for(int tx = minTileX; tx <= maxTileX; tx++) { + int tileIndex = ty*tilesAcross + tx; + boolean isEmpty = + replacePixelsByteCounts[tileIndex] == 0L; + WritableRaster raster; + if(isEmpty) { + SampleModel tileSM = + sm.createCompatibleSampleModel(tileWidth, + tileLength); + raster = Raster.createWritableRaster(tileSM, null); + } else { + BufferedImage tileImage = + reader.readTile(replacePixelsIndex, tx, ty); + raster = tileImage.getRaster(); + } + + tileRect.setLocation(tx*tileWidth, + ty*tileLength); + tileRect.setSize(raster.getWidth(), + raster.getHeight()); + raster = + raster.createWritableTranslatedChild(tileRect.x, + tileRect.y); + + Rectangle replacementRect = + tileRect.intersection(dstRect); + + int srcMinX = + (replacementRect.x - dstOffset.x)*subPeriodX + + subOriginX; + int srcXmax = + (replacementRect.x + replacementRect.width - 1 - + dstOffset.x)*subPeriodX + subOriginX; + int srcWidth = srcXmax - srcMinX + 1; + + int srcMinY = + (replacementRect.y - dstOffset.y)*subPeriodY + + subOriginY; + int srcYMax = + (replacementRect.y + replacementRect.height - 1 - + dstOffset.y)*subPeriodY + subOriginY; + int srcHeight = srcYMax - srcMinY + 1; + Rectangle srcTileRect = + new Rectangle(srcMinX, srcMinY, + srcWidth, srcHeight); + + Raster replacementData = image.getData(srcTileRect); + if(subPeriodX == 1 && subPeriodY == 1 && + subOriginX == 0 && subOriginY == 0) { + replacementData = + replacementData.createChild(srcTileRect.x, + srcTileRect.y, + srcTileRect.width, + srcTileRect.height, + replacementRect.x, + replacementRect.y, + sourceBands); + } else { + replacementData = subsample(replacementData, + sourceBands, + subOriginX, + subOriginY, + subPeriodX, + subPeriodY, + dstOffset.x, + dstOffset.y, + replacementRect); + if(replacementData == null) { + continue; + } + } + + raster.setRect(replacementData); + + if(isEmpty) { + stream.seek(nextSpace); + } else { + stream.seek(replacePixelsTileOffsets[tileIndex]); + } + + this.image = new SingleTileRenderedImage(raster, cm); + + int numBytes = writeTile(tileRect, encoder); + + if(isEmpty) { + // Update Strip/TileOffsets and + // Strip/TileByteCounts fields. + stream.mark(); + stream.seek(replacePixelsOffsetsPosition + + 4*tileIndex); + stream.writeInt((int)nextSpace); + stream.seek(replacePixelsByteCountsPosition + + 4*tileIndex); + stream.writeInt(numBytes); + stream.reset(); + + // Increment location of next available space. + nextSpace += numBytes; + } + } + } + + } catch(IOException e) { + throw e; + } finally { + stream.reset(); + } + } + } + + public void replacePixels(Raster raster, ImageWriteParam param) + throws IOException { + if (raster == null) { + throw new NullPointerException("raster == null!"); + } + + replacePixels(new SingleTileRenderedImage(raster, + image.getColorModel()), + param); + } + + public void endReplacePixels() throws IOException { + synchronized(replacePixelsLock) { + if(!this.inReplacePixelsNest) { + throw new IllegalStateException + ("No previous call to prepareReplacePixels()!"); + } + replacePixelsIndex = -1; + replacePixelsMetadata = null; + replacePixelsTileOffsets = null; + replacePixelsByteCounts = null; + replacePixelsOffsetsPosition = 0L; + replacePixelsByteCountsPosition = 0L; + replacePixelsRegion = null; + inReplacePixelsNest = false; + } + } + + // ----- END replacePixels methods ----- + + public void reset() { + super.reset(); + + stream = null; + image = null; + imageType = null; + byteOrder = null; + param = null; + compressor = null; + colorConverter = null; + streamMetadata = null; + imageMetadata = null; + + isWritingSequence = false; + isWritingEmpty = false; + isInsertingEmpty = false; + + replacePixelsIndex = -1; + replacePixelsMetadata = null; + replacePixelsTileOffsets = null; + replacePixelsByteCounts = null; + replacePixelsOffsetsPosition = 0L; + replacePixelsByteCountsPosition = 0L; + replacePixelsRegion = null; + inReplacePixelsNest = false; + } +} + +class EmptyImage extends SimpleRenderedImage { + EmptyImage(int minX, int minY, int width, int height, + int tileGridXOffset, int tileGridYOffset, + int tileWidth, int tileHeight, + SampleModel sampleModel, ColorModel colorModel) { + this.minX = minX; + this.minY = minY; + this.width = width; + this.height = height; + this.tileGridXOffset = tileGridXOffset; + this.tileGridYOffset = tileGridYOffset; + this.tileWidth = tileWidth; + this.tileHeight = tileHeight; + this.sampleModel = sampleModel; + this.colorModel = colorModel; + } + + public Raster getTile(int tileX, int tileY) { + return null; + } +}