--- /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.
+ *
+ * <p> If <code>tileWidth < 0</code>, the results of this method
+ * are undefined. If <code>tileWidth == 0</code>, an
+ * <code>ArithmeticException</code> will be thrown.
+ *
+ * @throws ArithmeticException If <code>tileWidth == 0</code>.
+ */
+ 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.
+ *
+ * <p> If <code>tileHeight < 0</code>, the results of this method
+ * are undefined. If <code>tileHeight == 0</code>, an
+ * <code>ArithmeticException</code> will be thrown.
+ *
+ * @throws ArithmeticException If <code>tileHeight == 0</code>.
+ */
+ 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<TIFFTagSet> tagSets = new ArrayList<TIFFTagSet>(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 <code>javax_imageio_1.0</code> tree to a
+ * <code>TIFFImageMetadata</code> object.
+ *
+ * @param inData The metadata object.
+ * @return a <code>TIFFImageMetadata</code> or <code>null</code> if
+ * the standard tree derived from the input object is <code>null</code>.
+ * @throws IllegalArgumentException if <code>inData</code> is
+ * <code>null</code>.
+ * @throws IllegalArgumentException if <code>inData</code> does not support
+ * the standard metadata format.
+ * @throws IIOInvalidTreeException if <code>inData</code> 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<TIFFTagSet> tagSets = new ArrayList<TIFFTagSet>(1);
+ tagSets.add(BaselineTIFFTagSet.getInstance());
+ outData = new TIFFImageMetadata(tagSets);
+ outData.setFromTree(formatName, tree);
+ }
+
+ return outData;
+ }
+
+ /**
+ * Converts a native
+ * <code>javax_imageio_tiff_image_1.0</code> tree to a
+ * <code>TIFFImageMetadata</code> object.
+ *
+ * @param inData The metadata object.
+ * @return a <code>TIFFImageMetadata</code> or <code>null</code> if
+ * the native tree derived from the input object is <code>null</code>.
+ * @throws IllegalArgumentException if <code>inData</code> is
+ * <code>null</code> or does not support the native metadata format.
+ * @throws IIOInvalidTreeException if <code>inData</code> 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<TIFFTagSet> tagSets = new ArrayList<TIFFTagSet>(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 <code>ColorModel</code> of the image being written.
+ * @param sm The <code>SampleModel</code> 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<TIFFTagSet> exifTagSets = new ArrayList<TIFFTagSet>(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<? extends BufferedImage> 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<? extends BufferedImage> 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<? extends BufferedImage> 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<TIFFTagSet> tagSets = new ArrayList<TIFFTagSet>(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;
+ }
+}