blob: 6d27c01bb93e2795c0b93ec65572809715ffa815 [file] [log] [blame]
/*
* Copyright (c) 2005, 2016, 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;
private long prevStreamPosition;
private long prevHeaderPosition;
private long prevNextSpace;
// 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}, the results of this method
* are undefined. If {@code tileWidth == 0}, an
* {@code ArithmeticException} will be thrown.
*
* @throws ArithmeticException If {@code tileWidth == 0}.
*/
public static int XToTileX(int x, int tileGridXOffset, int tileWidth) {
x -= tileGridXOffset;
if (x < 0) {
x += 1 - tileWidth; // force round to -infinity (ceiling)
}
return x/tileWidth;
}
/**
* Converts a pixel's Y coordinate into a vertical tile index
* relative to a given tile grid layout specified by its Y offset
* and tile height.
*
* <p> If {@code tileHeight < 0}, the results of this method
* are undefined. If {@code tileHeight == 0}, an
* {@code ArithmeticException} will be thrown.
*
* @throws ArithmeticException If {@code tileHeight == 0}.
*/
public static int YToTileY(int y, int tileGridYOffset, int tileHeight) {
y -= tileGridYOffset;
if (y < 0) {
y += 1 - tileHeight; // force round to -infinity (ceiling)
}
return y/tileHeight;
}
public TIFFImageWriter(ImageWriterSpi originatingProvider) {
super(originatingProvider);
}
public ImageWriteParam getDefaultWriteParam() {
return new TIFFImageWriteParam(getLocale());
}
public void setOutput(Object output) {
if (output != null) {
if (!(output instanceof ImageOutputStream)) {
throw new IllegalArgumentException
("output not an ImageOutputStream!");
}
// reset() must precede setOutput() as it sets output to null
reset();
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;
}
super.setOutput(output);
}
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} tree to a
* {@code TIFFImageMetadata} object.
*
* @param inData The metadata object.
* @return a {@code TIFFImageMetadata} or {@code null} if
* the standard tree derived from the input object is {@code null}.
* @throws IllegalArgumentException if {@code inData} is
* {@code null}.
* @throws IllegalArgumentException if {@code inData} does not support
* the standard metadata format.
* @throws IIOInvalidTreeException if {@code inData} generates an
* invalid standard metadata tree.
*/
private TIFFImageMetadata convertStandardImageMetadata(IIOMetadata inData)
throws IIOInvalidTreeException {
if(inData == null) {
throw new NullPointerException("inData == null!");
} else if(!inData.isStandardMetadataFormatSupported()) {
throw new IllegalArgumentException
("inData does not support standard metadata format!");
}
TIFFImageMetadata outData = null;
String formatName = IIOMetadataFormatImpl.standardMetadataFormatName;
Node tree = inData.getAsTree(formatName);
if (tree != null) {
List<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} tree to a
* {@code TIFFImageMetadata} object.
*
* @param inData The metadata object.
* @return a {@code TIFFImageMetadata} or {@code null} if
* the native tree derived from the input object is {@code null}.
* @throws IllegalArgumentException if {@code inData} is
* {@code null} or does not support the native metadata format.
* @throws IIOInvalidTreeException if {@code inData} generates an
* invalid native metadata tree.
*/
private TIFFImageMetadata convertNativeImageMetadata(IIOMetadata inData)
throws IIOInvalidTreeException {
if(inData == null) {
throw new NullPointerException("inData == null!");
} else if(!Arrays.asList(inData.getMetadataFormatNames()).contains(
TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME)) {
throw new IllegalArgumentException
("inData does not support native metadata format!");
}
TIFFImageMetadata outData = null;
String formatName = TIFFImageMetadata.NATIVE_METADATA_FORMAT_NAME;
Node tree = inData.getAsTree(formatName);
if (tree != null) {
List<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} of the image being written.
* @param sm The {@code SampleModel} of the image being written.
* @param destWidth The width of the written image after subsampling.
* @param destHeight The height of the written image after subsampling.
*/
void setupMetadata(ColorModel cm, SampleModel sm,
int destWidth, int destHeight)
throws IIOException {
// Get initial IFD from metadata
// Always emit these fields:
//
// Override values from metadata:
//
// planarConfiguration -> chunky (planar not supported on output)
//
// Override values from metadata with image-derived values:
//
// bitsPerSample (if not bilivel)
// colorMap (if palette color)
// photometricInterpretation (derive from image)
// imageLength
// imageWidth
//
// rowsPerStrip \ / tileLength
// stripOffsets | OR | tileOffsets
// stripByteCounts / | tileByteCounts
// \ tileWidth
//
//
// Override values from metadata with write param values:
//
// compression
// Use values from metadata if present for these fields,
// otherwise use defaults:
//
// resolutionUnit
// XResolution (take from metadata if present)
// YResolution
// rowsPerStrip
// sampleFormat
TIFFIFD rootIFD = imageMetadata.getRootIFD();
BaselineTIFFTagSet base = BaselineTIFFTagSet.getInstance();
// If PlanarConfiguration field present, set value to chunky.
TIFFField f =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION);
if(f != null &&
f.getAsInt(0) != BaselineTIFFTagSet.PLANAR_CONFIGURATION_CHUNKY) {
TIFFField planarConfigurationField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION),
BaselineTIFFTagSet.PLANAR_CONFIGURATION_CHUNKY);
rootIFD.addTIFFField(planarConfigurationField);
}
char[] extraSamples = null;
this.photometricInterpretation = -1;
boolean forcePhotometricInterpretation = false;
f =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION);
if (f != null) {
photometricInterpretation = f.getAsInt(0);
if(photometricInterpretation ==
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR &&
!(cm instanceof IndexColorModel)) {
photometricInterpretation = -1;
} else {
forcePhotometricInterpretation = true;
}
}
int[] sampleSize = sm.getSampleSize();
int numBands = sm.getNumBands();
int numExtraSamples = 0;
// Check that numBands > 1 here because TIFF requires that
// SamplesPerPixel = numBands + numExtraSamples and numBands
// cannot be zero.
if (numBands > 1 && cm != null && cm.hasAlpha()) {
--numBands;
numExtraSamples = 1;
extraSamples = new char[1];
if (cm.isAlphaPremultiplied()) {
extraSamples[0] =
BaselineTIFFTagSet.EXTRA_SAMPLES_ASSOCIATED_ALPHA;
} else {
extraSamples[0] =
BaselineTIFFTagSet.EXTRA_SAMPLES_UNASSOCIATED_ALPHA;
}
}
if (numBands == 3) {
this.nativePhotometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB;
if (photometricInterpretation == -1) {
photometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB;
}
} else if (sm.getNumBands() == 1 && cm instanceof IndexColorModel) {
IndexColorModel icm = (IndexColorModel)cm;
int r0 = icm.getRed(0);
int r1 = icm.getRed(1);
if (icm.getMapSize() == 2 &&
(r0 == icm.getGreen(0)) && (r0 == icm.getBlue(0)) &&
(r1 == icm.getGreen(1)) && (r1 == icm.getBlue(1)) &&
(r0 == 0 || r0 == 255) &&
(r1 == 0 || r1 == 255) &&
(r0 != r1)) {
// Black/white image
if (r0 == 0) {
nativePhotometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
} else {
nativePhotometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO;
}
// If photometricInterpretation is already set to
// WhiteIsZero or BlackIsZero, leave it alone
if (photometricInterpretation !=
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO &&
photometricInterpretation !=
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO) {
photometricInterpretation =
r0 == 0 ?
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO :
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO;
}
} else {
nativePhotometricInterpretation =
photometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR;
}
} else {
if(cm != null) {
switch(cm.getColorSpace().getType()) {
case ColorSpace.TYPE_Lab:
nativePhotometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CIELAB;
break;
case ColorSpace.TYPE_YCbCr:
nativePhotometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR;
break;
case ColorSpace.TYPE_CMYK:
nativePhotometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CMYK;
break;
default:
nativePhotometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
}
} else {
nativePhotometricInterpretation =
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
}
if (photometricInterpretation == -1) {
photometricInterpretation = nativePhotometricInterpretation;
}
}
// Emit compression tag
int compressionMode = param.getCompressionMode();
switch(compressionMode) {
case ImageWriteParam.MODE_EXPLICIT:
{
String compressionType = param.getCompressionType();
if (compressionType == null) {
this.compression = BaselineTIFFTagSet.COMPRESSION_NONE;
} else {
// Determine corresponding compression tag value.
int len = compressionTypes.length;
for (int i = 0; i < len; i++) {
if (compressionType.equals(compressionTypes[i])) {
this.compression = compressionNumbers[i];
}
}
}
}
break;
case ImageWriteParam.MODE_COPY_FROM_METADATA:
{
TIFFField compField =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_COMPRESSION);
if(compField != null) {
this.compression = compField.getAsInt(0);
} else {
this.compression = BaselineTIFFTagSet.COMPRESSION_NONE;
}
}
break;
default:
this.compression = BaselineTIFFTagSet.COMPRESSION_NONE;
}
TIFFField predictorField =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_PREDICTOR);
if (predictorField != null) {
this.predictor = predictorField.getAsInt(0);
// We only support Horizontal Predictor for a bitDepth of 8
if (sampleSize[0] != 8 ||
// Check the value of the tag for validity
(predictor != BaselineTIFFTagSet.PREDICTOR_NONE &&
predictor !=
BaselineTIFFTagSet.PREDICTOR_HORIZONTAL_DIFFERENCING)) {
// Set to default
predictor = BaselineTIFFTagSet.PREDICTOR_NONE;
// Emit this changed predictor value to metadata
TIFFField newPredictorField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_PREDICTOR),
predictor);
rootIFD.addTIFFField(newPredictorField);
}
}
TIFFField compressionField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_COMPRESSION),
compression);
rootIFD.addTIFFField(compressionField);
// Set Exif flag. Note that there is no way to determine definitively
// when an uncompressed thumbnail is being written as the Exif IFD
// pointer field is optional for thumbnails.
boolean isExif = false;
if(numBands == 3 &&
sampleSize[0] == 8 && sampleSize[1] == 8 && sampleSize[2] == 8) {
// Three bands with 8 bits per sample.
if(rootIFD.getTIFFField(ExifParentTIFFTagSet.TAG_EXIF_IFD_POINTER)
!= null) {
// Exif IFD pointer present.
if(compression == BaselineTIFFTagSet.COMPRESSION_NONE &&
(photometricInterpretation ==
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB ||
photometricInterpretation ==
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR)) {
// Uncompressed RGB or YCbCr.
isExif = true;
} else if(compression ==
BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) {
// Compressed.
isExif = true;
}
} else if(compressionMode == ImageWriteParam.MODE_EXPLICIT &&
EXIF_JPEG_COMPRESSION_TYPE.equals
(param.getCompressionType())) {
// Exif IFD pointer absent but Exif JPEG compression set.
isExif = true;
}
}
// Initialize JPEG interchange format flag which is used to
// indicate that the image is stored as a single JPEG stream.
// This flag is separated from the 'isExif' flag in case JPEG
// interchange format is eventually supported for non-Exif images.
boolean isJPEGInterchange =
isExif && compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG;
this.compressor = null;
if (compression == BaselineTIFFTagSet.COMPRESSION_CCITT_RLE) {
compressor = new TIFFRLECompressor();
if (!forcePhotometricInterpretation) {
photometricInterpretation
= BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO;
}
} else if (compression
== BaselineTIFFTagSet.COMPRESSION_CCITT_T_4) {
compressor = new TIFFT4Compressor();
if (!forcePhotometricInterpretation) {
photometricInterpretation
= BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO;
}
} else if (compression
== BaselineTIFFTagSet.COMPRESSION_CCITT_T_6) {
compressor = new TIFFT6Compressor();
if (!forcePhotometricInterpretation) {
photometricInterpretation
= BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO;
}
} else if (compression
== BaselineTIFFTagSet.COMPRESSION_LZW) {
compressor = new TIFFLZWCompressor(predictor);
} else if (compression
== BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) {
if (isExif) {
compressor = new TIFFExifJPEGCompressor(param);
} else {
throw new IIOException("Old JPEG compression not supported!");
}
} else if (compression
== BaselineTIFFTagSet.COMPRESSION_JPEG) {
if (numBands == 3 && sampleSize[0] == 8
&& sampleSize[1] == 8 && sampleSize[2] == 8) {
photometricInterpretation
= BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR;
} else if (numBands == 1 && sampleSize[0] == 8) {
photometricInterpretation
= BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
} else {
throw new IIOException("JPEG compression supported for 1- and 3-band byte images only!");
}
compressor = new TIFFJPEGCompressor(param);
} else if (compression
== BaselineTIFFTagSet.COMPRESSION_ZLIB) {
compressor = new TIFFZLibCompressor(param, predictor);
} else if (compression
== BaselineTIFFTagSet.COMPRESSION_PACKBITS) {
compressor = new TIFFPackBitsCompressor();
} else if (compression
== BaselineTIFFTagSet.COMPRESSION_DEFLATE) {
compressor = new TIFFDeflateCompressor(param, predictor);
} else {
// Determine inverse fill setting.
f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_FILL_ORDER);
boolean inverseFill = (f != null && f.getAsInt(0) == 2);
if (inverseFill) {
compressor = new TIFFLSBCompressor();
} else {
compressor = new TIFFNullCompressor();
}
}
this.colorConverter = null;
if (cm != null
&& cm.getColorSpace().getType() == ColorSpace.TYPE_RGB) {
//
// Perform color conversion only if image has RGB color space.
//
if (photometricInterpretation
== BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR
&& compression
!= BaselineTIFFTagSet.COMPRESSION_JPEG) {
//
// Convert RGB to YCbCr only if compression type is not
// JPEG in which case this is handled implicitly by the
// compressor.
//
colorConverter = new TIFFYCbCrColorConverter(imageMetadata);
} else if (photometricInterpretation
== BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_CIELAB) {
colorConverter = new TIFFCIELabColorConverter();
}
}
//
// Cannot at this time do YCbCr subsampling so set the
// YCbCrSubsampling field value to [1, 1] and the YCbCrPositioning
// field value to "cosited".
//
if(photometricInterpretation ==
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_Y_CB_CR &&
compression !=
BaselineTIFFTagSet.COMPRESSION_JPEG) {
// Remove old subsampling and positioning fields.
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING);
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING);
// Add unity chrominance subsampling factors.
rootIFD.addTIFFField
(new TIFFField
(base.getTag(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING),
TIFFTag.TIFF_SHORT,
2,
new char[] {(char)1, (char)1}));
// Add cosited positioning.
rootIFD.addTIFFField
(new TIFFField
(base.getTag(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING),
TIFFTag.TIFF_SHORT,
1,
new char[] {
(char)BaselineTIFFTagSet.Y_CB_CR_POSITIONING_COSITED
}));
}
TIFFField photometricInterpretationField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION),
photometricInterpretation);
rootIFD.addTIFFField(photometricInterpretationField);
this.bitsPerSample = new char[numBands + numExtraSamples];
this.bitDepth = 0;
for (int i = 0; i < numBands; i++) {
this.bitDepth = Math.max(bitDepth, sampleSize[i]);
}
if (bitDepth == 3) {
bitDepth = 4;
} else if (bitDepth > 4 && bitDepth < 8) {
bitDepth = 8;
} else if (bitDepth > 8 && bitDepth < 16) {
bitDepth = 16;
} else if (bitDepth > 16 && bitDepth < 32) {
bitDepth = 32;
} else if (bitDepth > 32) {
bitDepth = 64;
}
for (int i = 0; i < bitsPerSample.length; i++) {
bitsPerSample[i] = (char)bitDepth;
}
// Emit BitsPerSample. If the image is bilevel, emit if and only
// if already in the metadata and correct (count and value == 1).
if (bitsPerSample.length != 1 || bitsPerSample[0] != 1) {
TIFFField bitsPerSampleField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE),
TIFFTag.TIFF_SHORT,
bitsPerSample.length,
bitsPerSample);
rootIFD.addTIFFField(bitsPerSampleField);
} else { // bitsPerSample.length == 1 && bitsPerSample[0] == 1
TIFFField bitsPerSampleField =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE);
if(bitsPerSampleField != null) {
int[] bps = bitsPerSampleField.getAsInts();
if(bps == null || bps.length != 1 || bps[0] != 1) {
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE);
}
}
}
// Prepare SampleFormat field.
f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_SAMPLE_FORMAT);
if(f == null && (bitDepth == 16 || bitDepth == 32 || bitDepth == 64)) {
// Set up default content for 16-, 32-, and 64-bit cases.
char sampleFormatValue;
int dataType = sm.getDataType();
if(bitDepth == 16 && dataType == DataBuffer.TYPE_USHORT) {
sampleFormatValue =
(char)BaselineTIFFTagSet.SAMPLE_FORMAT_UNSIGNED_INTEGER;
} else if((bitDepth == 32 && dataType == DataBuffer.TYPE_FLOAT) ||
(bitDepth == 64 && dataType == DataBuffer.TYPE_DOUBLE)) {
sampleFormatValue =
(char)BaselineTIFFTagSet.SAMPLE_FORMAT_FLOATING_POINT;
} else {
sampleFormatValue =
BaselineTIFFTagSet.SAMPLE_FORMAT_SIGNED_INTEGER;
}
this.sampleFormat = (int)sampleFormatValue;
char[] sampleFormatArray = new char[bitsPerSample.length];
Arrays.fill(sampleFormatArray, sampleFormatValue);
// Update the metadata.
TIFFTag sampleFormatTag =
base.getTag(BaselineTIFFTagSet.TAG_SAMPLE_FORMAT);
TIFFField sampleFormatField =
new TIFFField(sampleFormatTag, TIFFTag.TIFF_SHORT,
sampleFormatArray.length, sampleFormatArray);
rootIFD.addTIFFField(sampleFormatField);
} else if(f != null) {
// Get whatever was provided.
sampleFormat = f.getAsInt(0);
} else {
// Set default value for internal use only.
sampleFormat = BaselineTIFFTagSet.SAMPLE_FORMAT_UNDEFINED;
}
if (extraSamples != null) {
TIFFField extraSamplesField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_EXTRA_SAMPLES),
TIFFTag.TIFF_SHORT,
extraSamples.length,
extraSamples);
rootIFD.addTIFFField(extraSamplesField);
} else {
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_EXTRA_SAMPLES);
}
TIFFField samplesPerPixelField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_SAMPLES_PER_PIXEL),
bitsPerSample.length);
rootIFD.addTIFFField(samplesPerPixelField);
// Emit ColorMap if image is of palette color type
if (photometricInterpretation ==
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR &&
cm instanceof IndexColorModel) {
char[] colorMap = new char[3*(1 << bitsPerSample[0])];
IndexColorModel icm = (IndexColorModel)cm;
// mapSize is determined by BitsPerSample, not by incoming ICM.
int mapSize = 1 << bitsPerSample[0];
int indexBound = Math.min(mapSize, icm.getMapSize());
for (int i = 0; i < indexBound; i++) {
colorMap[i] = (char)((icm.getRed(i)*65535)/255);
colorMap[mapSize + i] = (char)((icm.getGreen(i)*65535)/255);
colorMap[2*mapSize + i] = (char)((icm.getBlue(i)*65535)/255);
}
TIFFField colorMapField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_COLOR_MAP),
TIFFTag.TIFF_SHORT,
colorMap.length,
colorMap);
rootIFD.addTIFFField(colorMapField);
} else {
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_COLOR_MAP);
}
// Emit ICCProfile if there is no ICCProfile field already in the
// metadata and the ColorSpace is non-standard ICC.
if(cm != null &&
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_ICC_PROFILE) == null &&
ImageUtil.isNonStandardICCColorSpace(cm.getColorSpace())) {
ICC_ColorSpace iccColorSpace = (ICC_ColorSpace)cm.getColorSpace();
byte[] iccProfileData = iccColorSpace.getProfile().getData();
TIFFField iccProfileField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_ICC_PROFILE),
TIFFTag.TIFF_UNDEFINED,
iccProfileData.length,
iccProfileData);
rootIFD.addTIFFField(iccProfileField);
}
// Always emit XResolution and YResolution.
TIFFField XResolutionField =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_X_RESOLUTION);
TIFFField YResolutionField =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_Y_RESOLUTION);
if(XResolutionField == null && YResolutionField == null) {
long[][] resRational = new long[1][2];
resRational[0] = new long[2];
TIFFField ResolutionUnitField =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_RESOLUTION_UNIT);
// Don't force dimensionless if one of the other dimensional
// quantities is present.
if(ResolutionUnitField == null &&
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_X_POSITION) == null &&
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_Y_POSITION) == null) {
// Set resolution to unit and units to dimensionless.
resRational[0][0] = 1;
resRational[0][1] = 1;
ResolutionUnitField =
new TIFFField(rootIFD.getTag
(BaselineTIFFTagSet.TAG_RESOLUTION_UNIT),
BaselineTIFFTagSet.RESOLUTION_UNIT_NONE);
rootIFD.addTIFFField(ResolutionUnitField);
} else {
// Set resolution to a value which would make the maximum
// image dimension equal to 4 inches as arbitrarily stated
// in the description of ResolutionUnit in the TIFF 6.0
// specification. If the ResolutionUnit field specifies
// "none" then set the resolution to unity (1/1).
int resolutionUnit = ResolutionUnitField != null ?
ResolutionUnitField.getAsInt(0) :
BaselineTIFFTagSet.RESOLUTION_UNIT_INCH;
int maxDimension = Math.max(destWidth, destHeight);
switch(resolutionUnit) {
case BaselineTIFFTagSet.RESOLUTION_UNIT_INCH:
resRational[0][0] = maxDimension;
resRational[0][1] = 4;
break;
case BaselineTIFFTagSet.RESOLUTION_UNIT_CENTIMETER:
resRational[0][0] = 100L*maxDimension; // divide out 100
resRational[0][1] = 4*254; // 2.54 cm/inch * 100
break;
default:
resRational[0][0] = 1;
resRational[0][1] = 1;
}
}
XResolutionField =
new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_X_RESOLUTION),
TIFFTag.TIFF_RATIONAL,
1,
resRational);
rootIFD.addTIFFField(XResolutionField);
YResolutionField =
new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_Y_RESOLUTION),
TIFFTag.TIFF_RATIONAL,
1,
resRational);
rootIFD.addTIFFField(YResolutionField);
} else if(XResolutionField == null && YResolutionField != null) {
// Set XResolution to YResolution.
long[] yResolution =
YResolutionField.getAsRational(0).clone();
XResolutionField =
new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_X_RESOLUTION),
TIFFTag.TIFF_RATIONAL,
1,
yResolution);
rootIFD.addTIFFField(XResolutionField);
} else if(XResolutionField != null && YResolutionField == null) {
// Set YResolution to XResolution.
long[] xResolution =
XResolutionField.getAsRational(0).clone();
YResolutionField =
new TIFFField(rootIFD.getTag(BaselineTIFFTagSet.TAG_Y_RESOLUTION),
TIFFTag.TIFF_RATIONAL,
1,
xResolution);
rootIFD.addTIFFField(YResolutionField);
}
// Set mandatory fields, overriding metadata passed in
int width = destWidth;
TIFFField imageWidthField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_IMAGE_WIDTH),
width);
rootIFD.addTIFFField(imageWidthField);
int height = destHeight;
TIFFField imageLengthField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_IMAGE_LENGTH),
height);
rootIFD.addTIFFField(imageLengthField);
// Determine rowsPerStrip
int rowsPerStrip;
TIFFField rowsPerStripField =
rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP);
if (rowsPerStripField != null) {
rowsPerStrip = rowsPerStripField.getAsInt(0);
if(rowsPerStrip < 0) {
rowsPerStrip = height;
}
} else {
int bitsPerPixel = bitDepth*(numBands + numExtraSamples);
int bytesPerRow = (bitsPerPixel*width + 7)/8;
rowsPerStrip =
Math.max(Math.max(DEFAULT_BYTES_PER_STRIP/bytesPerRow, 1), 8);
}
rowsPerStrip = Math.min(rowsPerStrip, height);
// Tiling flag.
boolean useTiling = false;
// Analyze tiling parameters
int tilingMode = param.getTilingMode();
if (tilingMode == ImageWriteParam.MODE_DISABLED ||
tilingMode == ImageWriteParam.MODE_DEFAULT) {
this.tileWidth = width;
this.tileLength = rowsPerStrip;
useTiling = false;
} else if (tilingMode == ImageWriteParam.MODE_EXPLICIT) {
tileWidth = param.getTileWidth();
tileLength = param.getTileHeight();
useTiling = true;
} else if (tilingMode == ImageWriteParam.MODE_COPY_FROM_METADATA) {
f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_WIDTH);
if (f == null) {
tileWidth = width;
useTiling = false;
} else {
tileWidth = f.getAsInt(0);
useTiling = true;
}
f = rootIFD.getTIFFField(BaselineTIFFTagSet.TAG_TILE_LENGTH);
if (f == null) {
tileLength = rowsPerStrip;
} else {
tileLength = f.getAsInt(0);
useTiling = true;
}
} else {
throw new IIOException("Illegal value of tilingMode!");
}
if(compression == BaselineTIFFTagSet.COMPRESSION_JPEG) {
// Reset tile size per TTN2 spec for JPEG compression.
int subX;
int subY;
if(numBands == 1) {
subX = subY = 1;
} else {
subX = subY = TIFFJPEGCompressor.CHROMA_SUBSAMPLING;
}
if(useTiling) {
int MCUMultipleX = 8*subX;
int MCUMultipleY = 8*subY;
tileWidth =
Math.max(MCUMultipleX*((tileWidth +
MCUMultipleX/2)/MCUMultipleX),
MCUMultipleX);
tileLength =
Math.max(MCUMultipleY*((tileLength +
MCUMultipleY/2)/MCUMultipleY),
MCUMultipleY);
} else if(rowsPerStrip < height) {
int MCUMultiple = 8*Math.max(subX, subY);
rowsPerStrip = tileLength =
Math.max(MCUMultiple*((tileLength +
MCUMultiple/2)/MCUMultiple),
MCUMultiple);
}
// The written image may be unreadable if these fields are present.
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
// Also remove fields related to the old JPEG encoding scheme
// which may be misleading when the compression type is JPEG.
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_PROC);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_RESTART_INTERVAL);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_LOSSLESS_PREDICTORS);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_POINT_TRANSFORMS);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_Q_TABLES);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_DC_TABLES);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_JPEG_AC_TABLES);
} else if(isJPEGInterchange) {
// Force tile size to equal image size.
tileWidth = width;
tileLength = height;
} else if(useTiling) {
// Round tile size to multiple of 16 per TIFF 6.0 specification
// (see pages 67-68 of version 6.0.1 from Adobe).
int tileWidthRemainder = tileWidth % 16;
if(tileWidthRemainder != 0) {
// Round to nearest multiple of 16 not less than 16.
tileWidth = Math.max(16*((tileWidth + 8)/16), 16);
processWarningOccurred(currentImage,
"Tile width rounded to multiple of 16.");
}
int tileLengthRemainder = tileLength % 16;
if(tileLengthRemainder != 0) {
// Round to nearest multiple of 16 not less than 16.
tileLength = Math.max(16*((tileLength + 8)/16), 16);
processWarningOccurred(currentImage,
"Tile height rounded to multiple of 16.");
}
}
this.tilesAcross = (width + tileWidth - 1)/tileWidth;
this.tilesDown = (height + tileLength - 1)/tileLength;
if (!useTiling) {
this.isTiled = false;
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_WIDTH);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_LENGTH);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_OFFSETS);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_TILE_BYTE_COUNTS);
rowsPerStripField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP),
rowsPerStrip);
rootIFD.addTIFFField(rowsPerStripField);
TIFFField stripOffsetsField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_STRIP_OFFSETS),
TIFFTag.TIFF_LONG,
tilesDown);
rootIFD.addTIFFField(stripOffsetsField);
TIFFField stripByteCountsField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS),
TIFFTag.TIFF_LONG,
tilesDown);
rootIFD.addTIFFField(stripByteCountsField);
} else {
this.isTiled = true;
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS);
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS);
TIFFField tileWidthField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_TILE_WIDTH),
tileWidth);
rootIFD.addTIFFField(tileWidthField);
TIFFField tileLengthField =
new TIFFField(base.getTag(BaselineTIFFTagSet.TAG_TILE_LENGTH),
tileLength);
rootIFD.addTIFFField(tileLengthField);
TIFFField tileOffsetsField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_TILE_OFFSETS),
TIFFTag.TIFF_LONG,
tilesDown*tilesAcross);
rootIFD.addTIFFField(tileOffsetsField);
TIFFField tileByteCountsField =
new TIFFField(
base.getTag(BaselineTIFFTagSet.TAG_TILE_BYTE_COUNTS),
TIFFTag.TIFF_LONG,
tilesDown*tilesAcross);
rootIFD.addTIFFField(tileByteCountsField);
}
if(isExif) {
//
// Ensure presence of mandatory fields and absence of prohibited
// fields and those that duplicate information in JPEG marker
// segments per tables 14-18 of the Exif 2.2 specification.
//
// If an empty image is being written or inserted then infer
// that the primary IFD is being set up.
boolean isPrimaryIFD = isEncodingEmpty();
// Handle TIFF fields in order of increasing tag number.
if(compression == BaselineTIFFTagSet.COMPRESSION_OLD_JPEG) {
// ImageWidth
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_IMAGE_WIDTH);
// ImageLength
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_IMAGE_LENGTH);
// BitsPerSample
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_BITS_PER_SAMPLE);
// Compression
if(isPrimaryIFD) {
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_COMPRESSION);
}
// PhotometricInterpretation
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION);
// StripOffsets
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_OFFSETS);
// SamplesPerPixel
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_SAMPLES_PER_PIXEL);
// RowsPerStrip
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_ROWS_PER_STRIP);
// StripByteCounts
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_STRIP_BYTE_COUNTS);
// XResolution and YResolution are handled above for all TIFFs.
// PlanarConfiguration
rootIFD.removeTIFFField(BaselineTIFFTagSet.TAG_PLANAR_CONFIGURATION);
// ResolutionUnit
if(rootIFD.getTIFFField
(BaselineTIFFTagSet.TAG_RESOLUTION_UNIT) == null) {
f = new TIFFField(base.getTag
(BaselineTIFFTagSet.TAG_RESOLUTION_UNIT),
BaselineTIFFTagSet.RESOLUTION_UNIT_INCH);
rootIFD.addTIFFField(f);
}
if(isPrimaryIFD) {
// JPEGInterchangeFormat
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT);
// JPEGInterchangeFormatLength
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
// YCbCrSubsampling
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING);
// YCbCrPositioning
if(rootIFD.getTIFFField
(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING) == null) {
f = new TIFFField
(base.getTag
(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING),
TIFFTag.TIFF_SHORT,
1,
new char[] {
(char)BaselineTIFFTagSet.Y_CB_CR_POSITIONING_CENTERED
});
rootIFD.addTIFFField(f);
}
} else { // Thumbnail IFD
// JPEGInterchangeFormat
f = new TIFFField
(base.getTag
(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT),
TIFFTag.TIFF_LONG,
1);
rootIFD.addTIFFField(f);
// JPEGInterchangeFormatLength
f = new TIFFField
(base.getTag
(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH),
TIFFTag.TIFF_LONG,
1);
rootIFD.addTIFFField(f);
// YCbCrSubsampling
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING);
}
} else { // Uncompressed
// ImageWidth through PlanarConfiguration are set above.
// XResolution and YResolution are handled above for all TIFFs.
// ResolutionUnit
if(rootIFD.getTIFFField
(BaselineTIFFTagSet.TAG_RESOLUTION_UNIT) == null) {
f = new TIFFField(base.getTag
(BaselineTIFFTagSet.TAG_RESOLUTION_UNIT),
BaselineTIFFTagSet.RESOLUTION_UNIT_INCH);
rootIFD.addTIFFField(f);
}
// JPEGInterchangeFormat
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT);
// JPEGInterchangeFormatLength
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
if(photometricInterpretation ==
BaselineTIFFTagSet.PHOTOMETRIC_INTERPRETATION_RGB) {
// YCbCrCoefficients
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_Y_CB_CR_COEFFICIENTS);
// YCbCrSubsampling
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_Y_CB_CR_SUBSAMPLING);
// YCbCrPositioning
rootIFD.removeTIFFField
(BaselineTIFFTagSet.TAG_Y_CB_CR_POSITIONING);
}
}
// Get Exif tags.
TIFFTagSet exifTags = ExifTIFFTagSet.getInstance();
// Retrieve or create the Exif IFD.
TIFFIFD exifIFD = null;
f = rootIFD.getTIFFField
(ExifParentTIFFTagSet.TAG_EXIF_IFD_POINTER);
if(f != null && f.hasDirectory()) {
// Retrieve the Exif IFD.
exifIFD = TIFFIFD.getDirectoryAsIFD(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 {
if (stream == null) {
throw new IllegalStateException("output == null!");
}
markPositions();
write(sm, iioimage, p, true, true);
if (abortRequested()) {
resetPositions();
}
}
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 IllegalArgumentException("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);
if (abortRequested()) {
processWriteAborted();
return;
}
// 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;
}
// 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();
pixelsDone += tileRect.width*tileRect.height;
processImageProgress(100.0F*pixelsDone/totalPixels);
if (abortRequested()) {
processWriteAborted();
return;
}
} catch (IOException e) {
throw new IIOException("I/O error writing TIFF file!", e);
}
}
}
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 IllegalArgumentException("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);
markPositions();
// Seek to the position containing the pointer to the old IFD.
stream.seek(ifdpos[0]);
// Save the previous pointer value in case of abort.
stream.mark();
long prevPointerValue = stream.readUnsignedInt();
stream.reset();
// 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().
if (abortRequested()) {
stream.seek(ifdpos[0]);
stream.writeInt((int)prevPointerValue);
resetPositions();
}
}
// ----- 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 IllegalArgumentException("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 {
if (stream == null) {
throw new IllegalStateException("output == null!");
}
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());
markPositions();
write(streamMetadata, new IIOImage(emptyImage, null, imageMetadata),
param, true, false);
if (abortRequested()) {
resetPositions();
}
}
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, false, false);
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 IllegalArgumentException("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 IllegalArgumentException("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 -----
// Save stream positions for use when aborted.
private void markPositions() throws IOException {
prevStreamPosition = stream.getStreamPosition();
prevHeaderPosition = headerPosition;
prevNextSpace = nextSpace;
}
// Reset to positions saved by markPositions().
private void resetPositions() throws IOException {
stream.seek(prevStreamPosition);
headerPosition = prevHeaderPosition;
nextSpace = prevNextSpace;
}
public void reset() {
super.reset();
stream = null;
image = null;
imageType = null;
byteOrder = null;
param = null;
compressor = null;
colorConverter = null;
streamMetadata = null;
imageMetadata = null;
isRescaling = false;
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;
}
}