org.apache.xmlgraphics.image.codec.tiff.TIFFDirectory Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id: TIFFDirectory.java 1732018 2016-02-24 04:51:06Z gadams $ */
package org.apache.xmlgraphics.image.codec.tiff;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.xmlgraphics.image.codec.util.PropertyUtil;
import org.apache.xmlgraphics.image.codec.util.SeekableStream;
// CSOFF: ConstantName
// CSOFF: EmptyStatement
// CSOFF: InnerAssignment
// CSOFF: LocalVariableName
// CSOFF: MemberName
// CSOFF: MultipleVariableDeclarations
// CSOFF: NeedBraces
// CSOFF: ParameterName
// CSOFF: WhitespaceAround
/**
* A class representing an Image File Directory (IFD) from a TIFF 6.0
* stream. The TIFF file format is described in more detail in the
* comments for the TIFFDescriptor class.
*
* A TIFF IFD consists of a set of TIFFField tags. Methods are
* provided to query the set of tags and to obtain the raw field
* array. In addition, convenience methods are provided for acquiring
* the values of tags that contain a single value that fits into a
* byte, int, long, float, or double.
*
*
Every TIFF file is made up of one or more public IFDs that are
* joined in a linked list, rooted in the file header. A file may
* also contain so-called private IFDs that are referenced from
* tag data and do not appear in the main list.
*
*
This class is not a committed part of the JAI API. It may
* be removed or changed in future releases of JAI.
*
* @see TIFFField
* @version $Id: TIFFDirectory.java 1732018 2016-02-24 04:51:06Z gadams $
*/
public class TIFFDirectory implements Serializable {
private static final long serialVersionUID = 2007844835460959003L;
/** A boolean storing the endianness of the stream. */
boolean isBigEndian;
/** The number of entries in the IFD. */
int numEntries;
/** An array of TIFFFields. */
TIFFField[] fields;
/** A Hashtable indexing the fields by tag number. */
Map fieldIndex = new HashMap();
/** The offset of this IFD. */
long ifdOffset = 8;
/** The offset of the next IFD. */
long nextIFDOffset;
/** The default constructor. */
TIFFDirectory() { }
private static boolean isValidEndianTag(int endian) {
return ((endian == 0x4949) || (endian == 0x4d4d));
}
/**
* Constructs a TIFFDirectory from a SeekableStream.
* The directory parameter specifies which directory to read from
* the linked list present in the stream; directory 0 is normally
* read but it is possible to store multiple images in a single
* TIFF file by maintaing multiple directories.
*
* @param stream a SeekableStream to read from.
* @param directory the index of the directory to read.
*/
public TIFFDirectory(SeekableStream stream, int directory)
throws IOException {
long globalSaveOffset = stream.getFilePointer();
long ifdOffset;
// Read the TIFF header
stream.seek(0L);
int endian = stream.readUnsignedShort();
if (!isValidEndianTag(endian)) {
throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory1"));
}
isBigEndian = (endian == 0x4d4d);
int magic = readUnsignedShort(stream);
if (magic != 42) {
throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory2"));
}
// Get the initial ifd offset as an unsigned int (using a long)
ifdOffset = readUnsignedInt(stream);
for (int i = 0; i < directory; i++) {
if (ifdOffset == 0L) {
throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory3"));
}
stream.seek(ifdOffset);
long entries = readUnsignedShort(stream);
stream.skip(12 * entries);
ifdOffset = readUnsignedInt(stream);
}
if (ifdOffset == 0L) {
throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory3"));
}
stream.seek(ifdOffset);
initialize(stream);
stream.seek(globalSaveOffset);
}
/**
* Constructs a TIFFDirectory by reading a SeekableStream.
* The ifd_offset parameter specifies the stream offset from which
* to begin reading; this mechanism is sometimes used to store
* private IFDs within a TIFF file that are not part of the normal
* sequence of IFDs.
*
* @param stream a SeekableStream to read from.
* @param ifdOffset the long byte offset of the directory.
* @param directory the index of the directory to read beyond the
* one at the current stream offset; zero indicates the IFD
* at the current offset.
*/
public TIFFDirectory(SeekableStream stream, long ifdOffset, int directory)
throws IOException {
long globalSaveOffset = stream.getFilePointer();
stream.seek(0L);
int endian = stream.readUnsignedShort();
if (!isValidEndianTag(endian)) {
throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory1"));
}
isBigEndian = (endian == 0x4d4d);
// Seek to the first IFD.
stream.seek(ifdOffset);
// Seek to desired IFD if necessary.
int dirNum = 0;
while (dirNum < directory) {
// Get the number of fields in the current IFD.
long numEntries = readUnsignedShort(stream);
// Skip to the next IFD offset value field.
stream.seek(ifdOffset + 12 * numEntries);
// Read the offset to the next IFD beyond this one.
ifdOffset = readUnsignedInt(stream);
// Seek to the next IFD.
stream.seek(ifdOffset);
// Increment the directory.
dirNum++;
}
initialize(stream);
stream.seek(globalSaveOffset);
}
private static final int[] SIZE_OF_TYPE = {
0, // 0 = n/a
1, // 1 = byte
1, // 2 = ascii
2, // 3 = short
4, // 4 = long
8, // 5 = rational
1, // 6 = sbyte
1, // 7 = undefined
2, // 8 = sshort
4, // 9 = slong
8, // 10 = srational
4, // 11 = float
8 // 12 = double
};
private void initialize(SeekableStream stream) throws IOException {
long nextTagOffset;
int i;
int j;
ifdOffset = stream.getFilePointer();
numEntries = readUnsignedShort(stream);
fields = new TIFFField[numEntries];
for (i = 0; i < numEntries; i++) {
int tag = readUnsignedShort(stream);
int type = readUnsignedShort(stream);
int count = (int)(readUnsignedInt(stream));
int value = 0;
// The place to return to to read the next tag
nextTagOffset = stream.getFilePointer() + 4;
try {
// If the tag data can't fit in 4 bytes, the next 4 bytes
// contain the starting offset of the data
if (count * SIZE_OF_TYPE[type] > 4) {
value = (int)(readUnsignedInt(stream));
stream.seek(value);
}
} catch (ArrayIndexOutOfBoundsException ae) {
// System.err.println(tag + " " + "TIFFDirectory4"); TODO - log this message
// if the data type is unknown we should skip this TIFF Field
stream.seek(nextTagOffset);
continue;
}
fieldIndex.put(tag, i);
Object obj = null;
switch (type) {
case TIFFField.TIFF_BYTE:
case TIFFField.TIFF_SBYTE:
case TIFFField.TIFF_UNDEFINED:
case TIFFField.TIFF_ASCII:
byte[] bvalues = new byte[count];
stream.readFully(bvalues, 0, count);
if (type == TIFFField.TIFF_ASCII) {
// Can be multiple strings
int index = 0;
int prevIndex = 0;
List v = new ArrayList();
while (index < count) {
while ((index < count) && (bvalues[index++] != 0)) {
// NOP
}
// When we encountered zero, means one string has ended
v.add(new String(bvalues, prevIndex,
(index - prevIndex), "UTF-8"));
prevIndex = index;
}
count = v.size();
String[] strings = new String[count];
v.toArray(strings);
obj = strings;
} else {
obj = bvalues;
}
break;
case TIFFField.TIFF_SHORT:
char[] cvalues = new char[count];
for (j = 0; j < count; j++) {
cvalues[j] = (char)(readUnsignedShort(stream));
}
obj = cvalues;
break;
case TIFFField.TIFF_LONG:
long[] lvalues = new long[count];
for (j = 0; j < count; j++) {
lvalues[j] = readUnsignedInt(stream);
}
obj = lvalues;
break;
case TIFFField.TIFF_RATIONAL:
long[][] llvalues = new long[count][2];
for (j = 0; j < count; j++) {
llvalues[j][0] = readUnsignedInt(stream);
llvalues[j][1] = readUnsignedInt(stream);
}
obj = llvalues;
break;
case TIFFField.TIFF_SSHORT:
short[] svalues = new short[count];
for (j = 0; j < count; j++) {
svalues[j] = readShort(stream);
}
obj = svalues;
break;
case TIFFField.TIFF_SLONG:
int[] ivalues = new int[count];
for (j = 0; j < count; j++) {
ivalues[j] = readInt(stream);
}
obj = ivalues;
break;
case TIFFField.TIFF_SRATIONAL:
int[][] iivalues = new int[count][2];
for (j = 0; j < count; j++) {
iivalues[j][0] = readInt(stream);
iivalues[j][1] = readInt(stream);
}
obj = iivalues;
break;
case TIFFField.TIFF_FLOAT:
float[] fvalues = new float[count];
for (j = 0; j < count; j++) {
fvalues[j] = readFloat(stream);
}
obj = fvalues;
break;
case TIFFField.TIFF_DOUBLE:
double[] dvalues = new double[count];
for (j = 0; j < count; j++) {
dvalues[j] = readDouble(stream);
}
obj = dvalues;
break;
default:
throw new RuntimeException(PropertyUtil.getString("TIFFDirectory0"));
}
fields[i] = new TIFFField(tag, type, count, obj);
stream.seek(nextTagOffset);
}
// Read the offset of the next IFD.
nextIFDOffset = readUnsignedInt(stream);
}
/** Returns the number of directory entries. */
public int getNumEntries() {
return numEntries;
}
/**
* Returns the value of a given tag as a TIFFField,
* or null if the tag is not present.
*/
public TIFFField getField(int tag) {
Integer i = (Integer)fieldIndex.get(tag);
if (i == null) {
return null;
} else {
return fields[i];
}
}
/**
* Returns true if a tag appears in the directory.
*/
public boolean isTagPresent(int tag) {
return fieldIndex.containsKey(tag);
}
/**
* Returns an ordered array of ints indicating the tag
* values.
*/
public int[] getTags() {
int[] tags = new int[fieldIndex.size()];
Iterator iter = fieldIndex.keySet().iterator();
int i = 0;
while (iter.hasNext()) {
tags[i++] = (Integer) iter.next();
}
return tags;
}
/**
* Returns an array of TIFFFields containing all the fields
* in this directory.
*/
public TIFFField[] getFields() {
return fields;
}
/**
* Returns the value of a particular index of a given tag as a
* byte. The caller is responsible for ensuring that the tag is
* present and has type TIFFField.TIFF_SBYTE, TIFF_BYTE, or
* TIFF_UNDEFINED.
*/
public byte getFieldAsByte(int tag, int index) {
Integer i = (Integer)fieldIndex.get(tag);
byte [] b = (fields[i]).getAsBytes();
return b[index];
}
/**
* Returns the value of index 0 of a given tag as a
* byte. The caller is responsible for ensuring that the tag is
* present and has type TIFFField.TIFF_SBYTE, TIFF_BYTE, or
* TIFF_UNDEFINED.
*/
public byte getFieldAsByte(int tag) {
return getFieldAsByte(tag, 0);
}
/**
* Returns the value of a particular index of a given tag as a
* long. The caller is responsible for ensuring that the tag is
* present and has type TIFF_BYTE, TIFF_SBYTE, TIFF_UNDEFINED,
* TIFF_SHORT, TIFF_SSHORT, TIFF_SLONG or TIFF_LONG.
*/
public long getFieldAsLong(int tag, int index) {
Integer i = (Integer)fieldIndex.get(tag);
return (fields[i]).getAsLong(index);
}
/**
* Returns the value of index 0 of a given tag as a
* long. The caller is responsible for ensuring that the tag is
* present and has type TIFF_BYTE, TIFF_SBYTE, TIFF_UNDEFINED,
* TIFF_SHORT, TIFF_SSHORT, TIFF_SLONG or TIFF_LONG.
*/
public long getFieldAsLong(int tag) {
return getFieldAsLong(tag, 0);
}
/**
* Returns the value of a particular index of a given tag as a
* float. The caller is responsible for ensuring that the tag is
* present and has numeric type (all but TIFF_UNDEFINED and
* TIFF_ASCII).
*/
public float getFieldAsFloat(int tag, int index) {
Integer i = (Integer)fieldIndex.get(tag);
return fields[i].getAsFloat(index);
}
/**
* Returns the value of index 0 of a given tag as a float. The
* caller is responsible for ensuring that the tag is present and
* has numeric type (all but TIFF_UNDEFINED and TIFF_ASCII).
*/
public float getFieldAsFloat(int tag) {
return getFieldAsFloat(tag, 0);
}
/**
* Returns the value of a particular index of a given tag as a
* double. The caller is responsible for ensuring that the tag is
* present and has numeric type (all but TIFF_UNDEFINED and
* TIFF_ASCII).
*/
public double getFieldAsDouble(int tag, int index) {
Integer i = (Integer)fieldIndex.get(tag);
return fields[i].getAsDouble(index);
}
/**
* Returns the value of index 0 of a given tag as a double. The
* caller is responsible for ensuring that the tag is present and
* has numeric type (all but TIFF_UNDEFINED and TIFF_ASCII).
*/
public double getFieldAsDouble(int tag) {
return getFieldAsDouble(tag, 0);
}
// Methods to read primitive data types from the stream
private short readShort(SeekableStream stream)
throws IOException {
if (isBigEndian) {
return stream.readShort();
} else {
return stream.readShortLE();
}
}
private int readUnsignedShort(SeekableStream stream)
throws IOException {
if (isBigEndian) {
return stream.readUnsignedShort();
} else {
return stream.readUnsignedShortLE();
}
}
private int readInt(SeekableStream stream)
throws IOException {
if (isBigEndian) {
return stream.readInt();
} else {
return stream.readIntLE();
}
}
private long readUnsignedInt(SeekableStream stream)
throws IOException {
if (isBigEndian) {
return stream.readUnsignedInt();
} else {
return stream.readUnsignedIntLE();
}
}
// private long readLong(SeekableStream stream)
// throws IOException {
// if (isBigEndian) {
// return stream.readLong();
// } else {
// return stream.readLongLE();
// }
// }
private float readFloat(SeekableStream stream)
throws IOException {
if (isBigEndian) {
return stream.readFloat();
} else {
return stream.readFloatLE();
}
}
private double readDouble(SeekableStream stream)
throws IOException {
if (isBigEndian) {
return stream.readDouble();
} else {
return stream.readDoubleLE();
}
}
private static int readUnsignedShort(SeekableStream stream,
boolean isBigEndian)
throws IOException {
if (isBigEndian) {
return stream.readUnsignedShort();
} else {
return stream.readUnsignedShortLE();
}
}
private static long readUnsignedInt(SeekableStream stream,
boolean isBigEndian)
throws IOException {
if (isBigEndian) {
return stream.readUnsignedInt();
} else {
return stream.readUnsignedIntLE();
}
}
// Utilities
/**
* Returns the number of image directories (subimages) stored in a
* given TIFF file, represented by a SeekableStream
.
*/
public static int getNumDirectories(SeekableStream stream)
throws IOException {
long pointer = stream.getFilePointer(); // Save stream pointer
stream.seek(0L);
int endian = stream.readUnsignedShort();
if (!isValidEndianTag(endian)) {
throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory1"));
}
boolean isBigEndian = (endian == 0x4d4d);
int magic = readUnsignedShort(stream, isBigEndian);
if (magic != 42) {
throw new IllegalArgumentException(PropertyUtil.getString("TIFFDirectory2"));
}
stream.seek(4L);
long offset = readUnsignedInt(stream, isBigEndian);
int numDirectories = 0;
while (offset != 0L) {
++numDirectories;
stream.seek(offset);
long entries = readUnsignedShort(stream, isBigEndian);
stream.skip(12 * entries);
offset = readUnsignedInt(stream, isBigEndian);
}
stream.seek(pointer); // Reset stream pointer
return numDirectories;
}
/**
* Returns a boolean indicating whether the byte order used in the
* the TIFF file is big-endian. That is, whether the byte order is from
* the most significant to the least significant.
*/
public boolean isBigEndian() {
return isBigEndian;
}
/**
* Returns the offset of the IFD corresponding to this
* TIFFDirectory
.
*/
public long getIFDOffset() {
return ifdOffset;
}
/**
* Returns the offset of the next IFD after the IFD corresponding to this
* TIFFDirectory
.
*/
public long getNextIFDOffset() {
return nextIFDOffset;
}
}