//
// TiffTools.java
//
/*
LOCI Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan,
Eric Kjellman and Brian Loranger.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program 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 Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package loci.formats;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.nio.*;
import java.util.*;
import loci.formats.codec.*;
/**
* A utility class for manipulating TIFF files.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/TiffTools.java">Trac</a>,
* <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/TiffTools.java">SVN</a></dd></dl>
*
* @author Curtis Rueden ctrueden at wisc.edu
* @author Eric Kjellman egkjellman at wisc.edu
* @author Melissa Linkert linkert at wisc.edu
* @author Chris Allan callan at blackcat.ca
*/
public final class TiffTools {
// -- Constants --
private static final boolean DEBUG = false;
/** The number of bytes in each IFD entry. */
public static final int BYTES_PER_ENTRY = 12;
/** The number of bytes in each IFD entry of a BigTIFF file. */
public static final int BIG_TIFF_BYTES_PER_ENTRY = 20;
// non-IFD tags (for internal use)
public static final int LITTLE_ENDIAN = 0;
public static final int BIG_TIFF = 1;
// IFD types
public static final int BYTE = 1;
public static final int ASCII = 2;
public static final int SHORT = 3;
public static final int LONG = 4;
public static final int RATIONAL = 5;
public static final int SBYTE = 6;
public static final int UNDEFINED = 7;
public static final int SSHORT = 8;
public static final int SLONG = 9;
public static final int SRATIONAL = 10;
public static final int FLOAT = 11;
public static final int DOUBLE = 12;
public static final int LONG8 = 16;
public static final int SLONG8 = 17;
public static final int IFD8 = 18;
public static final int[] BYTES_PER_ELEMENT = {
-1, // invalid type
1, // BYTE
1, // ASCII
2, // SHORT
4, // LONG
8, // RATIONAL
1, // SBYTE
1, // UNDEFINED
2, // SSHORT
4, // SLONG
8, // SRATIONAL
4, // FLOAT
8, // DOUBLE
-1, // invalid type
-1, // invalid type
-1, // invalid type
-1, // invalid type
8, // LONG8
8, // SLONG8
8 // IFD8
};
// IFD tags
public static final int NEW_SUBFILE_TYPE = 254;
public static final int SUBFILE_TYPE = 255;
public static final int IMAGE_WIDTH = 256;
public static final int IMAGE_LENGTH = 257;
public static final int BITS_PER_SAMPLE = 258;
public static final int COMPRESSION = 259;
public static final int PHOTOMETRIC_INTERPRETATION = 262;
public static final int THRESHHOLDING = 263;
public static final int CELL_WIDTH = 264;
public static final int CELL_LENGTH = 265;
public static final int FILL_ORDER = 266;
public static final int DOCUMENT_NAME = 269;
public static final int IMAGE_DESCRIPTION = 270;
public static final int MAKE = 271;
public static final int MODEL = 272;
public static final int STRIP_OFFSETS = 273;
public static final int ORIENTATION = 274;
public static final int SAMPLES_PER_PIXEL = 277;
public static final int ROWS_PER_STRIP = 278;
public static final int STRIP_BYTE_COUNTS = 279;
public static final int MIN_SAMPLE_VALUE = 280;
public static final int MAX_SAMPLE_VALUE = 281;
public static final int X_RESOLUTION = 282;
public static final int Y_RESOLUTION = 283;
public static final int PLANAR_CONFIGURATION = 284;
public static final int PAGE_NAME = 285;
public static final int X_POSITION = 286;
public static final int Y_POSITION = 287;
public static final int FREE_OFFSETS = 288;
public static final int FREE_BYTE_COUNTS = 289;
public static final int GRAY_RESPONSE_UNIT = 290;
public static final int GRAY_RESPONSE_CURVE = 291;
public static final int T4_OPTIONS = 292;
public static final int T6_OPTIONS = 293;
public static final int RESOLUTION_UNIT = 296;
public static final int PAGE_NUMBER = 297;
public static final int TRANSFER_FUNCTION = 301;
public static final int SOFTWARE = 305;
public static final int DATE_TIME = 306;
public static final int ARTIST = 315;
public static final int HOST_COMPUTER = 316;
public static final int PREDICTOR = 317;
public static final int WHITE_POINT = 318;
public static final int PRIMARY_CHROMATICITIES = 319;
public static final int COLOR_MAP = 320;
public static final int HALFTONE_HINTS = 321;
public static final int TILE_WIDTH = 322;
public static final int TILE_LENGTH = 323;
public static final int TILE_OFFSETS = 324;
public static final int TILE_BYTE_COUNTS = 325;
public static final int INK_SET = 332;
public static final int INK_NAMES = 333;
public static final int NUMBER_OF_INKS = 334;
public static final int DOT_RANGE = 336;
public static final int TARGET_PRINTER = 337;
public static final int EXTRA_SAMPLES = 338;
public static final int SAMPLE_FORMAT = 339;
public static final int S_MIN_SAMPLE_VALUE = 340;
public static final int S_MAX_SAMPLE_VALUE = 341;
public static final int TRANSFER_RANGE = 342;
public static final int JPEG_PROC = 512;
public static final int JPEG_INTERCHANGE_FORMAT = 513;
public static final int JPEG_INTERCHANGE_FORMAT_LENGTH = 514;
public static final int JPEG_RESTART_INTERVAL = 515;
public static final int JPEG_LOSSLESS_PREDICTORS = 517;
public static final int JPEG_POINT_TRANSFORMS = 518;
public static final int JPEG_Q_TABLES = 519;
public static final int JPEG_DC_TABLES = 520;
public static final int JPEG_AC_TABLES = 521;
public static final int Y_CB_CR_COEFFICIENTS = 529;
public static final int Y_CB_CR_SUB_SAMPLING = 530;
public static final int Y_CB_CR_POSITIONING = 531;
public static final int REFERENCE_BLACK_WHITE = 532;
public static final int COPYRIGHT = 33432;
// compression types
public static final int UNCOMPRESSED = 1;
public static final int CCITT_1D = 2;
public static final int GROUP_3_FAX = 3;
public static final int GROUP_4_FAX = 4;
public static final int LZW = 5;
//public static final int JPEG = 6;
public static final int JPEG = 7;
public static final int PACK_BITS = 32773;
public static final int PROPRIETARY_DEFLATE = 32946;
public static final int DEFLATE = 8;
public static final int THUNDERSCAN = 32809;
public static final int NIKON = 34713;
public static final int LURAWAVE = -1;
// photometric interpretation types
public static final int WHITE_IS_ZERO = 0;
public static final int BLACK_IS_ZERO = 1;
public static final int RGB = 2;
public static final int RGB_PALETTE = 3;
public static final int TRANSPARENCY_MASK = 4;
public static final int CMYK = 5;
public static final int Y_CB_CR = 6;
public static final int CIE_LAB = 8;
public static final int CFA_ARRAY = -32733;
// TIFF header constants
public static final int MAGIC_NUMBER = 42;
public static final int BIG_TIFF_MAGIC_NUMBER = 43;
public static final int LITTLE = 0x49;
public static final int BIG = 0x4d;
// -- Constructor --
private TiffTools() { }
// -- TiffTools API methods --
/**
* Tests the given data block to see if it represents
* the first few bytes of a TIFF file.
*/
public static boolean isValidHeader(byte[] block) {
return checkHeader(block) != null;
}
/**
* Checks the TIFF header.
* @return true if little-endian,
* false if big-endian,
* or null if not a TIFF.
*/
public static Boolean checkHeader(byte[] block) {
if (block.length < 4) return null;
// byte order must be II or MM
boolean littleEndian = block[0] == LITTLE && block[1] == LITTLE; // II
boolean bigEndian = block[0] == BIG && block[1] == BIG; // MM
if (!littleEndian && !bigEndian) return null;
// check magic number (42)
short magic = DataTools.bytesToShort(block, 2, littleEndian);
if (magic != MAGIC_NUMBER && magic != BIG_TIFF_MAGIC_NUMBER) return null;
return new Boolean(littleEndian);
}
/** Gets whether this is a BigTIFF IFD. */
public static boolean isBigTiff(Hashtable ifd) throws FormatException {
return ((Boolean)
getIFDValue(ifd, BIG_TIFF, false, Boolean.class)).booleanValue();
}
/** Gets whether the TIFF information in the given IFD is little-endian. */
public static boolean isLittleEndian(Hashtable ifd) throws FormatException {
return ((Boolean)
getIFDValue(ifd, LITTLE_ENDIAN, true, Boolean.class)).booleanValue();
}
// --------------------------- Reading TIFF files ---------------------------
// -- IFD parsing methods --
/**
* Gets all IFDs within the given TIFF file, or null
* if the given file is not a valid TIFF file.
*/
public static Hashtable[] getIFDs(RandomAccessStream in) throws IOException {
// check TIFF header
Boolean result = checkHeader(in);
if (result == null) return null;
in.seek(2);
boolean bigTiff = in.readShort() == BIG_TIFF_MAGIC_NUMBER;
long offset = getFirstOffset(in, bigTiff);
// compute maximum possible number of IFDs, for loop safety
// each IFD must have at least one directory entry, which means that
// each IFD must be at least 2 + 12 + 4 = 18 bytes in length
long ifdMax = (in.length() - 8) / 18;
// read in IFDs
Vector v = new Vector();
for (long ifdNum=0; ifdNum<ifdMax; ifdNum++) {
Hashtable ifd = getIFD(in, ifdNum, offset, bigTiff);
if (ifd == null || ifd.size() <= 1) break;
v.add(ifd);
offset = bigTiff ? in.readLong() : in.readInt();
if (offset <= 0 || offset >= in.length()) break;
}
Hashtable[] ifds = new Hashtable[v.size()];
v.copyInto(ifds);
return ifds;
}
/**
* Gets the first IFD within the given TIFF file, or null
* if the given file is not a valid TIFF file.
*/
public static Hashtable getFirstIFD(RandomAccessStream in) throws IOException
{
// check TIFF header
Boolean result = checkHeader(in);
if (result == null) return null;
long offset = getFirstOffset(in);
return getIFD(in, 0, offset);
}
/**
* Retrieve a given entry from the first IFD in a stream.
*
* @param in the stream to retrieve the entry from.
* @param tag the tag of the entry to be retrieved.
* @return an object representing the entry's fields.
* @throws IOException when there is an error accessing the stream <i>in</i>.
*/
public static TiffIFDEntry getFirstIFDEntry(RandomAccessStream in, int tag)
throws IOException
{
// First lets re-position the file pointer by checking the TIFF header
Boolean result = checkHeader(in);
if (result == null) return null;
// Get the offset of the first IFD
long offset = getFirstOffset(in);
// The following loosely resembles the logic of getIFD()...
in.seek(offset);
int numEntries = in.readShort() & 0xffff;
for (int i = 0; i < numEntries; i++) {
in.seek(offset + // The beginning of the IFD
2 + // The width of the initial numEntries field
BYTES_PER_ENTRY * i);
int entryTag = in.readShort() & 0xffff;
// Skip this tag unless it matches the one we want
if (entryTag != tag) continue;
// Parse the entry's "Type"
int entryType = in.readShort() & 0xffff;
// Parse the entry's "ValueCount"
int valueCount = in.readInt();
if (valueCount < 0) {
throw new RuntimeException("Count of '" + valueCount + "' unexpected.");
}
// Parse the entry's "ValueOffset"
int valueOffset = in.readInt();
return new TiffIFDEntry(entryTag, entryType, valueCount, valueOffset);
}
throw new UnknownTagException();
}
/**
* Checks the TIFF header.
* @return true if little-endian,
* false if big-endian,
* or null if not a TIFF.
*/
public static Boolean checkHeader(RandomAccessStream in) throws IOException {
if (DEBUG) debug("getIFDs: reading IFD entries");
// start at the beginning of the file
in.seek(0);
byte[] header = new byte[4];
in.readFully(header);
Boolean b = checkHeader(header);
if (b != null) in.order(b.booleanValue());
return b;
}
/**
* Gets offset to the first IFD, or -1 if stream is not TIFF.
* Assumes the stream is positioned properly (checkHeader just called).
*/
public static long getFirstOffset(RandomAccessStream in)
throws IOException
{
return getFirstOffset(in, false);
}
/**
* Gets offset to the first IFD, or -1 if stream is not TIFF.
* Assumes the stream is positioned properly (checkHeader just called).
*
* @param bigTiff true if this is a BigTIFF file (8 byte pointers).
*/
public static long getFirstOffset(RandomAccessStream in, boolean bigTiff)
throws IOException
{
if (bigTiff) in.skipBytes(4);
return bigTiff ? in.readLong() : in.readInt();
}
/** Gets the IFD stored at the given offset. */
public static Hashtable getIFD(RandomAccessStream in, long ifdNum,
long offset) throws IOException
{
return getIFD(in, ifdNum, offset, false);
}
/** Gets the IFD stored at the given offset. */
public static Hashtable getIFD(RandomAccessStream in,
long ifdNum, long offset, boolean bigTiff) throws IOException
{
Hashtable ifd = new Hashtable();
// save little-endian flag to internal LITTLE_ENDIAN tag
ifd.put(new Integer(LITTLE_ENDIAN), new Boolean(in.isLittleEndian()));
ifd.put(new Integer(BIG_TIFF), new Boolean(bigTiff));
// read in directory entries for this IFD
if (DEBUG) {
debug("getIFDs: seeking IFD #" + ifdNum + " at " + offset);
}
in.seek(offset);
long numEntries = bigTiff ? in.readLong() : in.readShort() & 0xffff;
if (DEBUG) debug("getIFDs: " + numEntries + " directory entries to read");
if (numEntries == 0 || numEntries == 1) return ifd;
int bytesPerEntry = bigTiff ? BIG_TIFF_BYTES_PER_ENTRY : BYTES_PER_ENTRY;
int baseOffset = bigTiff ? 8 : 2;
int threshhold = bigTiff ? 8 : 4;
for (int i=0; i<numEntries; i++) {
in.seek(offset + baseOffset + bytesPerEntry * i);
int tag = in.readShort() & 0xffff;
int type = in.readShort() & 0xffff;
// BigTIFF case is a slight hack
int count = bigTiff ? (int) (in.readLong() & 0xffffffff) : in.readInt();
if (DEBUG) {
debug("getIFDs: read " + getIFDTagName(tag) +
" (type=" + getIFDTypeName(type) + "; count=" + count + ")");
}
if (count < 0) return null; // invalid data
Object value = null;
if (type == BYTE) {
// 8-bit unsigned integer
if (count > threshhold) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
if (count == 1) value = new Short(in.readByte());
else {
short[] bytes = new short[count];
for (int j=0; j<count; j++) {
bytes[j] = in.readByte();
if (bytes[j] < 0) bytes[j] += 255;
}
value = bytes;
}
}
else if (type == ASCII) {
// 8-bit byte that contain a 7-bit ASCII code;
// the last byte must be NUL (binary zero)
byte[] ascii = new byte[count];
if (count > threshhold) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
in.read(ascii);
// count number of null terminators
int nullCount = 0;
for (int j=0; j<count; j++) {
if (ascii[j] == 0 || j == count - 1) nullCount++;
}
// convert character array to array of strings
String[] strings = nullCount == 1 ? null : new String[nullCount];
String s = null;
int c = 0, ndx = -1;
for (int j=0; j<count; j++) {
if (ascii[j] == 0) {
s = new String(ascii, ndx + 1, j - ndx - 1);
ndx = j;
}
else if (j == count - 1) {
// handle non-null-terminated strings
s = new String(ascii, ndx + 1, j - ndx);
}
else s = null;
if (strings != null && s != null) strings[c++] = s;
}
value = strings == null ? (Object) s : strings;
}
else if (type == SHORT) {
// 16-bit (2-byte) unsigned integer
if (count > threshhold / 2) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
if (count == 1) value = new Integer(in.readShort());
else {
int[] shorts = new int[count];
for (int j=0; j<count; j++) {
shorts[j] = in.readShort() & 0xffff;
}
value = shorts;
}
}
else if (type == LONG) {
// 32-bit (4-byte) unsigned integer
if (count > threshhold / 4) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
if (count == 1) value = new Long(in.readInt());
else {
long[] longs = new long[count];
for (int j=0; j<count; j++) longs[j] = in.readInt();
value = longs;
}
}
else if (type == LONG8 || type == SLONG8 || type == IFD8) {
if (count > threshhold / 8) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
if (count == 1) value = new Long(in.readLong());
else {
long[] longs = new long[count];
for (int j=0; j<count; j++) longs[j] = in.readLong();
value = longs;
}
}
else if (type == RATIONAL || type == SRATIONAL) {
// Two LONGs: the first represents the numerator of a fraction;
// the second, the denominator
// Two SLONG's: the first represents the numerator of a fraction,
// the second the denominator
long pointer = bigTiff ? in.readLong() : in.readInt();
if (count > threshhold / 8) in.seek(pointer);
if (count == 1) value = new TiffRational(in.readInt(), in.readInt());
else {
TiffRational[] rationals = new TiffRational[count];
for (int j=0; j<count; j++) {
rationals[j] = new TiffRational(in.readInt(), in.readInt());
}
value = rationals;
}
}
else if (type == SBYTE || type == UNDEFINED) {
// SBYTE: An 8-bit signed (twos-complement) integer
// UNDEFINED: An 8-bit byte that may contain anything,
// depending on the definition of the field
if (count > threshhold) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
if (count == 1) value = new Byte(in.readByte());
else {
byte[] sbytes = new byte[count];
in.readFully(sbytes);
value = sbytes;
}
}
else if (type == SSHORT) {
// A 16-bit (2-byte) signed (twos-complement) integer
if (count > threshhold / 2) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
if (count == 1) value = new Short(in.readShort());
else {
short[] sshorts = new short[count];
for (int j=0; j<count; j++) sshorts[j] = in.readShort();
value = sshorts;
}
}
else if (type == SLONG) {
// A 32-bit (4-byte) signed (twos-complement) integer
if (count > threshhold / 4) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
if (count == 1) value = new Integer(in.readInt());
else {
int[] slongs = new int[count];
for (int j=0; j<count; j++) slongs[j] = in.readInt();
value = slongs;
}
}
else if (type == FLOAT) {
// Single precision (4-byte) IEEE format
if (count > threshhold / 4) {
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
}
if (count == 1) value = new Float(in.readFloat());
else {
float[] floats = new float[count];
for (int j=0; j<count; j++) floats[j] = in.readFloat();
value = floats;
}
}
else if (type == DOUBLE) {
// Double precision (8-byte) IEEE format
long pointer = bigTiff ? in.readLong() : in.readInt();
in.seek(pointer);
if (count == 1) value = new Double(in.readDouble());
else {
double[] doubles = new double[count];
for (int j=0; j<count; j++) {
doubles[j] = in.readDouble();
}
value = doubles;
}
}
if (value != null) ifd.put(new Integer(tag), value);
}
in.seek(offset + baseOffset + bytesPerEntry * numEntries);
return ifd;
}
/** Gets the name of the IFD tag encoded by the given number. */
public static String getIFDTagName(int tag) { return getFieldName(tag); }
/** Gets the name of the IFD type encoded by the given number. */
public static String getIFDTypeName(int type) { return getFieldName(type); }
/**
* This method uses reflection to scan the values of this class's
* static fields, returning the first matching field's name. It is
* probably not very efficient, and is mainly intended for debugging.
*/
public static String getFieldName(int value) {
Field[] fields = TiffTools.class.getFields();
for (int i=0; i<fields.length; i++) {
try {
if (fields[i].getInt(null) == value) return fields[i].getName();
}
catch (IllegalAccessException exc) { }
catch (IllegalArgumentException exc) { }
}
return "" + value;
}
/** Gets the given directory entry value from the specified IFD. */
public static Object getIFDValue(Hashtable ifd, int tag) {
return ifd.get(new Integer(tag));
}
/**
* Gets the given directory entry value from the specified IFD,
* performing some error checking.
*/
public static Object getIFDValue(Hashtable ifd,
int tag, boolean checkNull, Class checkClass) throws FormatException
{
Object value = ifd.get(new Integer(tag));
if (checkNull && value == null) {
throw new FormatException(
getIFDTagName(tag) + " directory entry not found");
}
if (checkClass != null && value != null &&
!checkClass.isInstance(value))
{
// wrap object in array of length 1, if appropriate
Class cType = checkClass.getComponentType();
Object array = null;
if (cType == value.getClass()) {
array = Array.newInstance(value.getClass(), 1);
Array.set(array, 0, value);
}
if (cType == boolean.class && value instanceof Boolean) {
array = Array.newInstance(boolean.class, 1);
Array.setBoolean(array, 0, ((Boolean) value).booleanValue());
}
else if (cType == byte.class && value instanceof Byte) {
array = Array.newInstance(byte.class, 1);
Array.setByte(array, 0, ((Byte) value).byteValue());
}
else if (cType == char.class && value instanceof Character) {
array = Array.newInstance(char.class, 1);
Array.setChar(array, 0, ((Character) value).charValue());
}
else if (cType == double.class && value instanceof Double) {
array = Array.newInstance(double.class, 1);
Array.setDouble(array, 0, ((Double) value).doubleValue());
}
else if (cType == float.class && value instanceof Float) {
array = Array.newInstance(float.class, 1);
Array.setFloat(array, 0, ((Float) value).floatValue());
}
else if (cType == int.class && value instanceof Integer) {
array = Array.newInstance(int.class, 1);
Array.setInt(array, 0, ((Integer) value).intValue());
}
else if (cType == long.class && value instanceof Long) {
array = Array.newInstance(long.class, 1);
Array.setLong(array, 0, ((Long) value).longValue());
}
else if (cType == short.class && value instanceof Short) {
array = Array.newInstance(short.class, 1);
Array.setShort(array, 0, ((Short) value).shortValue());
}
if (array != null) return array;
throw new FormatException(getIFDTagName(tag) +
" directory entry is the wrong type (got " +
value.getClass().getName() + ", expected " + checkClass.getName());
}
return value;
}
/**
* Gets the given directory entry value in long format from the
* specified IFD, performing some error checking.
*/
public static long getIFDLongValue(Hashtable ifd, int tag,
boolean checkNull, long defaultValue) throws FormatException
{
long value = defaultValue;
Number number = (Number) getIFDValue(ifd, tag, checkNull, Number.class);
if (number != null) value = number.longValue();
return value;
}
/**
* Gets the given directory entry value in int format from the
* specified IFD, or -1 if the given directory does not exist.
*/
public static int getIFDIntValue(Hashtable ifd, int tag) {
int value = -1;
try {
value = getIFDIntValue(ifd, tag, false, -1);
}
catch (FormatException exc) { }
return value;
}
/**
* Gets the given directory entry value in int format from the
* specified IFD, performing some error checking.
*/
public static int getIFDIntValue(Hashtable ifd, int tag,
boolean checkNull, int defaultValue) throws FormatException
{
int value = defaultValue;
Number number = (Number) getIFDValue(ifd, tag, checkNull, Number.class);
if (number != null) value = number.intValue();
return value;
}
/**
* Gets the given directory entry value in rational format from the
* specified IFD, performing some error checking.
*/
public static TiffRational getIFDRationalValue(Hashtable ifd, int tag,
boolean checkNull) throws FormatException
{
return (TiffRational) getIFDValue(ifd, tag, checkNull, TiffRational.class);
}
/**
* Gets the given directory entry values in long format
* from the specified IFD, performing some error checking.
*/
public static long[] getIFDLongArray(Hashtable ifd,
int tag, boolean checkNull) throws FormatException
{
Object value = getIFDValue(ifd, tag, checkNull, null);
long[] results = null;
if (value instanceof long[]) results = (long[]) value;
else if (value instanceof Number) {
results = new long[] {((Number) value).longValue()};
}
else if (value instanceof Number[]) {
Number[] numbers = (Number[]) value;
results = new long[numbers.length];
for (int i=0; i<results.length; i++) results[i] = numbers[i].longValue();
}
else if (value instanceof int[]) { // convert int[] to long[]
int[] integers = (int[]) value;
results = new long[integers.length];
for (int i=0; i<integers.length; i++) results[i] = integers[i];
}
else if (value != null) {
throw new FormatException(getIFDTagName(tag) +
" directory entry is the wrong type (got " +
value.getClass().getName() +
", expected Number, long[], Number[] or int[])");
}
return results;
}
/**
* Gets the given directory entry values in int format
* from the specified IFD, performing some error checking.
*/
public static int[] getIFDIntArray(Hashtable ifd,
int tag, boolean checkNull) throws FormatException
{
Object value = getIFDValue(ifd, tag, checkNull, null);
int[] results = null;
if (value instanceof int[]) results = (int[]) value;
else if (value instanceof Number) {
results = new int[] {((Number) value).intValue()};
}
else if (value instanceof Number[]) {
Number[] numbers = (Number[]) value;
results = new int[numbers.length];
for (int i=0; i<results.length; i++) results[i] = numbers[i].intValue();
}
else if (value != null) {
throw new FormatException(getIFDTagName(tag) +
" directory entry is the wrong type (got " +
value.getClass().getName() + ", expected Number, int[] or Number[])");
}
return results;
}
/**
* Gets the given directory entry values in short format
* from the specified IFD, performing some error checking.
*/
public static short[] getIFDShortArray(Hashtable ifd,
int tag, boolean checkNull) throws FormatException
{
Object value = getIFDValue(ifd, tag, checkNull, null);
short[] results = null;
if (value instanceof short[]) results = (short[]) value;
else if (value instanceof Number) {
results = new short[] {((Number) value).shortValue()};
}
else if (value instanceof Number[]) {
Number[] numbers = (Number[]) value;
results = new short[numbers.length];
for (int i=0; i<results.length; i++) {
results[i] = numbers[i].shortValue();
}
}
else if (value != null) {
throw new FormatException(getIFDTagName(tag) +
" directory entry is the wrong type (got " +
value.getClass().getName() +
", expected Number, short[] or Number[])");
}
return results;
}
/** Convenience method for obtaining a file's first ImageDescription. */
public static String getComment(String id)
throws FormatException, IOException
{
// read first IFD
RandomAccessStream in = new RandomAccessStream(id);
Hashtable ifd = TiffTools.getFirstIFD(in);
in.close();
// extract comment
Object o = TiffTools.getIFDValue(ifd, TiffTools.IMAGE_DESCRIPTION);
String comment = null;
if (o instanceof String) comment = (String) o;
else if (o instanceof String[]) {
String[] s = (String[]) o;
if (s.length > 0) comment = s[0];
}
else if (o != null) comment = o.toString();
if (comment != null) {
// sanitize line feeds
comment = comment.replaceAll("\r\n", "\n");
comment = comment.replaceAll("\r", "\n");
}
return comment;
}
// -- Image reading methods --
/** Reads the image defined in the given IFD from the specified file. */
public static byte[][] getSamples(Hashtable ifd, RandomAccessStream in)
throws FormatException, IOException
{
int samplesPerPixel = getSamplesPerPixel(ifd);
int photoInterp = getPhotometricInterpretation(ifd);
int bpp = getBitsPerSample(ifd)[0];
while ((bpp % 8) != 0) bpp++;
bpp /= 8;
long width = getImageWidth(ifd);
long length = getImageLength(ifd);
byte[] b = new byte[(int) (width * length * samplesPerPixel * bpp)];
getSamples(ifd, in, b);
byte[][] samples = new byte[samplesPerPixel][(int) (width * length * bpp)];
for (int i=0; i<samplesPerPixel; i++) {
System.arraycopy(b, (int) (i*width*length*bpp), samples[i], 0,
samples[i].length);
}
b = null;
return samples;
}
public static byte[] getSamples(Hashtable ifd, RandomAccessStream in,
byte[] buf) throws FormatException, IOException
{
if (DEBUG) debug("parsing IFD entries");
// get internal non-IFD entries
boolean littleEndian = isLittleEndian(ifd);
in.order(littleEndian);
// get relevant IFD entries
long imageWidth = getImageWidth(ifd);
long imageLength = getImageLength(ifd);
int[] bitsPerSample = getBitsPerSample(ifd);
int samplesPerPixel = getSamplesPerPixel(ifd);
int compression = getCompression(ifd);
int photoInterp = getPhotometricInterpretation(ifd);
long[] stripOffsets = getStripOffsets(ifd);
long[] stripByteCounts = getStripByteCounts(ifd);
long[] rowsPerStripArray = getRowsPerStrip(ifd);
boolean fakeByteCounts = stripByteCounts == null;
boolean fakeRPS = rowsPerStripArray == null;
boolean isTiled = stripOffsets == null;
long[] maxes = getIFDLongArray(ifd, MAX_SAMPLE_VALUE, false);
long maxValue = maxes == null ? 0 : maxes[0];
if (isTiled) {
stripOffsets = getIFDLongArray(ifd, TILE_OFFSETS, true);
stripByteCounts = getIFDLongArray(ifd, TILE_BYTE_COUNTS, true);
rowsPerStripArray = new long[] {imageLength};
}
else if (fakeByteCounts) {
// technically speaking, this shouldn't happen (since TIFF writers are
// required to write the StripByteCounts tag), but we'll support it
// anyway
// don't rely on RowsPerStrip, since it's likely that if the file doesn't
// have the StripByteCounts tag, it also won't have the RowsPerStrip tag
stripByteCounts = new long[stripOffsets.length];
if (stripByteCounts.length == 1) {
stripByteCounts[0] = imageWidth * imageLength * (bitsPerSample[0] / 8);
}
else {
stripByteCounts[0] = stripOffsets[0];
for (int i=1; i<stripByteCounts.length; i++) {
stripByteCounts[i] = stripOffsets[i] - stripByteCounts[i-1];
}
}
}
boolean lastBitsZero = bitsPerSample[bitsPerSample.length - 1] == 0;
if (fakeRPS && !isTiled) {
// create a false rowsPerStripArray if one is not present
// it's sort of a cheap hack, but here's how it's done:
// RowsPerStrip = stripByteCounts / (imageLength * bitsPerSample)
// since stripByteCounts and bitsPerSample are arrays, we have to
// iterate through each item
rowsPerStripArray = new long[bitsPerSample.length];
long temp = stripByteCounts[0];
stripByteCounts = new long[bitsPerSample.length];
for (int i=0; i<stripByteCounts.length; i++) stripByteCounts[i] = temp;
temp = bitsPerSample[0];
if (temp == 0) temp = 8;
bitsPerSample = new int[bitsPerSample.length];
for (int i=0; i<bitsPerSample.length; i++) bitsPerSample[i] = (int) temp;
temp = stripOffsets[0];
/*
stripOffsets = new long[bitsPerSample.length];
for (int i=0; i<bitsPerSample.length; i++) {
stripOffsets[i] = i == 0 ? temp :
stripOffsets[i - 1] + stripByteCounts[i];
}
*/
// we have two files that reverse the endianness for BitsPerSample,
// StripOffsets, and StripByteCounts
if (bitsPerSample[0] > 64) {
byte[] bps = new byte[2];
byte[] stripOffs = new byte[4];
byte[] byteCounts = new byte[4];
if (littleEndian) {
bps[0] = (byte) (bitsPerSample[0] & 0xff);
bps[1] = (byte) ((bitsPerSample[0] >>> 8) & 0xff);
int ndx = stripOffsets.length - 1;
stripOffs[0] = (byte) (stripOffsets[ndx] & 0xff);
stripOffs[1] = (byte) ((stripOffsets[ndx] >>> 8) & 0xff);
stripOffs[2] = (byte) ((stripOffsets[ndx] >>> 16) & 0xff);
stripOffs[3] = (byte) ((stripOffsets[ndx] >>> 24) & 0xff);
ndx = stripByteCounts.length - 1;
byteCounts[0] = (byte) (stripByteCounts[ndx] & 0xff);
byteCounts[1] = (byte) ((stripByteCounts[ndx] >>> 8) & 0xff);
byteCounts[2] = (byte) ((stripByteCounts[ndx] >>> 16) & 0xff);
byteCounts[3] = (byte) ((stripByteCounts[ndx] >>> 24) & 0xff);
}
else {
bps[1] = (byte) ((bitsPerSample[0] >>> 16) & 0xff);
bps[0] = (byte) ((bitsPerSample[0] >>> 24) & 0xff);
stripOffs[3] = (byte) (stripOffsets[0] & 0xff);
stripOffs[2] = (byte) ((stripOffsets[0] >>> 8) & 0xff);
stripOffs[1] = (byte) ((stripOffsets[0] >>> 16) & 0xff);
stripOffs[0] = (byte) ((stripOffsets[0] >>> 24) & 0xff);
byteCounts[3] = (byte) (stripByteCounts[0] & 0xff);
byteCounts[2] = (byte) ((stripByteCounts[0] >>> 8) & 0xff);
byteCounts[1] = (byte) ((stripByteCounts[0] >>> 16) & 0xff);
byteCounts[0] = (byte) ((stripByteCounts[0] >>> 24) & 0xff);
}
bitsPerSample[0] = DataTools.bytesToInt(bps, !littleEndian);
stripOffsets[0] = DataTools.bytesToInt(stripOffs, !littleEndian);
stripByteCounts[0] = DataTools.bytesToInt(byteCounts, !littleEndian);
}
if (rowsPerStripArray.length == 1 && stripByteCounts[0] !=
(imageWidth * imageLength * (bitsPerSample[0] / 8)) &&
compression == UNCOMPRESSED)
{
for (int i=0; i<stripByteCounts.length; i++) {
stripByteCounts[i] =
imageWidth * imageLength * (bitsPerSample[i] / 8);
stripOffsets[0] = (int) (in.length() - stripByteCounts[0] -
48 * imageWidth);
if (i != 0) {
stripOffsets[i] = stripOffsets[i - 1] + stripByteCounts[i];
}
in.seek((int) stripOffsets[i]);
in.read(buf, (int) (i*imageWidth), (int) imageWidth);
boolean isZero = true;
for (int j=0; j<imageWidth; j++) {
if (buf[(int) (i*imageWidth + j)] != 0) {
isZero = false;
break;
}
}
while (isZero) {
stripOffsets[i] -= imageWidth;
in.seek((int) stripOffsets[i]);
in.read(buf, (int) (i*imageWidth), (int) imageWidth);
for (int j=0; j<imageWidth; j++) {
if (buf[(int) (i*imageWidth + j)] != 0) {
isZero = false;
stripOffsets[i] -= (stripByteCounts[i] - imageWidth);
break;
}
}
}
}
}
for (int i=0; i<bitsPerSample.length; i++) {
// case 1: we're still within bitsPerSample array bounds
if (i < bitsPerSample.length) {
if (i == samplesPerPixel) {
bitsPerSample[i] = 0;
lastBitsZero = true;
}
// remember that the universe collapses when we divide by 0
if (bitsPerSample[i] != 0) {
rowsPerStripArray[i] = (long) stripByteCounts[i] /
(imageWidth * (bitsPerSample[i] / 8));
}
else if (bitsPerSample[i] == 0 && i > 0) {
rowsPerStripArray[i] = (long) stripByteCounts[i] /
(imageWidth * (bitsPerSample[i - 1] / 8));
bitsPerSample[i] = bitsPerSample[i - 1];
}
else {
throw new FormatException("BitsPerSample is 0");
}
}
// case 2: we're outside bitsPerSample array bounds
else if (i >= bitsPerSample.length) {
rowsPerStripArray[i] = (long) stripByteCounts[i] /
(imageWidth * (bitsPerSample[bitsPerSample.length - 1] / 8));
}
}
//samplesPerPixel = stripOffsets.length;
}
if (lastBitsZero) {
bitsPerSample[bitsPerSample.length - 1] = 0;
//samplesPerPixel--;
}
TiffRational xResolution = getIFDRationalValue(ifd, X_RESOLUTION, false);
TiffRational yResolution = getIFDRationalValue(ifd, Y_RESOLUTION, false);
int planarConfig = getIFDIntValue(ifd, PLANAR_CONFIGURATION, false, 1);
int resolutionUnit = getIFDIntValue(ifd, RESOLUTION_UNIT, false, 2);
if (xResolution == null || yResolution == null) resolutionUnit = 0;
int[] colorMap = getIFDIntArray(ifd, COLOR_MAP, false);
int predictor = getIFDIntValue(ifd, PREDICTOR, false, 1);
// If the subsequent color maps are empty, use the first IFD's color map
//if (colorMap == null) {
// colorMap = getIFDIntArray(getFirstIFD(in), COLOR_MAP, false);
//}
// use special color map for YCbCr
if (photoInterp == Y_CB_CR) {
int[] tempColorMap = getIFDIntArray(ifd, Y_CB_CR_COEFFICIENTS, false);
int[] refBlackWhite = getIFDIntArray(ifd, REFERENCE_BLACK_WHITE, false);
colorMap = new int[tempColorMap.length + refBlackWhite.length];
System.arraycopy(tempColorMap, 0, colorMap, 0, tempColorMap.length);
System.arraycopy(refBlackWhite, 0, colorMap, tempColorMap.length,
refBlackWhite.length);
}
if (DEBUG) {
StringBuffer sb = new StringBuffer();
sb.append("IFD directory entry values:");
sb.append("\n\tLittleEndian=");
sb.append(littleEndian);
sb.append("\n\tImageWidth=");
sb.append(imageWidth);
sb.append("\n\tImageLength=");
sb.append(imageLength);
sb.append("\n\tBitsPerSample=");
sb.append(bitsPerSample[0]);
for (int i=1; i<bitsPerSample.length; i++) {
sb.append(",");
sb.append(bitsPerSample[i]);
}
sb.append("\n\tSamplesPerPixel=");
sb.append(samplesPerPixel);
sb.append("\n\tCompression=");
sb.append(compression);
sb.append("\n\tPhotometricInterpretation=");
sb.append(photoInterp);
sb.append("\n\tStripOffsets=");
sb.append(stripOffsets[0]);
for (int i=1; i<stripOffsets.length; i++) {
sb.append(",");
sb.append(stripOffsets[i]);
}
sb.append("\n\tRowsPerStrip=");
sb.append(rowsPerStripArray[0]);
for (int i=1; i<rowsPerStripArray.length; i++) {
sb.append(",");
sb.append(rowsPerStripArray[i]);
}
sb.append("\n\tStripByteCounts=");
sb.append(stripByteCounts[0]);
for (int i=1; i<stripByteCounts.length; i++) {
sb.append(",");
sb.append(stripByteCounts[i]);
}
sb.append("\n\tXResolution=");
sb.append(xResolution);
sb.append("\n\tYResolution=");
sb.append(yResolution);
sb.append("\n\tPlanarConfiguration=");
sb.append(planarConfig);
sb.append("\n\tResolutionUnit=");
sb.append(resolutionUnit);
sb.append("\n\tColorMap=");
if (colorMap == null) sb.append("null");
else {
sb.append(colorMap[0]);
for (int i=1; i<colorMap.length; i++) {
sb.append(",");
sb.append(colorMap[i]);
}
}
sb.append("\n\tPredictor=");
sb.append(predictor);
debug(sb.toString());
}
for (int i=0; i<samplesPerPixel; i++) {
if (bitsPerSample[i] < 1) {
throw new FormatException("Illegal BitsPerSample (" +
bitsPerSample[i] + ")");
}
// don't support odd numbers of bits (except for 1)
else if (bitsPerSample[i] % 2 != 0 && bitsPerSample[i] != 1) {
throw new FormatException("Sorry, unsupported BitsPerSample (" +
bitsPerSample[i] + ")");
}
}
if (bitsPerSample.length < samplesPerPixel) {
throw new FormatException("BitsPerSample length (" +
bitsPerSample.length + ") does not match SamplesPerPixel (" +
samplesPerPixel + ")");
}
else if (photoInterp == TRANSPARENCY_MASK) {
throw new FormatException(
"Sorry, Transparency Mask PhotometricInterpretation is not supported");
}
else if (photoInterp == Y_CB_CR) {
throw new FormatException(
"Sorry, YCbCr PhotometricInterpretation is not supported");
}
else if (photoInterp == CIE_LAB) {
throw new FormatException(
"Sorry, CIELAB PhotometricInterpretation is not supported");
}
else if (photoInterp != WHITE_IS_ZERO &&
photoInterp != BLACK_IS_ZERO && photoInterp != RGB &&
photoInterp != RGB_PALETTE && photoInterp != CMYK &&
photoInterp != Y_CB_CR && photoInterp != CFA_ARRAY)
{
throw new FormatException("Unknown PhotometricInterpretation (" +
photoInterp + ")");
}
long rowsPerStrip = rowsPerStripArray[0];
for (int i=1; i<rowsPerStripArray.length; i++) {
if (rowsPerStrip != rowsPerStripArray[i]) {
throw new FormatException(
"Sorry, non-uniform RowsPerStrip is not supported");
}
}
long numStrips = (imageLength + rowsPerStrip - 1) / rowsPerStrip;
if (isTiled || fakeRPS) numStrips = stripOffsets.length;
if (planarConfig == 2) numStrips *= samplesPerPixel;
if (stripOffsets.length < numStrips && !fakeRPS) {
throw new FormatException("StripOffsets length (" +
stripOffsets.length + ") does not match expected " +
"number of strips (" + numStrips + ")");
}
else if (fakeRPS) numStrips = stripOffsets.length;
if (stripByteCounts.length < numStrips) {
throw new FormatException("StripByteCounts length (" +
stripByteCounts.length + ") does not match expected " +
"number of strips (" + numStrips + ")");
}
if (imageWidth > Integer.MAX_VALUE || imageLength > Integer.MAX_VALUE ||
imageWidth * imageLength > Integer.MAX_VALUE)
{
throw new FormatException("Sorry, ImageWidth x ImageLength > " +
Integer.MAX_VALUE + " is not supported (" +
imageWidth + " x " + imageLength + ")");
}
int numSamples = (int) (imageWidth * imageLength);
if (planarConfig != 1 && planarConfig != 2) {
throw new FormatException(
"Unknown PlanarConfiguration (" + planarConfig + ")");
}
// read in image strips
if (DEBUG) {
debug("reading image data (samplesPerPixel=" +
samplesPerPixel + "; numSamples=" + numSamples + ")");
}
if (photoInterp == CFA_ARRAY) {
int[] tempMap = new int[colorMap.length + 2];
System.arraycopy(colorMap, 0, tempMap, 0, colorMap.length);
tempMap[tempMap.length - 2] = (int) imageWidth;
tempMap[tempMap.length - 1] = (int) imageLength;
colorMap = tempMap;
}
if (stripOffsets.length > 1 && (stripOffsets[stripOffsets.length - 1] ==
stripOffsets[stripOffsets.length - 2]))
{
long[] tmp = stripOffsets;
stripOffsets = new long[tmp.length - 1];
System.arraycopy(tmp, 0, stripOffsets, 0, stripOffsets.length);
numStrips--;
}
short[][] samples = new short[samplesPerPixel][numSamples];
byte[] altBytes = new byte[0];
if (bitsPerSample[0] == 16) littleEndian = !littleEndian;
if (isTiled) {
long tileWidth = getIFDLongValue(ifd, TILE_WIDTH, true, 0);
long tileLength = getIFDLongValue(ifd, TILE_LENGTH, true, 0);
byte[] data = new byte[(int) (imageWidth * imageLength *
samplesPerPixel * (bitsPerSample[0] / 8))];
int row = 0;
int col = 0;
int bytes = bitsPerSample[0] / 8;
for (int i=0; i<stripOffsets.length; i++) {
byte[] b = new byte[(int) stripByteCounts[i]];
in.seek(stripOffsets[i]);
in.read(b);
b = uncompress(b, compression);
int ext = (int) (b.length / (tileWidth * tileLength));
int rowBytes = (int) (tileWidth * ext);
if (tileWidth + col > imageWidth) {
rowBytes = (int) ((imageWidth - col) * ext);
}
for (int j=0; j<tileLength; j++) {
if (row + j < imageLength) {
System.arraycopy(b, rowBytes*j, data,
(int) ((row + j)*imageWidth*ext + ext*col), rowBytes);
}
else break;
}
// update row and column
col += (int) tileWidth;
if (col >= imageWidth) {
row += (int) tileLength;
col = 0;
}
}
undifference(data, bitsPerSample, imageWidth, planarConfig, predictor);
unpackBytes(samples, 0, data, bitsPerSample, photoInterp, colorMap,
littleEndian, maxValue, planarConfig, 0, 1, imageWidth);
}
else {
int overallOffset = 0;
for (int strip=0, row=0; strip<numStrips; strip++, row+=rowsPerStrip) {
try {
if (DEBUG) debug("reading image strip #" + strip);
in.seek((int) stripOffsets[strip]);
if (stripByteCounts[strip] > Integer.MAX_VALUE) {
throw new FormatException("Sorry, StripByteCounts > " +
Integer.MAX_VALUE + " is not supported");
}
byte[] bytes = new byte[(int) stripByteCounts[strip]];
in.read(bytes);
if (compression != PACK_BITS) {
bytes = uncompress(bytes, compression);
undifference(bytes, bitsPerSample,
imageWidth, planarConfig, predictor);
int offset = (int) (imageWidth * row);
if (planarConfig == 2) {
offset = overallOffset / samplesPerPixel;
}
unpackBytes(samples, offset, bytes, bitsPerSample,
photoInterp, colorMap, littleEndian, maxValue, planarConfig,
strip, (int) numStrips, imageWidth);
overallOffset += bytes.length / bitsPerSample.length;
}
else {
// concatenate contents of bytes to altBytes
byte[] tempPackBits = new byte[altBytes.length];
System.arraycopy(altBytes, 0, tempPackBits, 0, altBytes.length);
altBytes = new byte[altBytes.length + bytes.length];
System.arraycopy(tempPackBits, 0, altBytes, 0, tempPackBits.length);
System.arraycopy(bytes, 0, altBytes,
tempPackBits.length, bytes.length);
}
}
catch (Exception e) {
// CTR TODO - eliminate catch-all exception handling
if (strip == 0) {
if (e instanceof FormatException) throw (FormatException) e;
else throw new FormatException(e);
}
byte[] bytes = new byte[samples[0].length];
undifference(bytes, bitsPerSample, imageWidth, planarConfig,
predictor);
int offset = (int) (imageWidth * row);
if (planarConfig == 2) offset = overallOffset / samplesPerPixel;
unpackBytes(samples, offset, bytes, bitsPerSample, photoInterp,
colorMap, littleEndian, maxValue, planarConfig,
strip, (int) numStrips, imageWidth);
overallOffset += bytes.length / bitsPerSample.length;
}
}
}
// only do this if the image uses PackBits compression
if (altBytes.length != 0) {
altBytes = uncompress(altBytes, compression);
undifference(altBytes, bitsPerSample,
imageWidth, planarConfig, predictor);
unpackBytes(samples, (int) imageWidth, altBytes, bitsPerSample,
photoInterp, colorMap, littleEndian, maxValue, planarConfig, 0, 1,
imageWidth);
}
// construct field
if (DEBUG) debug("constructing image");
// Since the lowest common denominator for all pixel operations is "byte"
// we're going to normalize everything to byte.
if (bitsPerSample[0] == 12) bitsPerSample[0] = 16;
if (photoInterp == CFA_ARRAY) {
samples =
ImageTools.demosaic(samples, (int) imageWidth, (int) imageLength);
}
if (bitsPerSample[0] == 16) {
int pt = 0;
for (int i = 0; i < samplesPerPixel; i++) {
for (int j = 0; j < numSamples; j++) {
buf[pt++] = (byte) ((samples[i][j] & 0xff00) >> 8);
buf[pt++] = (byte) (samples[i][j] & 0xff);
}
}
}
else if (bitsPerSample[0] == 32) {
int pt = 0;
for (int i=0; i<samplesPerPixel; i++) {
for (int j=0; j<numSamples; j++) {
buf[pt++] = (byte) ((samples[i][j] & 0xff000000) >> 24);
buf[pt++] = (byte) ((samples[i][j] & 0xff0000) >> 16);
buf[pt++] = (byte) ((samples[i][j] & 0xff00) >> 8);
buf[pt++] = (byte) (samples[i][j] & 0xff);
}
}
}
else {
for (int i=0; i<samplesPerPixel; i++) {
for (int j=0; j<numSamples; j++) {
buf[j + i*numSamples] = (byte) samples[i][j];
}
}
}
return buf;
}
/** Reads the image defined in the given IFD from the specified file. */
public static BufferedImage getImage(Hashtable ifd, RandomAccessStream in)
throws FormatException, IOException
{
// construct field
if (DEBUG) debug("constructing image");
byte[][] samples = getSamples(ifd, in);
int[] bitsPerSample = getBitsPerSample(ifd);
long imageWidth = getImageWidth(ifd);
long imageLength = getImageLength(ifd);
int samplesPerPixel = getSamplesPerPixel(ifd);
int photoInterp = getPhotometricInterpretation(ifd);
if (bitsPerSample[0] == 16 || bitsPerSample[0] == 12) {
// First wrap the byte arrays and then use the features of the
// ByteBuffer to transform to a ShortBuffer. Finally, use the ShortBuffer
// bulk get method to copy the data into a usable form for makeImage().
int len = samples.length == 2 ? 3 : samples.length;
short[][] sampleData = new short[len][samples[0].length / 2];
for (int i = 0; i < samplesPerPixel; i++) {
ShortBuffer sampleBuf = ByteBuffer.wrap(samples[i]).asShortBuffer();
sampleBuf.get(sampleData[i]);
}
// Now make our image.
return ImageTools.makeImage(sampleData,
(int) imageWidth, (int) imageLength);
}
else if (bitsPerSample[0] == 24) {
int[][] intData = new int[samplesPerPixel][samples[0].length / 3];
for (int i=0; i<samplesPerPixel; i++) {
for (int j=0; j<intData[i].length; j++) {
intData[i][j] = DataTools.bytesToInt(samples[i], j*3, 3,
isLittleEndian(ifd));
}
}
return ImageTools.makeImage(intData, (int) imageWidth, (int) imageLength);
}
else if (bitsPerSample[0] == 32) {
int type = getIFDIntValue(ifd, SAMPLE_FORMAT);
if (type == 3) {
// float data
float[][] floatData = new float[samplesPerPixel][samples[0].length / 4];
for (int i=0; i<samplesPerPixel; i++) {
floatData[i] = (float[]) DataTools.makeDataArray(samples[i], 4, true,
isLittleEndian(ifd));
}
return ImageTools.makeImage(floatData,
(int) imageWidth, (int) imageLength);
}
else {
// int data
int[][] intData = new int[samplesPerPixel][samples[0].length / 4];
for (int i=0; i<samplesPerPixel; i++) {
IntBuffer sampleBuf = ByteBuffer.wrap(samples[i]).asIntBuffer();
sampleBuf.get(intData[i]);
}
byte[][] shortData = new byte[samplesPerPixel][intData[0].length];
for (int i=0; i<samplesPerPixel; i++) {
for (int j=0; j<shortData[0].length; j++) {
shortData[i][j] = (byte) intData[i][j];
}
}
return ImageTools.makeImage(shortData,
(int) imageWidth, (int) imageLength);
}
}
if (samplesPerPixel == 1) {
return ImageTools.makeImage(samples[0], (int) imageWidth,
(int) imageLength, 1, false);
}
return ImageTools.makeImage(samples, (int) imageWidth, (int) imageLength);
}
/**
* Extracts pixel information from the given byte array according to the
* bits per sample, photometric interpretation, and the specified byte
* ordering.
* No error checking is performed.
* This method is tailored specifically for planar (separated) images.
*/
public static void planarUnpack(short[][] samples, int startIndex,
byte[] bytes, int[] bitsPerSample, int photoInterp, boolean littleEndian,
int strip, int numStrips) throws FormatException
{
int numChannels = bitsPerSample.length; // this should always be 3
if (bitsPerSample[bitsPerSample.length - 1] == 0) numChannels--;
// determine which channel the strip belongs to
int channelNum = strip / (numStrips / numChannels);
startIndex = (strip % (numStrips / numChannels)) * bytes.length;
int index = 0;
int counter = 0;
for (int j=0; j<bytes.length; j++) {
int numBytes = bitsPerSample[0] / 8;
if (bitsPerSample[0] % 8 != 0) {
// bits per sample is not a multiple of 8
//
// while images in this category are not in violation of baseline TIFF
// specs, it's a bad idea to write bits per sample values that aren't
// divisible by 8
if (index == bytes.length) {
//throw new FormatException("bad index : i = " + i + ", j = " + j);
index--;
}
short b = bytes[index];
index++;
int offset = (bitsPerSample[0] * (samples.length*j + channelNum)) % 8;
if (offset <= (8 - (bitsPerSample[0] % 8))) {
index--;
}
if (channelNum == 0) counter++;
if (counter % 4 == 0 && channelNum == 0) {
index++;
}
int ndx = startIndex + j;
if (ndx >= samples[channelNum].length) {
ndx = samples[channelNum].length - 1;
}
samples[channelNum][ndx] = (short) (b < 0 ? 256 + b : b);
if (photoInterp == WHITE_IS_ZERO || photoInterp == CMYK) {
samples[channelNum][ndx] =
(short) (Integer.MAX_VALUE - samples[channelNum][ndx]);
}
}
else if (numBytes == 1) {
float b = bytes[index];
index++;
int ndx = startIndex + j;
if (ndx < samples[channelNum].length) {
samples[channelNum][ndx] = (short) (b < 0 ? 256 + b : b);
if (photoInterp == WHITE_IS_ZERO) { // invert color value
samples[channelNum][ndx] =
(short) ((65535 - samples[channelNum][ndx]) & 0xffff);
}
else if (photoInterp == CMYK) {
samples[channelNum][ndx] =
(short) (Integer.MAX_VALUE - samples[channelNum][ndx]);
}
}
}
else {
byte[] b = new byte[numBytes];
if (numBytes + index < bytes.length) {
System.arraycopy(bytes, index, b, 0, numBytes);
}
else {
System.arraycopy(bytes, bytes.length - numBytes, b, 0, numBytes);
}
index += numBytes;
int ndx = startIndex + j;
if (ndx >= samples[0].length) ndx = samples[0].length - 1;
samples[channelNum][ndx] =
(short) DataTools.bytesToLong(b, !littleEndian);
if (photoInterp == WHITE_IS_ZERO) { // invert color value
long max = 1;
for (int q=0; q<numBytes; q++) max *= 8;
samples[channelNum][ndx] = (short) (max - samples[channelNum][ndx]);
}
else if (photoInterp == CMYK) {
samples[channelNum][ndx] =
(short) (Integer.MAX_VALUE - samples[channelNum][ndx]);
}
}
}
}
/**
* Extracts pixel information from the given byte array according to the
* bits per sample, photometric interpretation and color map IFD directory
* entry values, and the specified byte ordering.
* No error checking is performed.
*/
public static void unpackBytes(short[][] samples, int startIndex,
byte[] bytes, int[] bitsPerSample, int photoInterp, int[] colorMap,
boolean littleEndian, long maxValue, int planar, int strip, int numStrips,
long imageWidth) throws FormatException
{
if (planar == 2) {
planarUnpack(samples, startIndex, bytes, bitsPerSample, photoInterp,
littleEndian, strip, numStrips);
return;
}
int totalBits = 0;
for (int i=0; i<bitsPerSample.length; i++) totalBits += bitsPerSample[i];
int sampleCount = 8 * bytes.length / totalBits;
if (DEBUG) {
debug("unpacking " + sampleCount + " samples (startIndex=" + startIndex +
"; totalBits=" + totalBits + "; numBytes=" + bytes.length + ")");
}
if (startIndex + sampleCount > samples[0].length) {
int trunc = startIndex + sampleCount - samples[0].length;
if (DEBUG) debug("WARNING: truncated " + trunc + " extra samples");
sampleCount -= trunc;
}
// rules on incrementing the index:
// 1) if the planar configuration is set to 1 (interleaved), then add one
// to the index
// 2) if the planar configuration is set to 2 (separated), then go to
// j + (i*(bytes.length / sampleCount))
int bps0 = bitsPerSample[0];
int bpsPow = (int) Math.pow(2, bps0);
int numBytes = bps0 / 8;
boolean noDiv8 = bps0 % 8 != 0;
boolean bps8 = bps0 == 8;
boolean bps16 = bps0 == 16;
if (photoInterp == CFA_ARRAY) {
imageWidth = colorMap[colorMap.length - 2];
}
int row = 0, col = 0;
if (imageWidth != 0) row = startIndex / (int) imageWidth;
int cw = 0;
int ch = 0;
if (photoInterp == CFA_ARRAY) {
byte[] c = new byte[2];
c[0] = (byte) colorMap[0];
c[1] = (byte) colorMap[1];
cw = DataTools.bytesToInt(c, littleEndian);
c[0] = (byte) colorMap[2];
c[1] = (byte) colorMap[3];
ch = DataTools.bytesToInt(c, littleEndian);
int[] tmp = colorMap;
colorMap = new int[tmp.length - 6];
System.arraycopy(tmp, 4, colorMap, 0, colorMap.length);
}
int index = 0;
int count = 0;
BitBuffer bb = new BitBuffer(bytes);
byte[] copyByteArray = new byte[numBytes];
ByteBuffer nioBytes = MappedByteBuffer.wrap(bytes);
if (!littleEndian)
nioBytes.order(ByteOrder.LITTLE_ENDIAN);
for (int j=0; j<sampleCount; j++) {
for (int i=0; i<samples.length; i++) {
if (noDiv8) {
// bits per sample is not a multiple of 8
int ndx = startIndex + j;
short s = 0;
if ((i == 0 && (photoInterp == CFA_ARRAY ||
photoInterp == RGB_PALETTE) || (photoInterp != CFA_ARRAY &&
photoInterp != RGB_PALETTE)))
{
s = (short) bb.getBits(bps0);
if ((ndx % imageWidth) == imageWidth - 1) {
bb.skipBits((imageWidth * bps0 * sampleCount) % 8);
}
}
short b = s;
if (photoInterp != CFA_ARRAY) samples[i][ndx] = s;
if (photoInterp == WHITE_IS_ZERO || photoInterp == CMYK) {
samples[i][ndx] =
(short) (Integer.MAX_VALUE - samples[i][ndx]); // invert colors
}
else if (photoInterp == CFA_ARRAY) {
if (i == 0) {
int pixelIndex = (int) ((row + (count / cw))*imageWidth + col +
(count % cw));
samples[colorMap[count]][pixelIndex] = s;
count++;
if (count == colorMap.length) {
count = 0;
col += cw*ch;
if (col == imageWidth) col = cw;
else if (col > imageWidth) {
row += ch;
col = 0;
}
}
}
}
}
else if (bps8) {
// special case handles 8-bit data more quickly
//if (planar == 2) { index = j+(i*(bytes.length / samples.length)); }
short b = (short) (bytes[index] & 0xff);
index++;
int ndx = startIndex + j;
samples[i][ndx] = (short) (b < 0 ? Integer.MAX_VALUE + b : b);
if (photoInterp == WHITE_IS_ZERO) { // invert color value
samples[i][ndx] = (short) ((65535 - samples[i][ndx]) & 0xffff);
}
else if (photoInterp == CMYK) {
samples[i][ndx] = (short) (Integer.MAX_VALUE - samples[i][ndx]);
}
else if (photoInterp == Y_CB_CR) {
if (i == bitsPerSample.length - 1) {
int lumaRed = colorMap[0];
int lumaGreen = colorMap[1];
int lumaBlue = colorMap[2];
int red = (int)
(samples[2][ndx]*(2 - 2*lumaRed) + samples[0][ndx]);
int blue = (int)
(samples[1][ndx]*(2 - 2*lumaBlue) + samples[0][ndx]);
int green = (int)
(samples[0][ndx] - lumaBlue*blue - lumaRed*red);
if (lumaGreen != 0) green = green / lumaGreen;
samples[0][ndx] = (short) (red - colorMap[4]);
samples[1][ndx] = (short) (green - colorMap[6]);
samples[2][ndx] = (short) (blue - colorMap[8]);
}
}
} // End if (bps8)
else if (bps16) {
int ndx = startIndex + j;
if (numBytes + index < bytes.length) {
samples[i][ndx] = nioBytes.getShort(index);
} else {
samples[i][ndx] = nioBytes.getShort(bytes.length - numBytes);
}
index += numBytes;
if (photoInterp == WHITE_IS_ZERO) { // invert color value
long max = 1;
for (int q=0; q<numBytes; q++) max *= 8;
samples[i][ndx] = (short) (max - samples[i][ndx]);
}
else if (photoInterp == CMYK) {
samples[i][ndx] = (short) (Integer.MAX_VALUE - samples[i][ndx]);
}
} // End if (bps16)
else {
if (numBytes + index < bytes.length) {
System.arraycopy(bytes, index, copyByteArray, 0, numBytes);
}
else {
System.arraycopy(bytes, bytes.length - numBytes, copyByteArray,
0, numBytes);
}
index += numBytes;
int ndx = startIndex + j;
samples[i][ndx] =
(short) DataTools.bytesToLong(copyByteArray, !littleEndian);
if (photoInterp == WHITE_IS_ZERO) { // invert color value
long max = 1;
for (int q=0; q<numBytes; q++) max *= 8;
samples[i][ndx] = (short) (max - samples[i][ndx]);
}
else if (photoInterp == CMYK) {
samples[i][ndx] = (short) (Integer.MAX_VALUE - samples[i][ndx]);
}
} // end else
}
}
}
// -- Decompression methods --
/** Decodes a strip of data compressed with the given compression scheme. */
public static byte[] uncompress(byte[] input, int compression)
throws FormatException, IOException
{
if (compression < 0) compression += 65536;
if (compression == UNCOMPRESSED) return input;
else if (compression == CCITT_1D) {
throw new FormatException(
"Sorry, CCITT Group 3 1-Dimensional Modified Huffman " +
"run length encoding compression mode is not supported");
}
else if (compression == GROUP_3_FAX) {
throw new FormatException("Sorry, CCITT T.4 bi-level encoding " +
"(Group 3 Fax) compression mode is not supported");
}
else if (compression == GROUP_4_FAX) {
throw new FormatException("Sorry, CCITT T.6 bi-level encoding " +
"(Group 4 Fax) compression mode is not supported");
}
else if (compression == LZW) {
return new LZWCodec().decompress(input);
}
else if (compression == JPEG) {
throw new FormatException(
"Sorry, JPEG compression mode is not supported");
}
else if (compression == PACK_BITS) {
return new PackbitsCodec().decompress(input);
}
else if (compression == PROPRIETARY_DEFLATE || compression == DEFLATE) {
return new AdobeDeflateCodec().decompress(input);
}
else if (compression == THUNDERSCAN) {
throw new FormatException("Sorry, " +
"Thunderscan compression mode is not supported");
}
else if (compression == NIKON) {
//return new NikonCodec().decompress(input);
throw new FormatException("Sorry, Nikon compression mode is not " +
"supported; we hope to support it in the future");
}
else if (compression == LURAWAVE) {
return new LuraWaveCodec().decompress(input);
}
else {
throw new FormatException(
"Unknown Compression type (" + compression + ")");
}
}
/** Undoes in-place differencing according to the given predictor value. */
public static void undifference(byte[] input, int[] bitsPerSample,
long width, int planarConfig, int predictor) throws FormatException
{
if (predictor == 2) {
if (DEBUG) debug("reversing horizontal differencing");
int len = bitsPerSample.length;
if (bitsPerSample[len - 1] == 0) len = 1;
for (int b=0; b<input.length; b++) {
if (b / len % width == 0) continue;
input[b] += input[b - len];
}
}
else if (predictor != 1) {
throw new FormatException("Unknown Predictor (" + predictor + ")");
}
}
// --------------------------- Writing TIFF files ---------------------------
// -- IFD population methods --
/** Adds a directory entry to an IFD. */
public static void putIFDValue(Hashtable ifd, int tag, Object value) {
ifd.put(new Integer(tag), value);
}
/** Adds a directory entry of type BYTE to an IFD. */
public static void putIFDValue(Hashtable ifd, int tag, short value) {
putIFDValue(ifd, tag, new Short(value));
}
/** Adds a directory entry of type SHORT to an IFD. */
public static void putIFDValue(Hashtable ifd, int tag, int value) {
putIFDValue(ifd, tag, new Integer(value));
}
/** Adds a directory entry of type LONG to an IFD. */
public static void putIFDValue(Hashtable ifd, int tag, long value) {
putIFDValue(ifd, tag, new Long(value));
}
// -- IFD writing methods --
/**
* Writes the given IFD value to the given output object.
* @param ifdOut output object for writing IFD stream
* @param extraBuf buffer to which "extra" IFD information should be written
* @param extraOut data output wrapper for extraBuf (passed for efficiency)
* @param offset global offset to use for IFD offset values
* @param tag IFD tag to write
* @param value IFD value to write
*/
public static void writeIFDValue(DataOutput ifdOut,
ByteArrayOutputStream extraBuf, DataOutputStream extraOut, int offset,
int tag, Object value) throws FormatException, IOException
{
// convert singleton objects into arrays, for simplicity
if (value instanceof Short) {
value = new short[] {((Short) value).shortValue()};
}
else if (value instanceof Integer) {
value = new int[] {((Integer) value).intValue()};
}
else if (value instanceof Long) {
value = new long[] {((Long) value).longValue()};
}
else if (value instanceof TiffRational) {
value = new TiffRational[] {(TiffRational) value};
}
else if (value instanceof Float) {
value = new float[] {((Float) value).floatValue()};
}
else if (value instanceof Double) {
value = new double[] {((Double) value).doubleValue()};
}
// write directory entry to output buffers
ifdOut.writeShort(tag); // tag
if (value instanceof short[]) { // BYTE
short[] q = (short[]) value;
ifdOut.writeShort(BYTE); // type
ifdOut.writeInt(q.length); // count
if (q.length <= 4) {
for (int i=0; i<q.length; i++) ifdOut.writeByte(q[i]); // value(s)
for (int i=q.length; i<4; i++) ifdOut.writeByte(0); // padding
}
else {
ifdOut.writeInt(offset + extraBuf.size()); // offset
for (int i=0; i<q.length; i++) extraOut.writeByte(q[i]); // values
}
}
else if (value instanceof String) { // ASCII
char[] q = ((String) value).toCharArray();
ifdOut.writeShort(ASCII); // type
ifdOut.writeInt(q.length + 1); // count
if (q.length < 4) {
for (int i=0; i<q.length; i++) ifdOut.writeByte(q[i]); // value(s)
for (int i=q.length; i<4; i++) ifdOut.writeByte(0); // padding
}
else {
ifdOut.writeInt(offset + extraBuf.size()); // offset
for (int i=0; i<q.length; i++) extraOut.writeByte(q[i]); // values
extraOut.writeByte(0); // concluding NULL byte
}
}
else if (value instanceof int[]) { // SHORT
int[] q = (int[]) value;
ifdOut.writeShort(SHORT); // type
ifdOut.writeInt(q.length); // count
if (q.length <= 2) {
for (int i=0; i<q.length; i++) ifdOut.writeShort(q[i]); // value(s)
for (int i=q.length; i<2; i++) ifdOut.writeShort(0); // padding
}
else {
ifdOut.writeInt(offset + extraBuf.size()); // offset
for (int i=0; i<q.length; i++) extraOut.writeShort(q[i]); // values
}
}
else if (value instanceof long[]) { // LONG
long[] q = (long[]) value;
ifdOut.writeShort(LONG); // type
ifdOut.writeInt(q.length); // count
if (q.length <= 1) {
if (q.length == 1) ifdOut.writeInt((int) q[0]); // value
else ifdOut.writeInt(0); // padding
}
else {
ifdOut.writeInt(offset + extraBuf.size()); // offset
for (int i=0; i<q.length; i++) {
extraOut.writeInt((int) q[i]); // values
}
}
}
else if (value instanceof TiffRational[]) { // RATIONAL
TiffRational[] q = (TiffRational[]) value;
ifdOut.writeShort(RATIONAL); // type
ifdOut.writeInt(q.length); // count
ifdOut.writeInt(offset + extraBuf.size()); // offset
for (int i=0; i<q.length; i++) {
extraOut.writeInt((int) q[i].getNumerator()); // values
extraOut.writeInt((int) q[i].getDenominator()); // values
}
}
else if (value instanceof float[]) { // FLOAT
float[] q = (float[]) value;
ifdOut.writeShort(FLOAT); // type
ifdOut.writeInt(q.length); // count
if (q.length <= 1) {
if (q.length == 1) ifdOut.writeFloat(q[0]); // value
else ifdOut.writeInt(0); // padding
}
else {
ifdOut.writeInt(offset + extraBuf.size()); // offset
for (int i=0; i<q.length; i++) extraOut.writeFloat(q[i]); // values
}
}
else if (value instanceof double[]) { // DOUBLE
double[] q = (double[]) value;
ifdOut.writeShort(DOUBLE); // type
ifdOut.writeInt(q.length); // count
ifdOut.writeInt(offset + extraBuf.size()); // offset
for (int i=0; i<q.length; i++) extraOut.writeDouble(q[i]); // values
}
else {
throw new FormatException("Unknown IFD value type (" +
value.getClass().getName() + "): " + value);
}
}
/**
* Surgically overwrites an existing IFD value with the given one. This
* method requires that the IFD directory entry already exist. It
* intelligently updates the count field of the entry to match the new
* length. If the new length is longer than the old length, it appends the
* new data to the end of the file and updates the offset field; if not, or
* if the old data is already at the end of the file, it overwrites the old
* data in place.
*/
public static void overwriteIFDValue(RandomAccessFile raf,
int ifd, int tag, Object value) throws FormatException, IOException
{
if (DEBUG) {
debug("overwriteIFDValue (ifd=" + ifd + "; tag=" + tag + "; value=" +
value + ")");
}
byte[] header = new byte[4];
raf.seek(0);
raf.readFully(header);
if (!isValidHeader(header)) {
throw new FormatException("Invalid TIFF header");
}
boolean little = header[0] == LITTLE && header[1] == LITTLE; // II
long offset = 4; // offset to the IFD
int num = 0; // number of directory entries
// skip to the correct IFD
for (int i=0; i<=ifd; i++) {
offset = DataTools.read4UnsignedBytes(raf, little);
if (offset <= 0) {
throw new FormatException("No such IFD (" + ifd + " of " + i + ")");
}
raf.seek(offset);
num = DataTools.read2UnsignedBytes(raf, little);
if (i < ifd) raf.seek(offset + 2 + BYTES_PER_ENTRY * num);
}
// search directory entries for proper tag
for (int i=0; i<num; i++) {
int oldTag = DataTools.read2UnsignedBytes(raf, little);
int oldType = DataTools.read2UnsignedBytes(raf, little);
int oldCount = DataTools.read4SignedBytes(raf, little);
int oldOffset = DataTools.read4SignedBytes(raf, little);
if (oldTag == tag) {
// write new value to buffers
ByteArrayOutputStream ifdBuf = new ByteArrayOutputStream(14);
DataOutputStream ifdOut = new DataOutputStream(ifdBuf);
ByteArrayOutputStream extraBuf = new ByteArrayOutputStream();
DataOutputStream extraOut = new DataOutputStream(extraBuf);
writeIFDValue(ifdOut, extraBuf, extraOut, oldOffset, tag, value);
byte[] bytes = ifdBuf.toByteArray();
byte[] extra = extraBuf.toByteArray();
// extract new directory entry parameters
int newTag = DataTools.bytesToInt(bytes, 0, 2, false);
int newType = DataTools.bytesToInt(bytes, 2, 2, false);
int newCount = DataTools.bytesToInt(bytes, 4, false);
int newOffset = DataTools.bytesToInt(bytes, 8, false);
boolean terminate = false;
if (DEBUG) {
debug("overwriteIFDValue:\n\told: (tag=" + oldTag + "; type=" +
oldType + "; count=" + oldCount + "; offset=" + oldOffset +
");\n\tnew: (tag=" + newTag + "; type=" + newType + "; count=" +
newCount + "; offset=" + newOffset + ")");
}
// determine the best way to overwrite the old entry
if (extra.length == 0) {
// new entry is inline; if old entry wasn't, old data is orphaned
// do not override new offset value since data is inline
if (DEBUG) debug("overwriteIFDValue: new entry is inline");
}
else if (oldOffset +
oldCount * BYTES_PER_ELEMENT[oldType] == raf.length())
{
// old entry was already at EOF; overwrite it
newOffset = oldOffset;
terminate = true;
if (DEBUG) debug("overwriteIFDValue: old entry is at EOF");
}
else if (newCount <= oldCount) {
// new entry is as small or smaller than old entry; overwrite it
newOffset = oldOffset;
if (DEBUG) debug("overwriteIFDValue: new entry is <= old entry");
}
else {
// old entry was elsewhere; append to EOF, orphaning old entry
newOffset = (int) raf.length();
if (DEBUG) debug("overwriteIFDValue: old entry will be orphaned");
}
// overwrite old entry
raf.seek(raf.getFilePointer() - 10); // jump back
DataTools.writeShort(raf, newType, little);
DataTools.writeInt(raf, newCount, little);
DataTools.writeInt(raf, newOffset, little);
if (extra.length > 0) {
raf.seek(newOffset);
raf.write(extra);
}
if (terminate) raf.setLength(raf.getFilePointer());
return;
}
}
throw new FormatException("Tag not found (" + getIFDTagName(tag) + ")");
}
/** Convenience method for overwriting a file's first ImageDescription. */
public static void overwriteComment(String id, Object value)
throws FormatException, IOException
{
RandomAccessFile raf = new RandomAccessFile(id, "rw");
overwriteIFDValue(raf, 0, TiffTools.IMAGE_DESCRIPTION, value);
raf.close();
}
// -- Image writing methods --
/**
* Writes the given field to the specified output stream using the given
* byte offset and IFD, in big-endian format.
*
* @param img The field to write
* @param ifd Hashtable representing the TIFF IFD; can be null
* @param out The output stream to which the TIFF data should be written
* @param offset The value to use for specifying byte offsets
* @param last Whether this image is the final IFD entry of the TIFF data
* @return total number of bytes written
*/
public static long writeImage(BufferedImage img, Hashtable ifd,
OutputStream out, int offset, boolean last)
throws FormatException, IOException
{
if (img == null) throw new FormatException("Image is null");
if (DEBUG) debug("writeImage (offset=" + offset + "; last=" + last + ")");
byte[][] values = ImageTools.getPixelBytes(img, false);
int width = img.getWidth();
int height = img.getHeight();
if (values.length < 1 || values.length > 3) {
throw new FormatException("Image has an unsupported " +
"number of range components (" + values.length + ")");
}
if (values.length == 2) {
// pad values with extra set of zeroes
values = new byte[][] {
values[0], values[1], new byte[values[0].length]
};
}
int bytesPerPixel = values[0].length / (width * height);
// populate required IFD directory entries (except strip information)
if (ifd == null) ifd = new Hashtable();
putIFDValue(ifd, IMAGE_WIDTH, width);
putIFDValue(ifd, IMAGE_LENGTH, height);
if (getIFDValue(ifd, BITS_PER_SAMPLE) == null) {
int bps = 8 * bytesPerPixel;
int[] bpsArray = new int[values.length];
Arrays.fill(bpsArray, bps);
putIFDValue(ifd, BITS_PER_SAMPLE, bpsArray);
}
if (img.getRaster().getTransferType() == DataBuffer.TYPE_FLOAT) {
putIFDValue(ifd, SAMPLE_FORMAT, 3);
}
if (getIFDValue(ifd, COMPRESSION) == null) {
putIFDValue(ifd, COMPRESSION, UNCOMPRESSED);
}
if (getIFDValue(ifd, PHOTOMETRIC_INTERPRETATION) == null) {
putIFDValue(ifd, PHOTOMETRIC_INTERPRETATION, values.length == 1 ? 1 : 2);
}
if (getIFDValue(ifd, SAMPLES_PER_PIXEL) == null) {
putIFDValue(ifd, SAMPLES_PER_PIXEL, values.length);
}
if (getIFDValue(ifd, X_RESOLUTION) == null) {
putIFDValue(ifd, X_RESOLUTION, new TiffRational(1, 1)); // no unit
}
if (getIFDValue(ifd, Y_RESOLUTION) == null) {
putIFDValue(ifd, Y_RESOLUTION, new TiffRational(1, 1)); // no unit
}
if (getIFDValue(ifd, RESOLUTION_UNIT) == null) {
putIFDValue(ifd, RESOLUTION_UNIT, 1); // no unit
}
if (getIFDValue(ifd, SOFTWARE) == null) {
putIFDValue(ifd, SOFTWARE, "LOCI Bio-Formats");
}
if (getIFDValue(ifd, IMAGE_DESCRIPTION) == null) {
putIFDValue(ifd, IMAGE_DESCRIPTION, "");
}
// create pixel output buffers
int stripSize = 8192;
int rowsPerStrip = stripSize / (width * bytesPerPixel);
int stripsPerImage = (height + rowsPerStrip - 1) / rowsPerStrip;
int[] bps = (int[]) getIFDValue(ifd, BITS_PER_SAMPLE, true, int[].class);
ByteArrayOutputStream[] stripBuf =
new ByteArrayOutputStream[stripsPerImage];
DataOutputStream[] stripOut = new DataOutputStream[stripsPerImage];
for (int i=0; i<stripsPerImage; i++) {
stripBuf[i] = new ByteArrayOutputStream(stripSize);
stripOut[i] = new DataOutputStream(stripBuf[i]);
}
// write pixel strips to output buffers
for (int y=0; y<height; y++) {
int strip = y / rowsPerStrip;
for (int x=0; x<width; x++) {
int ndx = y * width * bytesPerPixel + x * bytesPerPixel;
for (int c=0; c<values.length; c++) {
int q = values[c][ndx];
if (bps[c] == 8) stripOut[strip].writeByte(q);
else if (bps[c] == 16) {
stripOut[strip].writeByte(q);
stripOut[strip].writeByte(values[c][ndx+1]);
}
else if (bps[c] == 32) {
for (int i=0; i<4; i++) {
stripOut[strip].writeByte(values[c][ndx + i]);
}
}
else {
throw new FormatException("Unsupported bits per sample value (" +
bps[c] + ")");
}
}
}
}
// compress strips according to given differencing and compression schemes
int planarConfig = getIFDIntValue(ifd, PLANAR_CONFIGURATION, false, 1);
int predictor = getIFDIntValue(ifd, PREDICTOR, false, 1);
int compression = getIFDIntValue(ifd, COMPRESSION, false, UNCOMPRESSED);
byte[][] strips = new byte[stripsPerImage][];
for (int i=0; i<stripsPerImage; i++) {
strips[i] = stripBuf[i].toByteArray();
difference(strips[i], bps, width, planarConfig, predictor);
strips[i] = compress(strips[i], compression);
}
// record strip byte counts and offsets
long[] stripByteCounts = new long[stripsPerImage];
long[] stripOffsets = new long[stripsPerImage];
putIFDValue(ifd, STRIP_OFFSETS, stripOffsets);
putIFDValue(ifd, ROWS_PER_STRIP, rowsPerStrip);
putIFDValue(ifd, STRIP_BYTE_COUNTS, stripByteCounts);
Object[] keys = ifd.keySet().toArray();
Arrays.sort(keys); // sort IFD tags in ascending order
int ifdBytes = 2 + BYTES_PER_ENTRY * keys.length + 4;
long pixelBytes = 0;
for (int i=0; i<stripsPerImage; i++) {
stripByteCounts[i] = strips[i].length;
stripOffsets[i] = pixelBytes + offset + ifdBytes;
pixelBytes += stripByteCounts[i];
}
// create IFD output buffers
ByteArrayOutputStream ifdBuf = new ByteArrayOutputStream(ifdBytes);
DataOutputStream ifdOut = new DataOutputStream(ifdBuf);
ByteArrayOutputStream extraBuf = new ByteArrayOutputStream();
DataOutputStream extraOut = new DataOutputStream(extraBuf);
offset += ifdBytes + pixelBytes;
// write IFD to output buffers
ifdOut.writeShort(keys.length); // number of directory entries
for (int k=0; k<keys.length; k++) {
Object key = keys[k];
if (!(key instanceof Integer)) {
throw new FormatException("Malformed IFD tag (" + key + ")");
}
if (((Integer) key).intValue() == LITTLE_ENDIAN) continue;
Object value = ifd.get(key);
if (DEBUG) {
String sk = getIFDTagName(((Integer) key).intValue());
String sv = value instanceof int[] ?
("int[" + ((int[]) value).length + "]") : value.toString();
debug("writeImage: writing " + sk + " (value=" + sv + ")");
}
writeIFDValue(ifdOut, extraBuf, extraOut, offset,
((Integer) key).intValue(), value);
}
ifdOut.writeInt(last ? 0 : offset + extraBuf.size()); // offset to next IFD
// flush buffers to output stream
byte[] ifdArray = ifdBuf.toByteArray();
byte[] extraArray = extraBuf.toByteArray();
long numBytes = ifdArray.length + extraArray.length;
out.write(ifdArray);
for (int i=0; i<strips.length; i++) {
out.write(strips[i]);
numBytes += strips[i].length;
}
out.write(extraArray);
return numBytes;
}
/**
* Retrieves the image's width (TIFF tag ImageWidth) from a given TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the image's width.
* @throws FormatException if there is a problem parsing the IFD metadata.
*/
public static long getImageWidth(Hashtable ifd) throws FormatException {
return getIFDLongValue(ifd, IMAGE_WIDTH, true, 0);
}
/**
* Retrieves the image's length (TIFF tag ImageLength) from a given TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the image's length.
* @throws FormatException if there is a problem parsing the IFD metadata.
*/
public static long getImageLength(Hashtable ifd) throws FormatException {
return getIFDLongValue(ifd, IMAGE_LENGTH, true, 0);
}
/**
* Retrieves the image's bits per sample (TIFF tag BitsPerSample) from a given
* TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the image's bits per sample. The length of the array is equal to
* the number of samples per pixel.
* @throws FormatException if there is a problem parsing the IFD metadata.
* @see #getSamplesPerPixel(Hashtable)
*/
public static int[] getBitsPerSample(Hashtable ifd) throws FormatException {
int[] bitsPerSample = getIFDIntArray(ifd, BITS_PER_SAMPLE, false);
if (bitsPerSample == null) bitsPerSample = new int[] {1};
return bitsPerSample;
}
/**
* Retrieves the number of samples per pixel for the image (TIFF tag
* SamplesPerPixel) from a given TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the number of samples per pixel.
* @throws FormatException if there is a problem parsing the IFD metadata.
*/
public static int getSamplesPerPixel(Hashtable ifd) throws FormatException {
return getIFDIntValue(ifd, SAMPLES_PER_PIXEL, false, 1);
}
/**
* Retrieves the image's compression type (TIFF tag Compression) from a
* given TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the image's compression type. As of TIFF 6.0 this is one of:
* <ul>
* <li>Uncompressed (1)</li>
* <li>CCITT 1D (2)</li>
* <li>Group 3 Fax (3)</li>
* <li>Group 4 Fax (4)</li>
* <li>LZW (5)</li>
* <li>JPEG (6)</li>
* <li>PackBits (32773)</li>
* </ul>
* @throws FormatException if there is a problem parsing the IFD metadata.
*/
public static int getCompression(Hashtable ifd) throws FormatException {
return getIFDIntValue(ifd, COMPRESSION, false, UNCOMPRESSED);
}
/**
* Retrieves the image's photometric interpretation (TIFF tag
* PhotometricInterpretation) from a given TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the image's photometric interpretation. As of TIFF 6.0 this is one
* of:
* <ul>
* <li>WhiteIsZero (0)</li>
* <li>BlackIsZero (1)</li>
* <li>RGB (2)</li>
* <li>RGB Palette (3)</li>
* <li>Transparency mask (4)</li>
* <li>CMYK (5)</li>
* <li>YbCbCr (6)</li>
* <li>CIELab (8)</li>
* </ul>
*
* @throws FormatException if there is a problem parsing the IFD metadata.
*/
public static int getPhotometricInterpretation(Hashtable ifd)
throws FormatException
{
return getIFDIntValue(ifd, PHOTOMETRIC_INTERPRETATION, true, 0);
}
/**
* Retrieves the strip offsets for the image (TIFF tag StripOffsets) from a
* given TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the strip offsets for the image. The lenght of the array is equal
* to the number of strips per image. <i>StripsPerImage =
* floor ((ImageLength + RowsPerStrip - 1) / RowsPerStrip)</i>.
* @throws FormatException if there is a problem parsing the IFD metadata.
* @see #getStripByteCounts(Hashtable)
* @see #getRowsPerStrip(Hashtable)
*/
public static long[] getStripOffsets(Hashtable ifd) throws FormatException {
return getIFDLongArray(ifd, STRIP_OFFSETS, false);
}
/**
* Retrieves strip byte counts for the image (TIFF tag StripByteCounts) from a
* given TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the byte counts for each strip. The length of the array is equal to
* the number of strips per image. <i>StripsPerImage =
* floor((ImageLength + RowsPerStrip - 1) / RowsPerStrip)</i>.
* @throws FormatException if there is a problem parsing the IFD metadata.
* @see #getStripOffsets(Hashtable)
*/
public static long[] getStripByteCounts(Hashtable ifd) throws FormatException
{
return getIFDLongArray(ifd, STRIP_BYTE_COUNTS, false);
}
/**
* Retrieves the number of rows per strip for image (TIFF tag RowsPerStrip)
* from a given TIFF IFD.
* @param ifd a TIFF IFD hashtable.
* @return the number of rows per strip.
* @throws FormatException if there is a problem parsing the IFD metadata.
*/
public static long[] getRowsPerStrip(Hashtable ifd) throws FormatException {
return getIFDLongArray(ifd, ROWS_PER_STRIP, false);
}
// -- Compression methods --
/** Encodes a strip of data with the given compression scheme. */
public static byte[] compress(byte[] input, int compression)
throws FormatException, IOException
{
if (compression == UNCOMPRESSED) return input;
else if (compression == CCITT_1D) {
throw new FormatException(
"Sorry, CCITT Group 3 1-Dimensional Modified Huffman " +
"run length encoding compression mode is not supported");
}
else if (compression == GROUP_3_FAX) {
throw new FormatException("Sorry, CCITT T.4 bi-level encoding " +
"(Group 3 Fax) compression mode is not supported");
}
else if (compression == GROUP_4_FAX) {
throw new FormatException("Sorry, CCITT T.6 bi-level encoding " +
"(Group 4 Fax) compression mode is not supported");
}
else if (compression == LZW) {
LZWCodec c = new LZWCodec();
return c.compress(input, 0, 0, null, null);
// return Compression.lzwCompress(input);
}
else if (compression == JPEG) {
throw new FormatException(
"Sorry, JPEG compression mode is not supported");
}
else if (compression == PACK_BITS) {
throw new FormatException(
"Sorry, PackBits compression mode is not supported");
}
else {
throw new FormatException(
"Unknown Compression type (" + compression + ")");
}
}
/** Performs in-place differencing according to the given predictor value. */
public static void difference(byte[] input, int[] bitsPerSample,
long width, int planarConfig, int predictor) throws FormatException
{
if (predictor == 2) {
if (DEBUG) debug("performing horizontal differencing");
for (int b=input.length-1; b>=0; b--) {
if (b / bitsPerSample.length % width == 0) continue;
input[b] -= input[b - bitsPerSample.length];
}
}
else if (predictor != 1) {
throw new FormatException("Unknown Predictor (" + predictor + ")");
}
}
// -- Debugging --
/** Prints a debugging message with current time. */
public static void debug(String message) {
LogTools.println(System.currentTimeMillis() + ": " + message);
}
}