blob: ed3d399d830d84d4ed9ff2cbc5e97c8d91a725dc [file] [log] [blame]
/*
* Copyright (c) 2014-2021 by Wen Yu
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* or any later version.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
*
* Change History - most recent changes go on top of previous changes
*
* TIFFMeta.java
*
* Who Date Description
* ==== ========= =====================================================================
* WY 21Jun2019 Added code for removeMetadata to return the removed metadata as a map
* WY 14May2019 Write IPTC to normal TIFF IPTC tag instead of PhotoShop IRB block
* WY 06Jul2015 Added insertXMP(InputSream, OutputStream, XMP)
* WY 15Apr2015 Changed the argument type for insertIPTC() and insertIRB()
* WY 07Apr2015 Removed insertICCProfile() AWT related code
* WY 07Apr2015 Merge Adobe IRB IPTC and TIFF IPTC data if both exist
* WY 13Mar2015 Initial creation
*/
package pixy.meta.tiff;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import pixy.meta.Metadata;
import pixy.meta.MetadataType;
import pixy.meta.adobe.DDB;
import pixy.meta.adobe.IRB;
import pixy.meta.adobe.IRBThumbnail;
import pixy.meta.adobe.ThumbnailResource;
import pixy.meta.adobe.ImageResourceID;
import pixy.meta.adobe._8BIM;
import pixy.meta.exif.Exif;
import pixy.meta.exif.ExifTag;
import pixy.meta.exif.GPSTag;
import pixy.meta.exif.InteropTag;
import pixy.meta.icc.ICCProfile;
import pixy.meta.image.Comments;
import pixy.meta.iptc.IPTC;
import pixy.meta.iptc.IPTCDataSet;
import pixy.meta.iptc.IPTCTag;
import pixy.meta.xmp.XMP;
import pixy.image.jpeg.Marker;
import pixy.image.tiff.ASCIIField;
import pixy.image.tiff.ByteField;
import pixy.image.tiff.DoubleField;
import pixy.image.tiff.FieldType;
import pixy.image.tiff.FloatField;
import pixy.image.tiff.IFD;
import pixy.image.tiff.IFDField;
import pixy.image.tiff.LongField;
import pixy.image.tiff.MakerNoteField;
import pixy.image.tiff.RationalField;
import pixy.image.tiff.SByteField;
import pixy.image.tiff.SLongField;
import pixy.image.tiff.SRationalField;
import pixy.image.tiff.SShortField;
import pixy.image.tiff.ShortField;
import pixy.image.tiff.Tag;
import pixy.image.tiff.TiffField;
import pixy.image.tiff.TiffFieldEnum;
import pixy.image.tiff.TiffTag;
import pixy.image.tiff.UndefinedField;
import pixy.image.tiff.TIFFImage;
import pixy.io.IOUtils;
import pixy.io.RandomAccessInputStream;
import pixy.io.RandomAccessOutputStream;
import pixy.io.ReadStrategy;
import pixy.io.ReadStrategyII;
import pixy.io.ReadStrategyMM;
import pixy.io.WriteStrategyII;
import pixy.io.WriteStrategyMM;
import pixy.string.StringUtils;
import pixy.string.XMLUtils;
import pixy.util.ArrayUtils;
import android.graphics.*;
public class TIFFMeta {
// Offset where to write the value of the first IFD offset
public static final int OFFSET_TO_WRITE_FIRST_IFD_OFFSET = 0x04;
public static final int FIRST_WRITE_OFFSET = 0x08;
public static final int STREAM_HEAD = 0x00;
// Obtain a logger instance
private static final Logger LOGGER = LoggerFactory.getLogger(TIFFMeta.class);
private static int copyHeader(RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
rin.seek(STREAM_HEAD);
// First 2 bytes determine the byte order of the file, "MM" or "II"
short endian = rin.readShort();
if (endian == IOUtils.BIG_ENDIAN) {
rin.setReadStrategy(ReadStrategyMM.getInstance());
rout.setWriteStrategy(WriteStrategyMM.getInstance());
} else if(endian == IOUtils.LITTLE_ENDIAN) {
rin.setReadStrategy(ReadStrategyII.getInstance());
rout.setWriteStrategy(WriteStrategyII.getInstance());
} else {
rin.close();
rout.close();
throw new RuntimeException("Invalid TIFF byte order");
}
rout.writeShort(endian);
// Read TIFF identifier
rin.seek(0x02);
short tiff_id = rin.readShort();
if(tiff_id!=0x2a)//"*" 42 decimal
{
rin.close();
rout.close();
throw new RuntimeException("Invalid TIFF identifier");
}
rout.writeShort(tiff_id);
rin.seek(OFFSET_TO_WRITE_FIRST_IFD_OFFSET);
return rin.readInt();
}
private static Collection<IPTCDataSet> copyIPTCDataSet(Collection<IPTCDataSet> iptcs, byte[] data) throws IOException {
IPTC iptc = new IPTC(data);
// Shallow copy the map
Map<IPTCTag, List<IPTCDataSet>> dataSetMap = new HashMap<IPTCTag, List<IPTCDataSet>>(iptc.getDataSets());
for(IPTCDataSet set : iptcs)
if(!set.allowMultiple())
dataSetMap.remove(set.getTagEnum());
for(List<IPTCDataSet> iptcList : dataSetMap.values())
iptcs.addAll(iptcList);
return iptcs;
}
private static TiffField<?> copyJpegHufTable(RandomAccessInputStream rin, RandomAccessOutputStream rout, TiffField<?> field, int curPos) throws IOException
{
int[] data = field.getDataAsLong();
int[] tmp = new int[data.length];
for(int i = 0; i < data.length; i++) {
rin.seek(data[i]);
tmp[i] = curPos;
byte[] htable = new byte[16];
IOUtils.readFully(rin, htable);
IOUtils.write(rout, htable);
curPos += 16;
int numCodes = 0;
for(int j = 0; j < 16; j++) {
numCodes += htable[j]&0xff;
}
curPos += numCodes;
htable = new byte[numCodes];
IOUtils.readFully(rin, htable);
IOUtils.write(rout, htable);
}
if(TiffTag.fromShort(field.getTag()) == TiffTag.JPEG_AC_TABLES)
return new LongField(TiffTag.JPEG_AC_TABLES.getValue(), tmp);
return new LongField(TiffTag.JPEG_DC_TABLES.getValue(), tmp);
}
private static void copyJpegIFByteCount(RandomAccessInputStream rin, RandomAccessOutputStream rout, int offset, int outOffset) throws IOException {
boolean finished = false;
int length = 0;
short marker;
Marker emarker;
rin.seek(offset);
rout.seek(outOffset);
// The very first marker should be the start_of_image marker!
if(Marker.fromShort(IOUtils.readShortMM(rin)) != Marker.SOI) {
return;
}
IOUtils.writeShortMM(rout, Marker.SOI.getValue());
marker = IOUtils.readShortMM(rin);
while (!finished) {
if (Marker.fromShort(marker) == Marker.EOI) {
IOUtils.writeShortMM(rout, marker);
finished = true;
} else { // Read markers
emarker = Marker.fromShort(marker);
switch (emarker) {
case JPG: // JPG and JPGn shouldn't appear in the image.
case JPG0:
case JPG13:
case TEM: // The only stand alone mark besides SOI, EOI, and RSTn.
marker = IOUtils.readShortMM(rin);
break;
case SOS:
marker = copyJpegSOS(rin, rout);
break;
case PADDING:
int nextByte = 0;
while((nextByte = rin.read()) == 0xff) {;}
marker = (short)((0xff<<8)|nextByte);
break;
default:
length = IOUtils.readUnsignedShortMM(rin);
byte[] buf = new byte[length - 2];
rin.read(buf);
IOUtils.writeShortMM(rout, marker);
IOUtils.writeShortMM(rout, length);
rout.write(buf);
marker = IOUtils.readShortMM(rin);
}
}
}
}
private static TiffField<?> copyJpegQTable(RandomAccessInputStream rin, RandomAccessOutputStream rout, TiffField<?> field, int curPos) throws IOException
{
byte[] qtable = new byte[64];
int[] data = field.getDataAsLong();
int[] tmp = new int[data.length];
for(int i = 0; i < data.length; i++) {
rin.seek(data[i]);
tmp[i] = curPos;
IOUtils.readFully(rin, qtable);
IOUtils.write(rout, qtable);
curPos += 64;
}
return new LongField(TiffTag.JPEG_Q_TABLES.getValue(), tmp);
}
private static short copyJpegSOS(RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
int len = IOUtils.readUnsignedShortMM(rin);
byte buf[] = new byte[len - 2];
IOUtils.readFully(rin, buf);
IOUtils.writeShortMM(rout, Marker.SOS.getValue());
IOUtils.writeShortMM(rout, len);
rout.write(buf);
// Actual image data follow.
int nextByte = 0;
short marker = 0;
while((nextByte = IOUtils.read(rin)) != -1) {
rout.write(nextByte);
if(nextByte == 0xff)
{
nextByte = IOUtils.read(rin);
rout.write(nextByte);
if (nextByte == -1) {
throw new IOException("Premature end of SOS segment!");
}
if (nextByte != 0x00) {
marker = (short)((0xff<<8)|nextByte);
switch (Marker.fromShort(marker)) {
case RST0:
case RST1:
case RST2:
case RST3:
case RST4:
case RST5:
case RST6:
case RST7:
continue;
default:
}
break;
}
}
}
if (nextByte == -1) {
throw new IOException("Premature end of SOS segment!");
}
return marker;
}
/**
* @param offset offset to write page image data
*
* @return the position where to write the IFD for the current image page
*/
private static int copyPageData(IFD ifd, int offset, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
int writeOffset = offset; // We copy the offset to a local variable to keep the original value
int writeByteCount = 0; // To fix JPEG data double copy issue
// Move stream pointer to the right place
rout.seek(writeOffset);
// Original image data start from these offsets.
TiffField<?> stripOffSets = ifd.removeField(TiffTag.STRIP_OFFSETS);
if(stripOffSets == null)
stripOffSets = ifd.removeField(TiffTag.TILE_OFFSETS);
TiffField<?> stripByteCounts = ifd.getField(TiffTag.STRIP_BYTE_COUNTS);
if(stripByteCounts == null)
stripByteCounts = ifd.getField(TiffTag.TILE_BYTE_COUNTS);
/*
* Make sure this will work in the case when neither STRIP_OFFSETS nor TILE_OFFSETS presents.
* Not sure if this will ever happen for TIFF. JPEG EXIF data do not contain these fields.
*/
if(stripOffSets != null) {
int[] counts = stripByteCounts.getDataAsLong();
int[] off = stripOffSets.getDataAsLong();
int[] temp = new int[off.length];
TiffField<?> tiffField = ifd.getField(TiffTag.COMPRESSION);
// Uncompressed image with one strip or tile (may contain wrong StripByteCounts value)
// Bug fix for uncompressed image with one strip and wrong StripByteCounts value
if((tiffField == null ) || (tiffField != null && tiffField.getDataAsLong()[0] == 1)) { // Uncompressed data
int planaryConfiguration = 1;
tiffField = ifd.getField(TiffTag.PLANAR_CONFIGURATTION);
if(tiffField != null) planaryConfiguration = tiffField.getDataAsLong()[0];
tiffField = ifd.getField(TiffTag.SAMPLES_PER_PIXEL);
int samplesPerPixel = 1;
if(tiffField != null) samplesPerPixel = tiffField.getDataAsLong()[0];
// If there is only one strip/samplesPerPixel strips for PlanaryConfiguration = 2
if((planaryConfiguration == 1 && off.length == 1) || (planaryConfiguration == 2 && off.length == samplesPerPixel)) {
int[] totalBytes2Read = getBytes2Read(ifd);
for(int i = 0; i < off.length; i++)
counts[i] = totalBytes2Read[i];
}
} // End of bug fix
writeByteCount = counts[0];
// We are going to write the image data first
rout.seek(writeOffset);
// Copy image data from offset
for(int i = 0; i < off.length; i++) {
rin.seek(off[i]);
byte[] buf = new byte[counts[i]];
rin.readFully(buf);
rout.write(buf);
temp[i] = writeOffset;
writeOffset += buf.length;
}
if(ifd.getField(TiffTag.STRIP_BYTE_COUNTS) != null)
ifd.addField(new LongField(TiffTag.STRIP_OFFSETS.getValue(), temp));
else
ifd.addField(new LongField(TiffTag.TILE_OFFSETS.getValue(), temp));
}
// Add software field.
String softWare = "ICAFE - https://github.com/dragon66/icafe\0";
ifd.addField(new ASCIIField(TiffTag.SOFTWARE.getValue(), softWare));
/* The following are added to work with old-style JPEG compression (type 6) */
/* One of the flavors (found in JPEG EXIF thumbnail IFD - IFD1) of the old JPEG compression contains this field */
TiffField<?> jpegIFOffset = ifd.removeField(TiffTag.JPEG_INTERCHANGE_FORMAT);
if(jpegIFOffset != null) {
TiffField<?> jpegIFByteCount = ifd.removeField(TiffTag.JPEG_INTERCHANGE_FORMAT_LENGTH);
if(jpegIFOffset.getDataAsLong()[0] != stripOffSets.getDataAsLong()[0]) {
try {
if(jpegIFByteCount != null) {
rin.seek(jpegIFOffset.getDataAsLong()[0]);
byte[] bytes2Read = new byte[jpegIFByteCount.getDataAsLong()[0]];
rin.readFully(bytes2Read);
rout.seek(writeOffset);
rout.write(bytes2Read);
ifd.addField(jpegIFByteCount);
} else {
long startOffset = rout.getStreamPointer();
copyJpegIFByteCount(rin, rout, jpegIFOffset.getDataAsLong()[0], writeOffset);
long endOffset = rout.getStreamPointer();
ifd.addField(new LongField(TiffTag.JPEG_INTERCHANGE_FORMAT_LENGTH.getValue(), new int[]{(int)(endOffset - startOffset)}));
}
jpegIFOffset = new LongField(TiffTag.JPEG_INTERCHANGE_FORMAT.getValue(), new int[]{writeOffset});
ifd.addField(jpegIFOffset);
} catch (EOFException ex) {;};
} else { // To fix the issue of double copy the JPEG data, we can safely re-assign the pointers.
ifd.addField(new LongField(TiffTag.JPEG_INTERCHANGE_FORMAT.getValue(), new int[]{offset}));
ifd.addField(new LongField(TiffTag.JPEG_INTERCHANGE_FORMAT_LENGTH.getValue(), new int[]{writeByteCount}));
}
}
/* Another flavor of the old style JPEG compression type 6 contains separate tables */
TiffField<?> jpegTable = ifd.removeField(TiffTag.JPEG_DC_TABLES);
if(jpegTable != null) {
try {
ifd.addField(copyJpegHufTable(rin, rout, jpegTable, (int)rout.getStreamPointer()));
} catch(EOFException ex) {;}
}
jpegTable = ifd.removeField(TiffTag.JPEG_AC_TABLES);
if(jpegTable != null) {
try {
ifd.addField(copyJpegHufTable(rin, rout, jpegTable, (int)rout.getStreamPointer()));
} catch(EOFException ex) {;}
}
jpegTable = ifd.removeField(TiffTag.JPEG_Q_TABLES);
if(jpegTable != null) {
try {
ifd.addField(copyJpegQTable(rin, rout, jpegTable, (int)rout.getStreamPointer()));
} catch(EOFException ex) {;}
}
/* End of code to work with old-style JPEG compression */
// Return the actual stream position (we may have lost track of it)
return (int)rout.getStreamPointer();
}
// Copy a list of IFD and associated image data if any
private static int copyPages(List<IFD> list, int writeOffset, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
// Write the first page data
writeOffset = copyPageData(list.get(0), writeOffset, rin, rout);
// Then write the first IFD
writeOffset = list.get(0).write(rout, writeOffset);
// We are going to write the remaining image pages and IFDs if any
for(int i = 1; i < list.size(); i++) {
writeOffset = copyPageData(list.get(i), writeOffset, rin, rout);
// Tell the IFD to update next IFD offset for the following IFD
list.get(i-1).setNextIFDOffset(rout, writeOffset);
writeOffset = list.get(i).write(rout, writeOffset);
}
return writeOffset;
}
/**
* Extracts ICC_Profile from certain page of TIFF if any
*
* @param pageNumber page number from which to extract ICC_Profile
* @param rin RandomAccessInputStream for the input TIFF
* @return a byte array for the extracted ICC_Profile or null if none exists
* @throws Exception
*/
public static byte[] extractICCProfile(int pageNumber, RandomAccessInputStream rin) throws Exception {
// Read pass image header
int offset = readHeader(rin);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD workingPage = ifds.get(pageNumber);
TiffField<?> f_iccProfile = workingPage.getField(TiffTag.ICC_PROFILE);
if(f_iccProfile != null) {
return (byte[])f_iccProfile.getData();
}
return null;
}
public static byte[] extractICCProfile(RandomAccessInputStream rin) throws Exception {
return extractICCProfile(0, rin);
}
public static IRBThumbnail extractThumbnail(int pageNumber, RandomAccessInputStream rin) throws IOException {
// Read pass image header
int offset = readHeader(rin);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD workingPage = ifds.get(pageNumber);
TiffField<?> f_photoshop = workingPage.getField(TiffTag.PHOTOSHOP);
if(f_photoshop != null) {
byte[] data = (byte[])f_photoshop.getData();
IRB irb = new IRB(data);
if(irb.containsThumbnail()) {
IRBThumbnail thumbnail = irb.getThumbnail();
return thumbnail;
}
}
return null;
}
public static IRBThumbnail extractThumbnail(RandomAccessInputStream rin) throws IOException {
return extractThumbnail(0, rin);
}
public static void extractThumbnail(RandomAccessInputStream rin, String pathToThumbnail) throws IOException {
IRBThumbnail thumbnail = extractThumbnail(rin);
if(thumbnail != null) {
String outpath = "";
if(pathToThumbnail.endsWith("\\") || pathToThumbnail.endsWith("/"))
outpath = pathToThumbnail + "photoshop_thumbnail.jpg";
else
outpath = pathToThumbnail.replaceFirst("[.][^.]+$", "") + "_photoshop_t.jpg";
FileOutputStream fout = new FileOutputStream(outpath);
if(thumbnail.getDataType() == IRBThumbnail.DATA_TYPE_KJpegRGB) {
fout.write(thumbnail.getCompressedImage());
} else {
Bitmap bm = thumbnail.getRawImage();
try {
bm.compress(Bitmap.CompressFormat.JPEG, 100, fout);
} catch (Exception e) {
throw new IOException("Writing thumbnail failed!");
}
}
fout.close();
}
}
// Used to calculate how many bytes to read in case we have only one strip or tile
private static int[] getBytes2Read(IFD ifd) {
// Let's calculate how many bytes we are supposed to read
TiffField<?> tiffField = ifd.getField(TiffTag.IMAGE_WIDTH);
int imageWidth = tiffField.getDataAsLong()[0];
tiffField = ifd.getField(TiffTag.IMAGE_LENGTH);
int imageHeight = tiffField.getDataAsLong()[0];
// For YCbCr image only
int horizontalSampleFactor = 2; // Default 2X2
int verticalSampleFactor = 2; // Not 1X1
int photoMetric = ifd.getField(TiffTag.PHOTOMETRIC_INTERPRETATION).getDataAsLong()[0];
// Correction for imageWidth and imageHeight for YCbCr image
if(photoMetric == TiffFieldEnum.PhotoMetric.YCbCr.getValue()) {
TiffField<?> f_YCbCrSubSampling = ifd.getField(TiffTag.YCbCr_SUB_SAMPLING);
if(f_YCbCrSubSampling != null) {
int[] sampleFactors = f_YCbCrSubSampling.getDataAsLong();
horizontalSampleFactor = sampleFactors[0];
verticalSampleFactor = sampleFactors[1];
}
imageWidth = ((imageWidth + horizontalSampleFactor - 1)/horizontalSampleFactor)*horizontalSampleFactor;
imageHeight = ((imageHeight + verticalSampleFactor - 1)/verticalSampleFactor)*verticalSampleFactor;
}
int samplesPerPixel = 1;
tiffField = ifd.getField(TiffTag.SAMPLES_PER_PIXEL);
if(tiffField != null) {
samplesPerPixel = tiffField.getDataAsLong()[0];
}
int bitsPerSample = 1;
tiffField = ifd.getField(TiffTag.BITS_PER_SAMPLE);
if(tiffField != null) {
bitsPerSample = tiffField.getDataAsLong()[0];
}
int tileWidth = -1;
int tileLength = -1;
TiffField<?> f_tileLength = ifd.getField(TiffTag.TILE_LENGTH);
TiffField<?> f_tileWidth = ifd.getField(TiffTag.TILE_WIDTH);
if(f_tileWidth != null) {
tileWidth = f_tileWidth.getDataAsLong()[0];
tileLength = f_tileLength.getDataAsLong()[0];
}
int rowsPerStrip = imageHeight;
int rowWidth = imageWidth;
TiffField<?> f_rowsPerStrip = ifd.getField(TiffTag.ROWS_PER_STRIP);
if(f_rowsPerStrip != null) rowsPerStrip = f_rowsPerStrip.getDataAsLong()[0];
if(rowsPerStrip > imageHeight) rowsPerStrip = imageHeight;
if(tileWidth > 0) {
rowsPerStrip = tileLength;
rowWidth = tileWidth;
}
int planaryConfiguration = 1;
tiffField = ifd.getField(TiffTag.PLANAR_CONFIGURATTION);
if(tiffField != null) planaryConfiguration = tiffField.getDataAsLong()[0];
int[] totalBytes2Read = new int[samplesPerPixel];
if(planaryConfiguration == 1)
totalBytes2Read[0] = ((rowWidth*bitsPerSample*samplesPerPixel + 7)/8)*rowsPerStrip;
else
totalBytes2Read[0] = totalBytes2Read[1] = totalBytes2Read[2] = ((rowWidth*bitsPerSample + 7)/8)*rowsPerStrip;
if(photoMetric == TiffFieldEnum.PhotoMetric.YCbCr.getValue()) {
if(samplesPerPixel != 3) samplesPerPixel = 3;
int[] sampleBytesPerRow = new int[samplesPerPixel];
sampleBytesPerRow[0] = (bitsPerSample*rowWidth + 7)/8;
sampleBytesPerRow[1] = (bitsPerSample*rowWidth/horizontalSampleFactor + 7)/8;
sampleBytesPerRow[2] = sampleBytesPerRow[1];
int[] sampleRowsPerStrip = new int[samplesPerPixel];
sampleRowsPerStrip[0] = rowsPerStrip;
sampleRowsPerStrip[1] = rowsPerStrip/verticalSampleFactor;
sampleRowsPerStrip[2]= sampleRowsPerStrip[1];
totalBytes2Read[0] = sampleBytesPerRow[0]*sampleRowsPerStrip[0];
totalBytes2Read[1] = sampleBytesPerRow[1]*sampleRowsPerStrip[1];
totalBytes2Read[2] = totalBytes2Read[1];
if(tiffField != null) planaryConfiguration = tiffField.getDataAsLong()[0];
if(planaryConfiguration == 1)
totalBytes2Read[0] = totalBytes2Read[0] + totalBytes2Read[1] + totalBytes2Read[2];
}
return totalBytes2Read;
}
public static void insertComments(List<String> comments, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
insertComments(comments, 0, rin, rout);
}
public static void insertComments(List<String> comments, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
int offset = copyHeader(rin, rout);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD workingPage = ifds.get(pageNumber);
StringBuilder commentsBuilder = new StringBuilder();
// ASCII field allows for multiple strings
for(String comment : comments) {
commentsBuilder.append(comment);
commentsBuilder.append('\0');
}
workingPage.addField(new ASCIIField(TiffTag.IMAGE_DESCRIPTION.getValue(), commentsBuilder.toString()));
offset = copyPages(ifds, offset, rin, rout);
int firstIFDOffset = ifds.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
}
public static void insertExif(RandomAccessInputStream rin, RandomAccessOutputStream rout, Exif exif, boolean update) throws IOException {
insertExif(rin, rout, exif, 0, update);
}
/**
* Insert EXIF data with optional thumbnail IFD
*
* @param rin input image stream
* @param rout output image stream
* @param exif EXIF wrapper instance
* @param pageNumber page offset where to insert EXIF (zero based)
* @param update True to keep the original data, otherwise false
* @throws Exception
*/
public static void insertExif(RandomAccessInputStream rin, RandomAccessOutputStream rout, Exif exif, int pageNumber, boolean update) throws IOException {
int offset = copyHeader(rin, rout);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD imageIFD = ifds.get(pageNumber);
IFD exifSubIFD = imageIFD.getChild(TiffTag.EXIF_SUB_IFD);
IFD gpsSubIFD = imageIFD.getChild(TiffTag.GPS_SUB_IFD);
IFD interopSubIFD = (exifSubIFD != null)? exifSubIFD.getChild(ExifTag.EXIF_INTEROPERABILITY_OFFSET) : null;
IFD newImageIFD = exif.getImageIFD();
IFD newExifSubIFD = exif.getExifIFD();
IFD newGpsSubIFD = exif.getGPSIFD();
IFD newInteropSubIFD = exif.getInteropIFD();
if(newImageIFD != null) {
Collection<TiffField<?>> fields = newImageIFD.getFields();
for(TiffField<?> field : fields) {
Tag tag = TiffTag.fromShort(field.getTag());
if(imageIFD.getField(tag) != null && tag.isCritical())
throw new RuntimeException("Override of TIFF critical Tag - " + tag.getName() + " is not allowed!");
imageIFD.addField(field);
}
}
if(update && exifSubIFD != null && newExifSubIFD != null) {
exifSubIFD.addFields(newExifSubIFD.getFields());
newExifSubIFD = exifSubIFD;
}
if(newExifSubIFD != null) {
imageIFD.addField(new LongField(TiffTag.EXIF_SUB_IFD.getValue(), new int[]{0})); // Place holder
imageIFD.addChild(TiffTag.EXIF_SUB_IFD, newExifSubIFD);
if(update && interopSubIFD != null && newInteropSubIFD != null) {
interopSubIFD.addFields(newInteropSubIFD.getFields());
newInteropSubIFD = interopSubIFD;
}
if(newInteropSubIFD != null) {
newExifSubIFD.addField(new LongField(ExifTag.EXIF_INTEROPERABILITY_OFFSET.getValue(), new int[]{0})); // Place holder
newExifSubIFD.addChild(ExifTag.EXIF_INTEROPERABILITY_OFFSET, newInteropSubIFD);
}
}
if(update && gpsSubIFD != null && newGpsSubIFD != null) {
gpsSubIFD.addFields(newGpsSubIFD.getFields());
newGpsSubIFD = gpsSubIFD;
}
if(newGpsSubIFD != null) {
imageIFD.addField(new LongField(TiffTag.GPS_SUB_IFD.getValue(), new int[]{0})); // Place holder
imageIFD.addChild(TiffTag.GPS_SUB_IFD, newGpsSubIFD);
}
int writeOffset = FIRST_WRITE_OFFSET;
// Copy pages
writeOffset = copyPages(ifds, writeOffset, rin, rout);
int firstIFDOffset = ifds.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
}
public static void insertICCProfile(byte[] icc_profile, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
insertICCProfile(icc_profile, 0, rin, rout);
}
/**
* Insert ICC_Profile into TIFF page
*
* @param icc_profile byte array holding the ICC_Profile
* @param pageNumber page offset where to insert ICC_Profile
* @param rin RandomAccessInputStream for the input image
* @param rout RandomAccessOutputStream for the output image
* @throws Exception
*/
public static void insertICCProfile(byte[] icc_profile, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
int offset = copyHeader(rin, rout);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD workingPage = ifds.get(pageNumber);
workingPage.addField(new UndefinedField(TiffTag.ICC_PROFILE.getValue(), icc_profile));
offset = copyPages(ifds, offset, rin, rout);
int firstIFDOffset = ifds.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
}
public static void insertIPTC(RandomAccessInputStream rin, RandomAccessOutputStream rout, Collection<IPTCDataSet> iptcs, boolean update) throws IOException {
insertIPTC(rin, rout, 0, iptcs, update);
}
/**
* Insert IPTC data into TIFF image. If the original TIFF image contains IPTC data, we either keep
* or override them depending on the input parameter "update."
* <p>
* There is a possibility that IPTC data presents in more than one places such as a normal TIFF
* tag, or buried inside a Photoshop IPTC-NAA Image Resource Block (IRB), or even in a XMP block.
* Currently this method does the following thing: if no IPTC data was found from both Photoshop or
* normal IPTC tag, we insert the IPTC data with a normal IPTC tag. If IPTC data is found both as
* a Photoshop tag and a normal IPTC tag, depending on the "update" parameter, we will either delete
* the IPTC data from both places and insert the new IPTC data into the Photoshop tag or we will
* synchronize the two sets of IPTC data, delete the original IPTC from both places and insert the
* synchronized IPTC data along with the new IPTC data into the Photoshop tag. In both cases, we
* will keep the other IRBs from the original Photoshop tag unchanged.
*
* @param rin RandomAccessInputStream for the original TIFF
* @param rout RandomAccessOutputStream for the output TIFF with IPTC inserted
* @param pageNumber page offset where to insert IPTC
* @param iptcs A list of IPTCDataSet to insert into the TIFF image
* @param update whether we want to keep the original IPTC data or override it
* completely new IPTC data set
* @throws IOException
*/
public static void insertIPTC(RandomAccessInputStream rin, RandomAccessOutputStream rout, int pageNumber, Collection<IPTCDataSet> iptcs, boolean update) throws IOException {
int offset = copyHeader(rin, rout);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD workingPage = ifds.get(pageNumber);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
// See if we also have regular IPTC tag field
TiffField<?> f_iptc = workingPage.removeField(TiffTag.IPTC);
TiffField<?> f_photoshop = workingPage.getField(TiffTag.PHOTOSHOP);
if(f_photoshop != null) { // Read 8BIMs
IRB irb = new IRB((byte[])f_photoshop.getData());
// Shallow copy the map.
Map<Short, _8BIM> bims = new HashMap<Short, _8BIM>(irb.get8BIM());
_8BIM photoshop_iptc = bims.remove(ImageResourceID.IPTC_NAA.getValue());
if(photoshop_iptc != null) { // If we have IPTC
if(update) { // If we need to keep the old data, copy it
if(f_iptc != null) {// We are going to synchronize the two IPTC data
byte[] data = null;
if(f_iptc.getType() == FieldType.LONG)
data = ArrayUtils.toByteArray(f_iptc.getDataAsLong(), rin.getEndian() == IOUtils.BIG_ENDIAN);
else
data = (byte[])f_iptc.getData();
copyIPTCDataSet(iptcs, data);
}
// Now copy the Photoshop IPTC data
copyIPTCDataSet(iptcs, photoshop_iptc.getData());
// Remove duplicates
iptcs = new ArrayList<IPTCDataSet>(new HashSet<IPTCDataSet>(iptcs));
}
}
for(_8BIM bim : bims.values()) // Copy the other 8BIMs if any
bim.write(bout);
// Add a new Photoshop tag field to TIFF
workingPage.addField(new UndefinedField(TiffTag.PHOTOSHOP.getValue(), bout.toByteArray()));
} else { // We don't have photoshop, copy the old IPTC data in the IPTC tag is any
if(f_iptc != null && update) {
byte[] data = null;
if(f_iptc.getType() == FieldType.LONG)
data = ArrayUtils.toByteArray(f_iptc.getDataAsLong(), rin.getEndian() == IOUtils.BIG_ENDIAN);
else
data = (byte[])f_iptc.getData();
copyIPTCDataSet(iptcs, data);
}
}
// Sort the IPTCDataSet collection
List<IPTCDataSet> iptcList = new ArrayList<IPTCDataSet>(iptcs);
Collections.sort(iptcList);
// Write IPTCDataSet collection
bout.reset();
for(IPTCDataSet dataset : iptcList) {
dataset.write(bout);
}
// Add IPTC to regular IPTC tag field
workingPage.addField(new UndefinedField(TiffTag.IPTC.getValue(), bout.toByteArray()));
offset = copyPages(ifds, offset, rin, rout);
int firstIFDOffset = ifds.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
}
public static void insertIRB(RandomAccessInputStream rin, RandomAccessOutputStream rout, Collection<_8BIM> bims, boolean update) throws IOException {
insertIRB(rin, rout, 0, bims, update);
}
public static void insertIRB(RandomAccessInputStream rin, RandomAccessOutputStream rout, int pageNumber, Collection<_8BIM> bims, boolean update) throws IOException {
int offset = copyHeader(rin, rout);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD workingPage = ifds.get(pageNumber);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
if(update) {
TiffField<?> f_irb = workingPage.getField(TiffTag.PHOTOSHOP);
if(f_irb != null) {
IRB irb = new IRB((byte[])f_irb.getData());
// Shallow copy the map.
Map<Short, _8BIM> bimMap = new HashMap<Short, _8BIM>(irb.get8BIM());
for(_8BIM bim : bims) // Replace the original data
bimMap.put(bim.getID(), bim);
// In case we have two ThumbnailResource IRB, remove the Photoshop4.0 one
if(bimMap.containsKey(ImageResourceID.THUMBNAIL_RESOURCE_PS4.getValue())
&& bimMap.containsKey(ImageResourceID.THUMBNAIL_RESOURCE_PS5.getValue()))
bimMap.remove(ImageResourceID.THUMBNAIL_RESOURCE_PS4.getValue());
bims = bimMap.values();
}
}
for(_8BIM bim : bims)
bim.write(bout);
workingPage.addField(new UndefinedField(TiffTag.PHOTOSHOP.getValue(), bout.toByteArray()));
offset = copyPages(ifds, offset, rin, rout);
int firstIFDOffset = ifds.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
}
/**
* Insert a thumbnail into PHOTOSHOP private tag field
*
* @param rin RandomAccessInputStream for the input TIFF
* @param rout RandomAccessOutputStream for the output TIFF
* @param thumbnail a Bitmap to be inserted
* @throws Exception
*/
public static void insertThumbnail(RandomAccessInputStream rin, RandomAccessOutputStream rout, Bitmap thumbnail) throws IOException {
// Sanity check
if(thumbnail == null) throw new IllegalArgumentException("Input thumbnail is null");
_8BIM bim = new ThumbnailResource(thumbnail);
insertIRB(rin, rout, Arrays.asList(bim), true);
}
public static void insertXMP(XMP xmp, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
insertXMP(xmp.getData(), rin, rout);
}
public static void insertXMP(XMP xmp, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
insertXMP(xmp.getData(), pageNumber, rin, rout);
}
public static void insertXMP(byte[] xmp, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
insertXMP(xmp, 0, rin, rout);
}
/**
* Insert XMP data into TIFF image
* @param xmp byte array for the XMP data to be inserted
* @param pageNumber page offset where to insert XMP
* @param rin RandomAccessInputStream for the input image
* @param rout RandomAccessOutputStream for the output image
* @throws IOException
*/
public static void insertXMP(byte[] xmp, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
int offset = copyHeader(rin, rout);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD workingPage = ifds.get(pageNumber);
workingPage.addField(new UndefinedField(TiffTag.XMP.getValue(), xmp));
offset = copyPages(ifds, offset, rin, rout);
int firstIFDOffset = ifds.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
}
public static void insertXMP(String xmp, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
Document doc = XMLUtils.createXML(xmp);
XMLUtils.insertLeadingPI(doc, "xpacket", "begin='' id='W5M0MpCehiHzreSzNTczkc9d'");
XMLUtils.insertTrailingPI(doc, "xpacket", "end='w'");
byte[] xmpBytes = XMLUtils.serializeToByteArray(doc);
insertXMP(xmpBytes, rin, rout);
}
public static void printIFDs(Collection<IFD> list, String indent) {
int id = 0;
LOGGER.info("Printing IFDs ... ");
for(IFD currIFD : list) {
LOGGER.info("IFD #{}", id);
printIFD(currIFD, TiffTag.class, indent);
id++;
}
}
public static void printIFD(IFD currIFD, Class<? extends Tag> tagClass, String indent) {
StringBuilder ifd = new StringBuilder();
print(currIFD, tagClass, indent, ifd);
LOGGER.info("\n{}", ifd);
}
private static void print(IFD currIFD, Class<? extends Tag> tagClass, String indent, StringBuilder ifds) {
// Use reflection to invoke fromShort(short) method
Method method = null;
try {
method = tagClass.getDeclaredMethod("fromShort", short.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
Collection<TiffField<?>> fields = currIFD.getFields();
int i = 0;
for(TiffField<?> field : fields) {
ifds.append(indent);
ifds.append("Field #" + i + "\n");
ifds.append(indent);
short tag = field.getTag();
Tag ftag = TiffTag.UNKNOWN;
if(tag == ExifTag.PADDING.getValue()) {
ftag = ExifTag.PADDING;
} else {
try {
ftag = (Tag)method.invoke(null, tag);
} catch (IllegalAccessException e) {
LOGGER.error("IllegalAcessException", e);
} catch (IllegalArgumentException e) {
LOGGER.error("IllegalArgumentException", e);
} catch (InvocationTargetException e) {
LOGGER.error("InvocationTargetException", e);
}
}
if (ftag == TiffTag.UNKNOWN) {
LOGGER.warn("Tag: {} {}{}{} {}", ftag, "[Value: 0x", Integer.toHexString(tag&0xffff), "]", "(Unknown)");
} else {
ifds.append("Tag: " + ftag + "\n");
}
FieldType ftype = field.getType();
ifds.append(indent);
ifds.append("Field type: " + ftype + "\n");
int field_length = field.getLength();
ifds.append(indent);
ifds.append("Field length: " + field_length + "\n");
ifds.append(indent);
String suffix = null;
if(ftype == FieldType.SHORT || ftype == FieldType.SSHORT)
suffix = ftag.getFieldAsString(field.getDataAsLong());
else
suffix = ftag.getFieldAsString(field.getData());
ifds.append("Field value: " + field.getDataAsString() + (StringUtils.isNullOrEmpty(suffix)?"":" => " + suffix) + "\n");
i++;
}
Map<Tag, IFD> children = currIFD.getChildren();
if(children.get(TiffTag.EXIF_SUB_IFD) != null) {
ifds.append(indent + "--------- ");
ifds.append("<<Exif SubIFD starts>>\n");
print(children.get(TiffTag.EXIF_SUB_IFD), ExifTag.class, indent + "--------- ", ifds);
ifds.append(indent + "--------- ");
ifds.append("<<Exif SubIFD ends>>\n");
}
if(children.get(TiffTag.GPS_SUB_IFD) != null) {
ifds.append(indent + "--------- ");
ifds.append("<<GPS SubIFD starts>>\n");
print(children.get(TiffTag.GPS_SUB_IFD), GPSTag.class, indent + "--------- ", ifds);
ifds.append(indent + "--------- ");
ifds.append("<<GPS SubIFD ends>>\n");
}
}
private static int readHeader(RandomAccessInputStream rin) throws IOException {
int offset = 0;
// First 2 bytes determine the byte order of the file
rin.seek(STREAM_HEAD);
short endian = rin.readShort();
offset += 2;
if (endian == IOUtils.BIG_ENDIAN) {
rin.setReadStrategy(ReadStrategyMM.getInstance());
} else if(endian == IOUtils.LITTLE_ENDIAN) {
rin.setReadStrategy(ReadStrategyII.getInstance());
} else {
rin.close();
throw new RuntimeException("Invalid TIFF byte order");
}
// Read TIFF identifier
rin.seek(offset);
short tiff_id = rin.readShort();
offset +=2;
if(tiff_id!=0x2a) { //"*" 42 decimal
rin.close();
throw new RuntimeException("Invalid TIFF identifier");
}
rin.seek(offset);
offset = rin.readInt();
return offset;
}
// Read IFD without header
public static int readIFD(RandomAccessInputStream rin, List<IFD> list, Class<? extends Tag> tagClass) throws IOException {
return readIFD(null, null, tagClass, rin, list, 0);
}
private static int readIFD(IFD parent, Tag parentTag, Class<? extends Tag> tagClass, RandomAccessInputStream rin, List<IFD> list, int offset) throws IOException {
// Use reflection to invoke fromShort(short) method
Method method = null;
try {
method = tagClass.getDeclaredMethod("fromShort", short.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
IFD tiffIFD = new IFD();
rin.seek(offset);
int no_of_fields = rin.readShort();
offset += 2;
for (int i = 0; i < no_of_fields; i++) {
rin.seek(offset);
short tag = rin.readShort();
Tag ftag = TiffTag.UNKNOWN;
try {
ftag = (Tag)method.invoke(null, tag);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
offset += 2;
rin.seek(offset);
short type = rin.readShort();
FieldType ftype = FieldType.fromShort(type);
offset += 2;
rin.seek(offset);
int field_length = rin.readInt();
offset += 4;
////// Try to read actual data.
switch (ftype) {
case BYTE:
case SBYTE:
case UNDEFINED:
byte[] data = new byte[field_length];
rin.seek(offset);
if(field_length <= 4) {
rin.readFully(data, 0, field_length);
} else {
rin.seek(rin.readInt());
rin.readFully(data, 0, field_length);
}
TiffField<byte[]> byteField = null;
if(ftype == FieldType.BYTE) {
byteField = new ByteField(tag, data);
} else if(ftype == FieldType.SBYTE) {
byteField = new SByteField(tag, data);
} else {
if(ftag == ExifTag.MAKER_NOTE)
byteField = new MakerNoteField(tiffIFD, data);
else
byteField = new UndefinedField(tag, data);
}
tiffIFD.addField(byteField);
offset += 4;
break;
case ASCII:
data = new byte[field_length];
if(field_length <= 4) {
rin.seek(offset);
rin.readFully(data, 0, field_length);
}
else {
rin.seek(offset);
rin.seek(rin.readInt());
rin.readFully(data, 0, field_length);
}
TiffField<String> ascIIField = new ASCIIField(tag, new String(data, "UTF-8"));
tiffIFD.addField(ascIIField);
offset += 4;
break;
case SHORT:
case SSHORT:
short[] sdata = new short[field_length];
if(field_length == 1) {
rin.seek(offset);
sdata[0] = rin.readShort();
offset += 4;
} else if (field_length == 2) {
rin.seek(offset);
sdata[0] = rin.readShort();
offset += 2;
rin.seek(offset);
sdata[1] = rin.readShort();
offset += 2;
} else {
rin.seek(offset);
int toOffset = rin.readInt();
offset += 4;
for (int j = 0; j <field_length; j++){
rin.seek(toOffset);
sdata[j] = rin.readShort();
toOffset += 2;
}
}
TiffField<short[]> shortField = null;
if(ftype == FieldType.SSHORT) {
shortField = new SShortField(tag, sdata);
} else {
shortField = new ShortField(tag, sdata);
}
tiffIFD.addField(shortField);
break;
case LONG:
case SLONG:
int[] ldata = new int[field_length];
if(field_length == 1) {
rin.seek(offset);
ldata[0] = rin.readInt();
offset += 4;
} else {
rin.seek(offset);
int toOffset = rin.readInt();
offset += 4;
for (int j=0;j<field_length; j++){
rin.seek(toOffset);
ldata[j] = rin.readInt();
toOffset += 4;
}
}
TiffField<int[]> longField = null;
if(ftype == FieldType.SLONG) {
longField = new SLongField(tag, ldata);
} else {
longField = new LongField(tag, ldata);
}
tiffIFD.addField(longField);
if ((ftag == TiffTag.EXIF_SUB_IFD) && (ldata[0]!= 0)) {
try { // If something bad happens, we skip the sub IFD
readIFD(tiffIFD, TiffTag.EXIF_SUB_IFD, ExifTag.class, rin, null, ldata[0]);
} catch(Exception e) {
tiffIFD.removeField(TiffTag.EXIF_SUB_IFD);
e.printStackTrace();
}
} else if ((ftag == TiffTag.GPS_SUB_IFD) && (ldata[0] != 0)) {
try {
readIFD(tiffIFD, TiffTag.GPS_SUB_IFD, GPSTag.class, rin, null, ldata[0]);
} catch(Exception e) {
tiffIFD.removeField(TiffTag.GPS_SUB_IFD);
e.printStackTrace();
}
} else if((ftag == ExifTag.EXIF_INTEROPERABILITY_OFFSET) && (ldata[0] != 0)) {
try {
readIFD(tiffIFD, ExifTag.EXIF_INTEROPERABILITY_OFFSET, InteropTag.class, rin, null, ldata[0]);
} catch(Exception e) {
tiffIFD.removeField(ExifTag.EXIF_INTEROPERABILITY_OFFSET);
e.printStackTrace();
}
} else if (ftag == TiffTag.SUB_IFDS) {
for(int ifd = 0; ifd < ldata.length; ifd++) {
try {
readIFD(tiffIFD, TiffTag.SUB_IFDS, TiffTag.class, rin, null, ldata[0]);
} catch(Exception e) {
tiffIFD.removeField(TiffTag.SUB_IFDS);
e.printStackTrace();
}
}
}
break;
case FLOAT:
float[] fdata = new float[field_length];
if(field_length == 1) {
rin.seek(offset);
fdata[0] = rin.readFloat();
offset += 4;
} else {
rin.seek(offset);
int toOffset = rin.readInt();
offset += 4;
for (int j=0;j<field_length; j++){
rin.seek(toOffset);
fdata[j] = rin.readFloat();
toOffset += 4;
}
}
TiffField<float[]> floatField = new FloatField(tag, fdata);
tiffIFD.addField(floatField);
break;
case DOUBLE:
double[] ddata = new double[field_length];
rin.seek(offset);
int toOffset = rin.readInt();
offset += 4;
for (int j=0;j<field_length; j++){
rin.seek(toOffset);
ddata[j] = rin.readDouble();
toOffset += 8;
}
TiffField<double[]> doubleField = new DoubleField(tag, ddata);
tiffIFD.addField(doubleField);
break;
case RATIONAL:
case SRATIONAL:
int len = 2*field_length;
ldata = new int[len];
rin.seek(offset);
toOffset = rin.readInt();
offset += 4;
for (int j=0;j<len; j+=2){
rin.seek(toOffset);
ldata[j] = rin.readInt();
toOffset += 4;
rin.seek(toOffset);
ldata[j+1] = rin.readInt();
toOffset += 4;
}
TiffField<int[]> rationalField = null;
if(ftype == FieldType.SRATIONAL) {
rationalField = new SRationalField(tag, ldata);
} else {
rationalField = new RationalField(tag, ldata);
}
tiffIFD.addField(rationalField);
break;
case IFD:
ldata = new int[field_length];
if(field_length == 1) {
rin.seek(offset);
ldata[0] = rin.readInt();
offset += 4;
} else {
rin.seek(offset);
toOffset = rin.readInt();
offset += 4;
for (int j=0;j<field_length; j++){
rin.seek(toOffset);
ldata[j] = rin.readInt();
toOffset += 4;
}
}
TiffField<int[]> ifdField = new IFDField(tag, ldata);
tiffIFD.addField(ifdField);
for(int ifd = 0; ifd < ldata.length; ifd++) {
readIFD(tiffIFD, TiffTag.SUB_IFDS, TiffTag.class, rin, null, ldata[0]);
}
break;
default:
offset += 4;
break;
}
}
// If this is a child IFD, add it to its parent
if(parent != null)
parent.addChild(parentTag, tiffIFD);
else // Otherwise, add to the main IFD list
list.add(tiffIFD);
rin.seek(offset);
return rin.readInt();
}
private static void readIFDs(IFD parent, Tag parentTag, Class<? extends Tag> tagClass, List<IFD> list, int offset, RandomAccessInputStream rin) throws IOException {
// Read the IFDs into a list first
while (offset != 0) {
offset = readIFD(parent, parentTag, tagClass, rin, list, offset);
}
}
public static void readIFDs(List<IFD> list, RandomAccessInputStream rin) throws IOException {
int offset = readHeader(rin);
readIFDs(null, null, TiffTag.class, list, offset, rin);
}
public static Map<MetadataType, Metadata> readMetadata(RandomAccessInputStream rin) throws IOException {
return readMetadata(rin, 0);
}
public static Map<MetadataType, Metadata> readMetadata(RandomAccessInputStream rin, int pageNumber) throws IOException {
Map<MetadataType, Metadata> metadataMap = new HashMap<MetadataType, Metadata>();
int offset = readHeader(rin);
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD currIFD = ifds.get(pageNumber);
TiffField<?> field = currIFD.getField(TiffTag.ICC_PROFILE);
if(field != null) { // We have found ICC_Profile
metadataMap.put(MetadataType.ICC_PROFILE, new ICCProfile((byte[])field.getData()));
}
field = currIFD.getField(TiffTag.XMP);
if(field != null) { // We have found XMP
metadataMap.put(MetadataType.XMP, new TiffXMP((byte[])field.getData()));
}
field = currIFD.getField(TiffTag.PHOTOSHOP);
if(field != null) { // We have found Photoshop IRB
IRB irb = new IRB((byte[])field.getData());
metadataMap.put(MetadataType.PHOTOSHOP_IRB, irb);
_8BIM photoshop_8bim = irb.get8BIM(ImageResourceID.IPTC_NAA.getValue());
if(photoshop_8bim != null) { // If we have IPTC data inside Photoshop, keep it
IPTC iptc = new IPTC(photoshop_8bim.getData());
metadataMap.put(MetadataType.IPTC, iptc);
}
}
field = currIFD.getField(TiffTag.IPTC);
if(field != null) { // We have found IPTC data
IPTC iptc = (IPTC)(metadataMap.get(MetadataType.IPTC));
byte[] iptcData = null;
FieldType type = field.getType();
if(type == FieldType.LONG)
iptcData = ArrayUtils.toByteArray(field.getDataAsLong(), rin.getEndian() == IOUtils.BIG_ENDIAN);
else
iptcData = (byte[])field.getData();
if(iptc != null) // If we have IPTC data from IRB, consolidate it with the current data
iptcData = ArrayUtils.concat(iptcData, iptc.getData());
metadataMap.put(MetadataType.IPTC, new IPTC(iptcData));
}
field = currIFD.getField(TiffTag.EXIF_SUB_IFD);
if(field != null) { // We have found EXIF SubIFD
metadataMap.put(MetadataType.EXIF, new TiffExif(currIFD));
}
field = currIFD.getField(TiffTag.IMAGE_SOURCE_DATA);
if(field != null) {
boolean bigEndian = (rin.getEndian() == IOUtils.BIG_ENDIAN);
ReadStrategy readStrategy = bigEndian?ReadStrategyMM.getInstance():ReadStrategyII.getInstance();
metadataMap.put(MetadataType.PHOTOSHOP_DDB, new DDB((byte[])field.getData(), readStrategy));
}
field = currIFD.getField(TiffTag.IMAGE_DESCRIPTION);
if(field != null) { // We have Comment
Comments comments = new pixy.meta.image.Comments();
comments.addComment(field.getDataAsString());
metadataMap.put(MetadataType.COMMENT, comments);
}
return metadataMap;
}
/**
* Remove meta data from TIFF image
*
* @param rin RandomAccessInputStream for the input image
* @param rout RandomAccessOutputStream for the output image
* @param pageNumber working page from which to remove metadata
* @param metadataTypes a variable length array of MetadataType to be removed
* @throws IOException
* @return A map of the removed metadata
*/
public static Map<MetadataType, Metadata> removeMetadata(int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout, MetadataType ... metadataTypes) throws IOException {
return removeMetadata(new HashSet<MetadataType>(Arrays.asList(metadataTypes)), pageNumber, rin, rout);
}
/**
* Remove meta data from TIFF image
*
* @param rin RandomAccessInputStream for the input image
* @param rout RandomAccessOutputStream for the output image
* @param metadataTypes a variable length array of MetadataType to be removed
* @throws IOException
* @return A map of the removed metadata
*/
public static Map<MetadataType, Metadata> removeMetadata(RandomAccessInputStream rin, RandomAccessOutputStream rout, MetadataType ... metadataTypes) throws IOException {
return removeMetadata(0, rin, rout, metadataTypes);
}
/**
* Remove meta data from TIFF image
*
* @param pageNumber working page from which to remove EXIF and GPS data
* @param rin RandomAccessInputStream for the input image
* @param rout RandomAccessOutputStream for the output image
* @throws IOException
* @return A map of the removed metadata
*/
public static Map<MetadataType, Metadata> removeMetadata(Set<MetadataType> metadataTypes, int pageNumber, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
int offset = copyHeader(rin, rout);
// Read the IFDs into a list first
List<IFD> ifds = new ArrayList<IFD>();
readIFDs(null, null, TiffTag.class, ifds, offset, rin);
// Create a map to hold all the metadata and thumbnails
Map<MetadataType, Metadata> metadataMap = new HashMap<MetadataType, Metadata>();
if(pageNumber < 0 || pageNumber >= ifds.size())
throw new IllegalArgumentException("pageNumber " + pageNumber + " out of bounds: 0 - " + (ifds.size() - 1));
IFD workingPage = ifds.get(pageNumber);
TiffField<?> metadata = null;
for(MetadataType metaType : metadataTypes) {
switch(metaType) {
case XMP:
TiffField<?> xmpField = workingPage.removeField(TiffTag.XMP);
if(xmpField != null) metadataMap.put(MetadataType.XMP, new TiffXMP((byte[])xmpField.getData()));
metadata = workingPage.removeField(TiffTag.PHOTOSHOP);
if(metadata != null) {
byte[] data = (byte[])metadata.getData();
// We only remove XMP and keep the other IRB data untouched.
List<_8BIM> bims = removeMetadataFromIRB(workingPage, data, ImageResourceID.XMP_METADATA);
if(bims.size() > 0 && xmpField == null) metadataMap.put(MetadataType.XMP, new TiffXMP(bims.get(0).getData()));
}
break;
case IPTC:
TiffField<?> iptcField = workingPage.removeField(TiffTag.IPTC);
if(iptcField != null) metadataMap.put(MetadataType.IPTC, new IPTC((byte[])iptcField.getData()));
metadata = workingPage.removeField(TiffTag.PHOTOSHOP);
if(metadata != null) {
byte[] data = (byte[])metadata.getData();
// We only remove IPTC_NAA and keep the other IRB data untouched.
List<_8BIM> bims = removeMetadataFromIRB(workingPage, data, ImageResourceID.IPTC_NAA);
if(bims.size() > 0 && iptcField == null) metadataMap.put(MetadataType.IPTC, new IPTC(bims.get(0).getData()));
}
break;
case ICC_PROFILE:
TiffField<?> iccField = workingPage.removeField(TiffTag.ICC_PROFILE);
if(iccField != null) metadataMap.put(MetadataType.ICC_PROFILE, new ICCProfile((byte[])iccField.getData()));
metadata = workingPage.removeField(TiffTag.PHOTOSHOP);
if(metadata != null) {
byte[] data = (byte[])metadata.getData();
// We only remove ICC_PROFILE and keep the other IRB data untouched.
List<_8BIM> bims = removeMetadataFromIRB(workingPage, data, ImageResourceID.ICC_PROFILE);
if(bims.size() > 0 && iccField == null) metadataMap.put(MetadataType.ICC_PROFILE, new ICCProfile(bims.get(0).getData()));
}
break;
case PHOTOSHOP_IRB:
TiffField<?> irbField = workingPage.removeField(TiffTag.PHOTOSHOP);
if(irbField != null) metadataMap.put(MetadataType.PHOTOSHOP_IRB, new IRB((byte[])irbField.getData()));
break;
case EXIF:
TiffField<?> exifField = workingPage.removeField(TiffTag.EXIF_SUB_IFD);
if(exifField != null) metadataMap.put(MetadataType.EXIF, new TiffExif(workingPage));
workingPage.removeField(TiffTag.GPS_SUB_IFD);
metadata = workingPage.removeField(TiffTag.PHOTOSHOP);
if(metadata != null) {
byte[] data = (byte[])metadata.getData();
// We only remove EXIF and keep the other IRB data untouched.
removeMetadataFromIRB(workingPage, data, ImageResourceID.EXIF_DATA1, ImageResourceID.EXIF_DATA3);
}
break;
case COMMENT:
TiffField<?> commentField = workingPage.removeField(TiffTag.IMAGE_DESCRIPTION);
if(commentField != null) {
Comments comments = new Comments();
comments.addComment(commentField.getDataAsString());
metadataMap.put(MetadataType.COMMENT, comments);
}
break;
default:
}
}
offset = copyPages(ifds, offset, rin, rout);
int firstIFDOffset = ifds.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
return metadataMap;
}
/**
* Remove meta data from TIFF image
*
* @param metadataTypes a set of MetadataType to be removed
* @param rin RandomAccessInputStream for the input image
* @param rout RandomAccessOutputStream for the output image
* @throws IOException
* @return A map of the removed metadata
*/
public static Map<MetadataType, Metadata> removeMetadata(Set<MetadataType> metadataTypes, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
return removeMetadata(metadataTypes, 0, rin, rout);
}
private static List<_8BIM> removeMetadataFromIRB(IFD workingPage, byte[] data, ImageResourceID ... ids) throws IOException {
IRB irb = new IRB(data);
// Shallow copy the map.
Map<Short, _8BIM> bimMap = new HashMap<Short, _8BIM>(irb.get8BIM());
List<_8BIM> bimList = new ArrayList<_8BIM>();
// We only remove XMP and keep the other IRB data untouched.
for(ImageResourceID id : ids) {
_8BIM bim = bimMap.remove(id.getValue());
if(bim != null) bimList.add(bim);
}
if(bimMap.size() > 0) {
// Write back the IRB
ByteArrayOutputStream bout = new ByteArrayOutputStream();
for(_8BIM bim : bimMap.values())
bim.write(bout);
// Add new PHOTOSHOP field
workingPage.addField(new ByteField(TiffTag.PHOTOSHOP.getValue(), bout.toByteArray()));
}
return bimList;
}
public static int retainPages(int startPage, int endPage, RandomAccessInputStream rin, RandomAccessOutputStream rout) throws IOException {
if(startPage < 0 || endPage < 0)
throw new IllegalArgumentException("Negative start or end page");
else if(startPage > endPage)
throw new IllegalArgumentException("Start page is larger than end page");
List<IFD> list = new ArrayList<IFD>();
int offset = copyHeader(rin, rout);
// Step 1: read the IFDs into a list first
readIFDs(null, null, TiffTag.class, list, offset, rin);
// Step 2: remove pages from a multiple page TIFF
int pagesRetained = list.size();
List<IFD> newList = new ArrayList<IFD>();
if(startPage <= list.size() - 1) {
if(endPage > list.size() - 1) endPage = list.size() - 1;
for(int i = endPage; i >= startPage; i--) {
newList.add(list.get(i));
}
}
if(newList.size() > 0) {
pagesRetained = newList.size();
list.retainAll(newList);
}
// Reset pageNumber for the existing pages
for(int i = 0; i < list.size(); i++) {
list.get(i).removeField(TiffTag.PAGE_NUMBER);
list.get(i).addField(new ShortField(TiffTag.PAGE_NUMBER.getValue(), new short[]{(short)i, (short)(list.size() - 1)}));
}
// End of removing pages
// Step 3: copy the remaining pages
// 0x08 is the first write offset
int writeOffset = FIRST_WRITE_OFFSET;
offset = copyPages(list, writeOffset, rin, rout);
int firstIFDOffset = list.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
return pagesRetained;
}
// Return number of pages retained
public static int retainPages(RandomAccessInputStream rin, RandomAccessOutputStream rout, int... pages) throws IOException {
List<IFD> list = new ArrayList<IFD>();
int offset = copyHeader(rin, rout);
// Step 1: read the IFDs into a list first
readIFDs(null, null, TiffTag.class, list, offset, rin);
// Step 2: remove pages from a multiple page TIFF
int pagesRetained = list.size();
List<IFD> newList = new ArrayList<IFD>();
Arrays.sort(pages);
for(int i = pages.length - 1; i >= 0; i--) {
if(pages[i] >= 0 && pages[i] < list.size())
newList.add(list.get(pages[i]));
}
if(newList.size() > 0) {
pagesRetained = newList.size();
list.retainAll(newList);
}
// End of removing pages
// Reset pageNumber for the existing pages
for(int i = 0; i < list.size(); i++) {
list.get(i).removeField(TiffTag.PAGE_NUMBER);
list.get(i).addField(new ShortField(TiffTag.PAGE_NUMBER.getValue(), new short[]{(short)i, (short)(list.size() - 1)}));
}
// Step 3: copy the remaining pages
// 0x08 is the first write offset
int writeOffset = FIRST_WRITE_OFFSET;
offset = copyPages(list, writeOffset, rin, rout);
int firstIFDOffset = list.get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
return pagesRetained;
}
public static void write(TIFFImage tiffImage, RandomAccessOutputStream rout) throws IOException {
RandomAccessInputStream rin = tiffImage.getInputStream();
int offset = writeHeader(rout);
offset = copyPages(tiffImage.getIFDs(), offset, rin, rout);
int firstIFDOffset = tiffImage.getIFDs().get(0).getStartOffset();
writeToStream(rout, firstIFDOffset);
}
// Return stream offset where to write actual image data or IFD
private static int writeHeader(RandomAccessOutputStream rout) throws IOException {
// Write byte order
short endian = rout.getEndian();
rout.writeShort(endian);
// Write TIFF identifier
rout.writeShort(0x2a);
return FIRST_WRITE_OFFSET;
}
private static void writeToStream(RandomAccessOutputStream rout, int firstIFDOffset) throws IOException {
// Go to the place where we should write the first IFD offset
// and write the first IFD offset
rout.seek(OFFSET_TO_WRITE_FIRST_IFD_OFFSET);
rout.writeInt(firstIFDOffset);
// Dump the data to the real output stream
rout.seek(STREAM_HEAD);
rout.writeToStream(rout.getLength());
//rout.flush();
}
}