package com.almalence.plugins.export.ExifDriver;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import android.util.Log;
import com.almalence.plugins.export.ExifDriver.Values.ExifValue;
import com.almalence.plugins.export.ExifDriver.Values.UndefinedValueAccessException;
import com.almalence.plugins.export.ExifDriver.Values.ValueByteArray;
import com.almalence.plugins.export.ExifDriver.Values.ValueNumber;
import com.almalence.plugins.export.ExifDriver.Values.ValueRationals;
/**
* Driver for reading/writting EXIF meta data to JPEG images. It tries to
* conform Exif Version 2.2. For more info see http://exif.org/ JPEG data from
* the (very brief) point of view of the driver looks like this: 2B SOI - Start
* Of Image FF D8 2B APP1 marker FF E1 2B APP1 size (includes itself) 6B EXIF
* header 45 78 69 66 00 00 6B TIFF header 49 49 2A 00 08 00 00 00 (Intel
* endian) or 4D 4D 00 2A 00 00 00 08 (Motorola endian) Following EXIF data are
* encoded with endian declared above
*
* IFD0 - main IFD directory. It contains tags, which holds offsets of IFDExif
* and IFDGPS subdirectories. It's last 4B holds offset of IFD1 IFD0-related
* data
*
* IFDExif - Exif subdirectory IFDExif-related data
*
* IFDInteroperability - Interoperability subdirectory
* IFDInteroperability-related data
*
* IFDGPS - GPS subdirectory IFDGPS-related data
*
* IFD1 - Thumbnail information directory - it's offset is in the last 4B of
* IFD0 IFD1-related data * * End of area this driver modifies 2B SOI - Start Of
* Image (Thumbnail actually) FF D8 Thumbnail data 2B EOI - End Of Image FF D9 *
* * End of area this driver works with ... "Not-interesting" stuff follows
*
* IFD (sub)directory structure is always the following: 2B count of entries n
* of entries - 12B per entry (2B tag, 2B format, 4B number of values, 4B values
* or offset to them). 4B "Next" IFD - is used only in IFD0 for reference to
* IFD1 in other IFD's 0.
*
* @author kocian
*/
public class ExifDriver
{
// Private constants
private final String LOGTAG = getClass().getName();
// Datatypes
public static final int FORMAT_UNSIGNED_BYTE = 0x01;
public static final int FORMAT_ASCII_STRINGS = 0x02;
public static final int FORMAT_UNSIGNED_SHORT = 0x03;
public static final int FORMAT_UNSIGNED_LONG = 0x04;
public static final int FORMAT_UNSIGNED_RATIONAL = 0x05;
public static final int FORMAT_SIGNED_BYTE = 0x06;
public static final int FORMAT_UNDEFINED = 0x07;
public static final int FORMAT_SIGNED_SHORT = 0x08;
public static final int FORMAT_SIGNED_LONG = 0x09;
public static final int FORMAT_SIGNED_RATIONAL = 0x0a;
// Convenience 'hash' for finding component bit width (see above formats)
public static final int[] COMP_WIDTHS = new int[] { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4,
8 };
// Pointer values
private final int TAG_EXIF_POINTER = 0x8769;
private final int TAG_GPS_POINTER = 0x8825;
private final int TAG_INTEROPERABILITY_POINTER = 0xa005;
// Public constants
// IFD0-related tags, some of them are presented in IFD1 too
public static final int TAG_IMAGE_WIDTH = 0x0100;
public static final int TAG_IMAGE_HEIGHT = 0x0101;
public static final int TAG_BITS_PER_SAMPLE = 0x0102;
public static final int TAG_COMPRESSION = 0x0103;
public static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106;
public static final int TAG_ORIENTATION = 0x0112;
public static final int TAG_SAMPLES_PER_PIXEL = 0x0115;
public static final int TAG_PLANAR_CONFIGURATION = 0x011c;
public static final int TAG_YCBCR_SUBSAMPLING = 0x0212;
public static final int TAG_YCBCRPOSITIONING = 0x0213;
public static final int TAG_XRESOLUTION = 0x011a;
public static final int TAG_YRESOLUTION = 0x011b;
public static final int TAG_RESOLUTION_UNIT = 0x0128;
public static final int TAG_STRIP_OFFSETS = 0x0111;
public static final int TAG_ROWS_PER_STRIP = 0x0116;
public static final int TAG_STRIP_BYTECOUNTS = 0x0117;
public static final int TAG_JPEG_INTERCHANGE_FORMAT = 0x0201;
public static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 0x0202;
public static final int TAG_TRANSFER_FUNCTION = 0x012d;
public static final int TAG_WHITE_POINT = 0x013e;
public static final int TAG_PRIMARY_CHROMATICITIES = 0x013f;
public static final int TAG_YCBCR_COEFICIENTS = 0x0211;
public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214;
public static final int TAG_DATETIME = 0x0132;
public static final int TAG_IMAGE_DESCRIPTION = 0x010e;
public static final int TAG_MAKE = 0x010f;
public static final int TAG_MODEL = 0x0110;
public static final int TAG_SOFTWARE = 0x0131;
public static final int TAG_ARTIST = 0x013b;
public static final int TAG_COPYRIGHT = 0x8298;
// IFD Exif tags
public static final int TAG_EXIF_VERSION = 0x9000;
public static final int TAG_FLASHPIX_VERSION = 0xa000;
public static final int TAG_COLOR_SPACE = 0xa001;
public static final int TAG_COMPONENT_CONFIGURATION = 0x9101;
public static final int TAG_COMPRESSED_BITS_PER_PIXEL = 0x9102;
public static final int TAG_PIXEL_X_DIMENSION = 0xa002;
public static final int TAG_PIXEL_Y_DIMENSION = 0xa003;
public static final int TAG_MARKER_NOTE = 0x927c;
public static final int TAG_USER_COMMENT = 0x9286;
public static final int TAG_RELATED_SOUND_FILE = 0xa004;
public static final int TAG_DATETIME_ORIGINAL = 0x9003;
public static final int TAG_DATETIME_DIGITIZED = 0x9004;
public static final int TAG_SUB_SEC_TIME = 0x9290;
public static final int TAG_SUB_SEC_TIME_ORIGINAL = 0x9291;
public static final int TAG_SUB_SEC_TIME_DIGITIZED = 0x9292;
public static final int TAG_IMAGE_UNIQUE_ID = 0xa420;
public static final int TAG_EXPOSURE_TIME = 0x829a;
public static final int TAG_FNUMBER = 0x829d;
public static final int TAG_EXPOSURE_PROGRAM = 0x8822;
public static final int TAG_SPECTRAL_SENSITIVITY = 0x8824;
public static final int TAG_ISO_SPEED_RATINGS = 0x8827;
public static final int TAG_OECF = 0x8828;
public static final int TAG_SHUTTER_SPEED_VALUE = 0x9201;
public static final int TAG_APERTURE_VALUE = 0x9202;
public static final int TAG_BRIGHTNESS_VALUE = 0x9203;
public static final int TAG_EXPOSURE_BIAS_VALUE = 0x9204;
public static final int TAG_MAX_APERTURE_VALUE = 0x9205;
public static final int TAG_SUBJECT_DISTANCE = 0x9206;
public static final int TAG_METERING_MODE = 0x9207;
public static final int TAG_LIGHT_SOURCE = 0x9208;
public static final int TAG_FLASH = 0x9209;
public static final int TAG_FOCAL_LENGTH = 0x920a;
public static final int TAG_SUBJECT_AREA = 0x9214;
public static final int TAG_FLASH_ENERGY = 0xa20b;
public static final int TAG_SPATIAL_FREQUENCY_RESPONSE = 0xa20c;
public static final int TAG_FOCAL_PLANE_X_RESOLUTION = 0xa20e;
public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = 0xa20f;
public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT = 0xa210;
public static final int TAG_SUBJECT_LOCATION = 0xA214;
public static final int TAG_EXPOSURE_INDEX = 0xA215;
public static final int TAG_SENSING_METHOD = 0xA217;
public static final int TAG_FILE_SOURCE = 0xA300;
public static final int TAG_SCENE_TYPE = 0xA301;
public static final int TAG_CFA_PATTERN = 0xA302;
public static final int TAG_CUSTOM_RENDERED = 0xA401;
public static final int TAG_EXPOSURE_MODE = 0xA402;
public static final int TAG_WHITE_BALANCE = 0xA403;
public static final int TAG_DIGITAL_ZOOM_RATIO = 0xA404;
public static final int TAG_FOCAL_LENGTH_35MM_FILM = 0xA405;
public static final int TAG_SCENE_CAPTURE_TYPE = 0xA406;
public static final int TAG_GAIN_CONTROL = 0xA407;
public static final int TAG_CONTRAST = 0xA408;
public static final int TAG_SATURATION = 0xA409;
public static final int TAG_SHARPNESS = 0xA40A;
public static final int TAG_DEVICE_SETTING_DESCRIPTION = 0xA40B;
public static final int TAG_SUBJECT_DISTANCE_RANGE = 0xA40C;
// IFD GPS tags
public static final int TAG_GPS_VERSION_ID = 0x0;
public static final int TAG_GPS_LATITUDE_REF = 0x1;
public static final int TAG_GPS_LATITUDE = 0x2;
public static final int TAG_GPS_LONGITUDE_REF = 0x3;
public static final int TAG_GPS_LONGITUDE = 0x4;
public static final int TAG_GPS_ALTITUDE_REF = 0x5;
public static final int TAG_GPS_ALTITUDE = 0x6;
public static final int TAG_GPS_TIME_STAMP = 0x7;
public static final int TAG_GPS_SATELITES = 0x8;
public static final int TAG_GPS_STATUS = 0x9;
public static final int TAG_GPS_MEASURE_MODE = 0xa;
public static final int TAG_GPS_DOP = 0xb;
public static final int TAG_GPS_SPEED_REF = 0xc;
public static final int TAG_GPS_SPEED = 0xd;
public static final int TAG_GPS_TRACK_REF = 0xe;
public static final int TAG_GPS_TRACK = 0xf;
public static final int TAG_GPS_SLMG_DIRECTION_REF = 0x10;
public static final int TAG_GPS_SLMG_DIRECTION = 0x11;
public static final int TAG_GPS_MAP_DATUM = 0x12;
public static final int TAG_GPS_DEST_LATITUDE_REF = 0x13;
public static final int TAG_GPS_DEST_LATITUDE = 0x14;
public static final int TAG_GPS_DEST_LONGITUDE_REF = 0x15;
public static final int TAG_GPS_DEST_LONGITUDE = 0x16;
public static final int TAG_GPS_DEST_BEARING_REF = 0x17;
public static final int TAG_GPS_DEST_BEARING = 0x18;
public static final int TAG_GPS_DEST_DISTANCE_REF = 0x19;
public static final int TAG_GPS_DEST_DISTANCE = 0x1a;
public static final int TAG_GPS_PROCESSING_METHOD = 0x1b;
public static final int TAG_GPS_AREA_INFORMATION = 0x1c;
public static final int TAG_GPS_DATE_STAMP = 0x1d;
public static final int TAG_GPS_DIFFERENTIAL = 0x1e;
// IFD Interoperability tags
public static final int TAG_INTEROPERABILITY_1 = 0x1;
public static final int TAG_INTEROPERABILITY_2 = 0x2;
// Length of Exif data size declaration - 2B
private final int LENGTH_EXIF_SIZE_DECL = 2;
private final int LENGTH_APP1_EXIF_HEADER = 10; // APP1Marker+EXIF
// size
// +
// EXIF
// header
private String sourceFile;
private byte[] origEXIFdata;
// JPEG's Start of image
private final byte[] SOI = new byte[] { (byte) 0xFF, (byte) 0xD8 };
private final byte[] APP1Marker = new byte[] { (byte) 0xFF, (byte) 0xE1 };
private final byte[] EXIFHeader = new byte[] { 'E', 'x', 'i', 'f', '\0',
'\0' };
// Note it's a "Intel fashion one" we use it's values only during saving
private final byte[] TIFFHeader = new byte[] { 'I', 'I', (byte) 0x2A, '\0',
(byte) 0x08, '\0', '\0', '\0' };
// Specification requires this tag to have this value
private int origAPP1MarkerOffset = 2;
private int origThumbnailOffset = -1;
private int origThumbnailLength = 0;
public static final int ALIGN_II = 0x4949; // Intel
// endian
public static final int ALIGN_MM = 0x4D4D; // Motorola
// endian
private int originalAlign; // endian
// IFD directories are represented as simple tag-value hashes
private HashMap<Integer, ExifValue> ifd0 = new HashMap<Integer, ExifValue>();
private HashMap<Integer, ExifValue> ifdExif = new HashMap<Integer, ExifValue>();
private HashMap<Integer, ExifValue> ifdGps = new HashMap<Integer, ExifValue>();
private HashMap<Integer, ExifValue> ifd1 = new HashMap<Integer, ExifValue>();
private HashMap<Integer, ExifValue> ifdIOper = new HashMap<Integer, ExifValue>();
private boolean readyToWork = false;
private boolean debug = true;
/**
* Get instance of driver for given image file. If everything works well
* (file can be read, it is Exif file .. etc.), this method reurns the
* driver object. In opposite case it reurns null.
*
* @param _file
* image file
* @return ExifDriver or null in case, that anyhing went wrong
*/
public static ExifDriver getInstance(String _file)
{
ExifDriver result = new ExifDriver(_file);
if (result.readyToWork())
{
return result;
} else
{
return null;
}
}
public String getSourceFile()
{
return sourceFile;
}
/**
* Constructor. Do the basics like find Exif data and prepare them to array.
* Then call parser to read information from the array. Variable readyToWork
* is set during consturction. Constructor is private and can be called only
* through getInstance method, which desides, if user can obtain he driver
* object or null.
*
* @param _file
* Path of file to work with
*/
private ExifDriver(String _file)
{
sourceFile = _file;
readyToWork = true; // Hope for the best;
byte[] findBuffer = new byte[100];
FileInputStream fis = null;
FileChannel channel = null;
origAPP1MarkerOffset = -1;
int exifDataSize = -1;
int read;
try
{
fis = new FileInputStream(sourceFile);
channel = fis.getChannel();
read = fis.read(findBuffer);
// Make sure, that image is JPG
if (findBuffer[0] == SOI[0] && findBuffer[1] == SOI[1])
{
// Make sure, that image is the Exif one. Find APP1 marker and
// also remember it's offset from start of file
int findOffset = 0;
while (origAPP1MarkerOffset < SOI.length && read > LENGTH_APP1_EXIF_HEADER)
{
for (int i = 0; i - 1 < read - LENGTH_APP1_EXIF_HEADER; i++)
{
if (findBuffer[i] == APP1Marker[0] && findBuffer[i + 1] == APP1Marker[1]
&& findBuffer[i + 4] == EXIFHeader[0] && findBuffer[i + 5] == EXIFHeader[1]
&& findBuffer[i + 6] == EXIFHeader[2] && findBuffer[i + 7] == EXIFHeader[3]
&& findBuffer[i + 8] == EXIFHeader[4] && findBuffer[i + 9] == EXIFHeader[5])
{
origAPP1MarkerOffset = i + findOffset; // APP1marker
// and EXIF
// header
// found
exifDataSize = (findBuffer[i + 2] & 0xFF) << 8;
exifDataSize += findBuffer[i + 3] & 0xFF;
break;
}
}
// shift the finding window
findOffset += read - LENGTH_APP1_EXIF_HEADER;
channel.position(findOffset);
read = fis.read(findBuffer);
}
if (origAPP1MarkerOffset >= SOI.length)
{
origEXIFdata = new byte[exifDataSize - (LENGTH_EXIF_SIZE_DECL + EXIFHeader.length)];
System.out.println("APP1 data size: " + Long.toHexString(exifDataSize));
// data will start with TIFF header
channel.position(origAPP1MarkerOffset + LENGTH_APP1_EXIF_HEADER);
fis.read(origEXIFdata);
readExifData(origEXIFdata);
} else
{
readyToWork = false;
}
} else
{
readyToWork = false;
}
channel.close();
fis.close();
} catch (EOFException e)
{
e.printStackTrace();
readyToWork = false;
} catch (Exception e)
{
e.printStackTrace();
readyToWork = false;
} finally
{
try
{
if (channel != null)
{
channel.close();
}
if (fis != null)
{
fis.close();
}
} catch (IOException ex)
{
Logger.getLogger(ExifDriver.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
/**
* Tells the caller if the driver has been initialized corectly and we can
* work with it. It is used by getInstance method. In case, that readyToWork
* returns false, getInstance returns null
*
* @return true if driver has been initialized correctly
*/
private boolean readyToWork()
{
return readyToWork;
}
/**
* Method reads sequentially the given source data and fills the structures
* with information.
*
* @param _data
* @throws UndefinedValueAccessException
* @throws Exception
*/
private void readExifData(byte[] _data) throws UndefinedValueAccessException
{
originalAlign = (_data[0] << 8) + (_data[1] & 0xFF);
int ifdStart = readUInt(_data, 4, 4, originalAlign); // See the TIFF
// header
ifdStart = readIfd(ifd0, _data, ifdStart);
// Was there any IFD1 reference?
// Check pointer value is less then data.length, because sometimes
// it happens to be more. And it's cause exception.
// Actually problem is caused by bugs in android's ExifEnterface.
if (ifdStart > 0 && ifdStart < _data.length)
{
readIfd(ifd1, _data, ifdStart);
// Remember thumbnail info
if (ifd1.containsKey(TAG_JPEG_INTERCHANGE_FORMAT))
{
origThumbnailOffset = ifd1.get(TAG_JPEG_INTERCHANGE_FORMAT).getIntegers()[0];
origThumbnailLength = ifd1.get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH).getIntegers()[0];
}
}
// Is there a IFDExif reference?
if (ifd0.get(TAG_EXIF_POINTER) != null)
{
ifdStart = ifd0.get(TAG_EXIF_POINTER).getIntegers()[0];
readIfd(ifdExif, _data, ifdStart);
if (ifdExif.get(TAG_INTEROPERABILITY_POINTER) != null)
{
ifdStart = ifdExif.get(TAG_INTEROPERABILITY_POINTER).getIntegers()[0];
readIfd(ifdIOper, _data, ifdStart);
}
}
// Is there a IFDGPS reference?
if (ifd0.get(TAG_GPS_POINTER) != null)
{
ifdStart = ifd0.get(TAG_GPS_POINTER).getIntegers()[0];
readIfd(ifdGps, _data, ifdStart);
}
}
/**
* Read information from one IFD directory
*
* @param _ifd
* Structure to store data in
* @param _data
* Source data byte array
* @param _start
* Offset, where the ifd directory starts
* @return Long value from the last 4 bytes of the directory, which, if
* non-zero, refers to next ifd. As a matter of fact, the non-zero
* is expected only in IFD0.
* @throws Exception
*/
private int readIfd(HashMap<Integer, ExifValue> _ifd, byte[] _data, int _start)
{
int entriesNumber = readUInt(_data, _start, 2, originalAlign);
if (debug)
{
System.out.println(entriesNumber + " entries found in ifd begining at "
+ Integer.toHexString(origAPP1MarkerOffset + LENGTH_APP1_EXIF_HEADER + _start));
}
for (int i = 0; i < entriesNumber; i++)
{
int entryStart = _start + 2 + i * 12;// 2B is size, 12B is the
// length of each item
// Parse item structure (2B-tag, 2B-datatype, 4B number of
// components, 4B value(or offset to value))
int tag = readUInt(_data, entryStart, 2, originalAlign);
int datatype = readUInt(_data, entryStart + 2, 2, originalAlign);
int components = readUInt(_data, entryStart + 4, 4, originalAlign);
// If the totalLength is >4 it does not fit in directory
int totalLength = 0;
Object dType = COMP_WIDTHS[0];
if(datatype > COMP_WIDTHS.length)
{
Log.e("EXIF_TAG", "Error in exifDriver. Tag " + tag + ", datatype = " + datatype);
if(tag == 274)
dType = COMP_WIDTHS[3];
}
else
dType = COMP_WIDTHS[datatype];
if (dType != null)
{
totalLength = components * (Integer) dType;
} else
{
_ifd.clear();
return 0;
}
// Offset right in directory
int offset = entryStart + 8;
if (totalLength > 4)
{
// Offset in data area
offset = readUInt(_data, offset, 4, originalAlign);
}
if (debug)
{
if (tag == TAG_INTEROPERABILITY_POINTER || tag == TAG_EXIF_POINTER || tag == TAG_GPS_POINTER)
{
System.out.print("Pointer: " + tag);
System.out.print(" Datatype: " + Integer.toHexString(datatype));
System.out.print(" Components: " + Integer.toHexString(components));
System.out.println(" Offset: "
+ Integer.toHexString(origAPP1MarkerOffset + LENGTH_APP1_EXIF_HEADER + offset));
}
}
ExifValue value = null;
switch (datatype)
{
case FORMAT_UNSIGNED_BYTE:
case FORMAT_UNSIGNED_SHORT:
case FORMAT_UNSIGNED_LONG:
case FORMAT_SIGNED_BYTE:
case FORMAT_SIGNED_SHORT:
case FORMAT_SIGNED_LONG:
value = new ValueNumber(datatype);
break;
case FORMAT_ASCII_STRINGS:
case FORMAT_UNDEFINED:
value = new ValueByteArray(datatype);
break;
case FORMAT_UNSIGNED_RATIONAL:
case FORMAT_SIGNED_RATIONAL:
value = new ValueRationals(datatype);
break;
default:
break;
}
if (value != null)
{
value.readValueFromData(_data, offset, components, originalAlign);
_ifd.put(tag, value);
}
}
// Return the long value represented by 4B after the last entry
return readUInt(_data, _start + entriesNumber * 12 + 2, 4, originalAlign);
}
/**
* Read signed int from source byte array. Handles endianes.
*
* @param _data
* Data array to read from
* @param _offset
* offset, where the value starts
* @param _bytesNumber
* Number of bytes (1,2,4 for byte, short, long)
* @param _align
* - Endian
* @return Integer value
*/
public static Integer readSInt(byte[] _data, int _offset, int _bytesNumber, int _align)
{
int signMask = 1 << (_bytesNumber * 8 - 1);
int valueMask = 0xFFFFFFFF >>> (4 - _bytesNumber) * 8 + 1;
int value = readUInt(_data, _offset, _bytesNumber, _align) & valueMask;
if ((value & signMask) > 0)
{
value = -value;
}
return value;
}
/**
* Read unsigned int from source byte array. Handles endianes.
*
* @param _data
* Data array to read from
* @param _offset
* offset, where the value starts
* @param _bytesNumber
* Number of bytes (1,2,4 for byte, short, long)
* @param _align
* - Endian
* @return Integer value
*/
public static Integer readUInt(byte[] _data, int _offset, int _bytesNumber, int _align)
{
int result = 0;
int shift = 0;
switch (_align)
{
case ALIGN_MM:
shift = _bytesNumber * 8;
break;
case ALIGN_II:
shift = 0;
break;
default:
break;
}
for (int i = _offset; i < _bytesNumber + _offset; i++)
{
switch (_align)
{
case ALIGN_MM:
shift -= 8;
result += (_data[i] & 0xFF) << shift;
break;
case ALIGN_II:
result += (_data[i] & 0xFF) << shift;
shift += 8;
break;
default:
break;
}
}
return result;
}
/**
* The total space, that the given IFD requires. It is space required by
* directory itself and it's related data too.
*
* @param _ifd
* Given IFD
* @return Required space in bytes
*/
private int requiredSpace(HashMap<Integer, ExifValue> _ifd)
{
int result = 6;// 2B number of items, 4B the "next" address
Object[] oKeys = _ifd.keySet().toArray();
for (int i = 0; i < oKeys.length; i++)
{
result += 12;
ExifValue val = _ifd.get((Integer) oKeys[i]);
if (val != null)
{
result += val.getExtraSize();
}
}
return result;
}
/**
* Write one "integer" number to output byte array. The method uses Intel
* endian.
*
* @param _data
* Output byte array
* @param _offset
* offset, where the number will written
* @param _value
* numeric value of the namber
* @param _width
* number of bytes, the number covers
*/
public static void writeNumber(byte[] _data, int _offset, int _value, int _width)
{
int mask = 0xFF;
for (int i = 0; i < _width; i++)
{
_data[_offset + i] = (byte) ((_value & (mask << i * 8)) >>> (i * 8));
}
}
/**
* Write the specified IFD into given output byte array. The method writes
* to array the directory itself, same as all the "extra" values to
* directory-related area.
*
* @param _data
* Output byte array
* @param _ifd
* the directory to write
* @param _offset
* Offset in the byte array, where the data goes
* @param _nextOffset
* Value, which will be written to the "next" area of the
* directory. In fact only in case of IFD0 it will be a nonzero.
*/
private void writeIfd(byte[] _data, HashMap<Integer, ExifValue> _ifd, int _offset, int _nextOffset)
{
Object[] oKeys = _ifd.keySet().toArray();
int valuesOffset = _offset + 2 + oKeys.length * 12 + 4;
writeNumber(_data, _offset, oKeys.length, 2);
int itemOffset = _offset + 2;
Arrays.sort(oKeys);
for (int i = 0; i < oKeys.length; i++)
{
Integer key = (Integer) oKeys[i];
writeNumber(_data, itemOffset, key, 2);
valuesOffset = _ifd.get(key).writeToData(_data, itemOffset, valuesOffset);
itemOffset += 12;
}
writeNumber(_data, itemOffset, _nextOffset, 4);
}
/**
* Saves new image file with current Exif information. It is quite expensive
* operation, so it is recomended to call it only at the end of work.
*
* @param _name
* name of the new file
*/
public void save(String _name)
{
// Write empty directory referencies to calculate size of dirs
ValueNumber val = new ValueNumber(FORMAT_UNSIGNED_LONG, 0);
ifd0.put(TAG_EXIF_POINTER, val);
val = new ValueNumber(FORMAT_UNSIGNED_LONG, 0);
ifd0.put(TAG_GPS_POINTER, val);
val = new ValueNumber(FORMAT_UNSIGNED_LONG, 0);
ifdExif.put(TAG_INTEROPERABILITY_POINTER, val);
// Adjust referencies to image data
val = new ValueNumber(FORMAT_UNSIGNED_LONG, 0);
ifd1.put(TAG_JPEG_INTERCHANGE_FORMAT, val);
int startOfIfd0 = TIFFHeader.length;
int startOfIfdExif = startOfIfd0 + requiredSpace(ifd0);
int startOfIfdIOper = startOfIfdExif + requiredSpace(ifdExif);
int startOfIfdGps = startOfIfdIOper + requiredSpace(ifdIOper);
int startOfIfd1 = startOfIfdGps + requiredSpace(ifdGps);
int startOfThumbnail = startOfIfd1 + requiredSpace(ifd1);
int reqSize = startOfThumbnail + origThumbnailLength;
if (origThumbnailOffset == -1)
{
reqSize = startOfIfd1;
}
// Write directory referencies
val = new ValueNumber(FORMAT_UNSIGNED_LONG, startOfIfdExif);
ifd0.put(TAG_EXIF_POINTER, val);
val = new ValueNumber(FORMAT_UNSIGNED_LONG, startOfIfdGps);
ifd0.put(TAG_GPS_POINTER, val);
val = new ValueNumber(FORMAT_UNSIGNED_LONG, startOfIfdIOper);
ifdExif.put(TAG_INTEROPERABILITY_POINTER, val);
// Adjust referencies to image data
val = new ValueNumber(FORMAT_UNSIGNED_LONG, startOfThumbnail);
ifd1.put(TAG_JPEG_INTERCHANGE_FORMAT, val);
// }
// Write all headers
byte[] resultExif = new byte[reqSize];
byte[] exifHeader = new byte[] { (byte) 0xFF, (byte) 0xE1, 0, 0, (byte) 0x45, (byte) 0x78, (byte) 0x69,
(byte) 0x66, 0, 0 };
exifHeader[2] = (byte) (((reqSize + 8) & 0xFF00) >> 8);
exifHeader[3] = (byte) ((reqSize + 8) & 0xFF);
// Note, we will always use Intel align
byte[] tiffHeader = new byte[] { 0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00 };
System.arraycopy(tiffHeader, 0, resultExif, 0, tiffHeader.length);
writeIfd(resultExif, ifd0, startOfIfd0, origThumbnailOffset == -1 ? 0 : startOfIfd1);
writeIfd(resultExif, ifdExif, startOfIfdExif, 0);
writeIfd(resultExif, ifdIOper, startOfIfdIOper, 0);
writeIfd(resultExif, ifdGps, startOfIfdGps, 0);
if (origThumbnailOffset != -1)
{
writeIfd(resultExif, ifd1, startOfIfd1, 0);
System.arraycopy(origEXIFdata, origThumbnailOffset, resultExif, startOfThumbnail, origThumbnailLength);
}
FileOutputStream fos = null;
FileInputStream fis = null;
try
{
fos = new FileOutputStream(_name);
fis = new FileInputStream(sourceFile);
fos.write((byte) 0xFF);
fos.write((byte) 0xD8);
fos.write(exifHeader);
fos.write(resultExif);
int skipped = 0;
int imageOffset = origAPP1MarkerOffset + APP1Marker.length + LENGTH_EXIF_SIZE_DECL + EXIFHeader.length
+ origEXIFdata.length;
while (skipped < imageOffset)
{
int skip = (int) fis.skip(imageOffset - skipped);
if (skip < 0)
{
fos.close();
throw (new IOException());
} else
{
skipped += skip;
}
}
byte[] buffer = new byte[10240];
int len;
while ((len = fis.read(buffer)) > 0)
{
fos.write(buffer, 0, len);
}
fis.close();
fos.close();
} catch (FileNotFoundException ex)
{
Logger.getLogger(ExifDriver.class.getName()).log(Level.SEVERE, null, ex);
} catch (IOException ex)
{
Logger.getLogger(ExifDriver.class.getName()).log(Level.SEVERE, null, ex);
} finally
{
try
{
fis.close();
fos.close();
} catch (IOException ex)
{
Logger.getLogger(ExifDriver.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
public HashMap<Integer, ExifValue> getIfd0()
{
return ifd0;
}
public HashMap<Integer, ExifValue> getIfdExif()
{
return ifdExif;
}
public HashMap<Integer, ExifValue> getIfdGps()
{
return ifdGps;
}
public HashMap<Integer, ExifValue> getIfd1()
{
return ifd1;
}
public HashMap<Integer, ExifValue> getIfdIOper()
{
return ifdIOper;
}
}