/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.image.metadata;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import com.lightcrafts.image.metadata.values.*;
import com.lightcrafts.utils.bytebuffer.ByteBufferUtil;
import com.lightcrafts.utils.Rational;
import static com.lightcrafts.image.metadata.EXIFConstants.*;
import static com.lightcrafts.image.metadata.EXIFTags.*;
import static com.lightcrafts.image.types.TIFFConstants.*;
/**
* An <code>EXIFEncoder</code> is used to encode EXIF metadata to a
* {@link ByteBuffer} containing the raw bytes of a JPEG APP1 segment.
*
* @author Paul J. Lucas [paul@lightcrafts.com]
*/
public final class EXIFEncoder {
////////// public /////////////////////////////////////////////////////////
/**
* Encode EXIF metadata to a {@link ByteBuffer} containing the raw bytes of
* a JPEG APP1 segment.
*
* @param metadata The {@link ImageMetadata} to encode.
* @param includeHeader If <code>true</code>, include the EXIF header.
* @return Returns Returns said {@link ByteBuffer}.
*/
public static ByteBuffer encode( ImageMetadata metadata,
boolean includeHeader ) {
final EXIFEncoder encoder = new EXIFEncoder( metadata, includeHeader );
return encoder.encode();
}
////////// private ////////////////////////////////////////////////////////
/**
* Construct an <code>EXIFEncoder</code>.
*
* @param metadata The {link ImageMetadata} to adapt.
* @param includeHeader If <code>true</code>, include the EXIF header.
*/
private EXIFEncoder( ImageMetadata metadata, boolean includeHeader ) {
m_metadata = metadata;
m_includeHeader = includeHeader;
}
/**
* Calculate the size of a directory if it were to be encoded inside of a
* JPEG image.
*
* @param dir The {@link ImageMetadataDirectory} to calculate the size of.
* @return Returns the size in bytes.
*/
private int calcDirSize( ImageMetadataDirectory dir ) {
int size = calcIFDSize( dir );
for ( Iterator<Map.Entry<Integer,ImageMetaValue>> i = dir.iterator();
i.hasNext(); ) {
final Map.Entry<Integer,ImageMetaValue> me = i.next();
final int tagID = me.getKey();
if ( skipTag( tagID ) ) {
size -= EXIF_IFD_ENTRY_SIZE;
continue;
}
final ImageMetaValue imValue = me.getValue();
int valueSize = calcValueSize( imValue );
valueSize += valueSize & 1; // ensure all sizes are even
if ( valueSize > TIFF_INLINE_VALUE_MAX_SIZE )
size += valueSize;
switch ( tagID ) {
case EXIF_GPS_IFD_POINTER:
final ImageMetadataDirectory gpsDir =
m_metadata.getDirectoryFor( GPSDirectory.class );
if ( gpsDir != null )
size += calcDirSize( gpsDir );
break;
case EXIF_IFD_POINTER:
final ImageMetadataDirectory subEXIFDir =
m_metadata.getDirectoryFor( SubEXIFDirectory.class );
if ( subEXIFDir != null )
size += calcDirSize( subEXIFDir );
break;
case EXIF_INTEROPERABILITY_POINTER:
// TODO
break;
case EXIF_MAKER_NOTE:
// TODO
break;
}
}
return size;
}
/**
* Calculate a delta for a directory's entry count. This is needed since
* we don't handle some tags so they must not be exported as part of the
* metadata. Hence, the count delta is the original count minus the number
* of tags we don't handle.
*
* @param dir The {@link ImageMetadataDirectory} to calculate the entry
* count detla of.
* @return Returns said size.
*/
private static int calcDirEntriesDelta( ImageMetadataDirectory dir ) {
int delta = 0;
for ( Iterator<Map.Entry<Integer,ImageMetaValue>> i = dir.iterator();
i.hasNext(); ) {
final Map.Entry<Integer,ImageMetaValue> me = i.next();
final int tagID = me.getKey();
if ( skipTag( tagID ) )
--delta;
}
return delta;
}
/**
* Calculate the size of a directory IFD without the size of the values
* longer than 4 bytes.
*
* @param dir The {@link ImageMetadataDirectory} to calculate the size of.
* @return Returns said size.
*/
private static int calcIFDSize( ImageMetadataDirectory dir ) {
return EXIF_SHORT_SIZE // number of entries
+ dir.size() * EXIF_IFD_ENTRY_SIZE
+ EXIF_INT_SIZE; // next directory offset
}
/**
* Calculate the encoded size of an {@link ImageMetaValue}.
*
* @param value The {@link ImageMetaValue} to calculate the size of.
* @return Returns said size in bytes.
*/
private static int calcValueSize( ImageMetaValue value ) {
int size = 0;
switch ( value.getType() ) {
case META_DATE:
//
// A date type is our invention and doesn't exist in EXIF
// metadata as a distinct type. Instead, dates are stored as
// strings.
//
case META_STRING:
for ( String s : value.getValues() ) {
try {
final byte[] b = s.getBytes( "ISO-8859-1" );
size += b.length + 1 /* for null */;
}
catch ( UnsupportedEncodingException e ) {
throw new IllegalStateException( e );
}
}
break;
case META_UNDEFINED:
final UndefinedMetaValue undefined = (UndefinedMetaValue)value;
size = undefined.getUndefinedValue().length;
break;
default:
size = value.getValueCount()
* EXIF_FIELD_SIZE[ value.getType().getTIFFConstant() ];
}
return size;
}
/**
* Encode EXIF metadata to a {@link ByteBuffer} containing the raw bytes of
* a JPEG APP1 segment.
*
* @return Returns Returns said {@link ByteBuffer}.
*/
private ByteBuffer encode() {
final ImageMetadataDirectory exifDir =
m_metadata.getDirectoryFor( EXIFDirectory.class );
return exifDir != null ? encodeEXIFDir( (EXIFDirectory)exifDir ) : null;
}
/**
* Encode an {@link EXIFDirectory}'s values into a {@link ByteBuffer}
* suitable for writing into a JPEG image file.
*
* @param dir The {@link EXIFDirectory} to encode.
* @return Returns said {@link ByteBuffer}.
*/
private ByteBuffer encodeEXIFDir( EXIFDirectory dir ) {
final int entriesDelta = calcDirEntriesDelta( dir );
m_nextEXIFBigValuePos =
calcIFDSize( dir ) + entriesDelta * EXIF_IFD_ENTRY_SIZE;
m_endPosPlus1 = calcDirSize( dir );
if ( m_includeHeader ) {
m_nextEXIFBigValuePos += EXIF_HEADER_SIZE;
m_endPosPlus1 += EXIF_HEADER_SIZE;
}
final ByteBuffer buf = ByteBuffer.allocate( m_endPosPlus1 );
final ByteOrder nativeOrder = ByteOrder.nativeOrder();
buf.order( nativeOrder );
if ( m_includeHeader ) {
ByteBufferUtil.put( buf, "Exif\0\0", "ASCII" );
buf.putShort(
nativeOrder == ByteOrder.BIG_ENDIAN ?
TIFF_BIG_ENDIAN : TIFF_LITTLE_ENDIAN
);
buf.putShort( TIFF_MAGIC_NUMBER );
buf.putInt( TIFF_HEADER_SIZE ); // 0th directory offset
}
encodeEXIFDir( dir, buf, entriesDelta );
return buf;
}
/**
* Encode an {@link EXIFDirectory}'s values into a {@link ByteBuffer}
* suitable for writing into a JPEG image file.
*
* @param dir The {@link ImageMetadataDirectory} to encode.
* @param buf The {@link ByteBuffer} to encode into.
* @param entriesDelta The detla to add to the directory's entry count, if
* any.
*/
private void encodeEXIFDir( ImageMetadataDirectory dir, ByteBuffer buf,
int entriesDelta ) {
buf.putShort( (short)(dir.size() + entriesDelta) );
//
// When written to a JPEG file, tag IDs must be in ascending order, so
// sort them.
//
final Integer[] tagIDs =
dir.getTagIDSet( false ).toArray( new Integer[]{ null } );
Arrays.sort( tagIDs );
for ( int tagID : tagIDs ) {
if ( skipTag( tagID ) )
continue;
buf.putShort( (short)tagID );
final ImageMetaValue imValue = dir.getValue( tagID );
int valueSize = calcValueSize( imValue );
switch ( imValue.getType() ) {
case META_DATE:
//
// A date type is our invention and doesn't exist in JPEG
// metadata as a distinct type. Instead, dates are stored
// as strings.
//
case META_STRING:
buf.putShort( EXIF_FIELD_TYPE_STRING );
buf.putInt( valueSize );
break;
case META_UNDEFINED:
buf.putShort( imValue.getType().getTIFFConstant() );
buf.putInt( valueSize );
break;
default:
buf.putShort( imValue.getType().getTIFFConstant() );
buf.putInt( imValue.getValueCount() );
}
valueSize += valueSize & 1; // ensure all sizes are even
int prevBufPos = 0;
if ( valueSize > TIFF_INLINE_VALUE_MAX_SIZE ) {
buf.putInt(
m_nextEXIFBigValuePos
- (m_includeHeader ? EXIF_HEADER_START_SIZE : 0)
);
prevBufPos = buf.position();
buf.position( m_nextEXIFBigValuePos );
m_nextEXIFBigValuePos += valueSize;
}
switch ( imValue.getType() ) {
case META_SBYTE:
case META_UBYTE: {
final long[] bytes =
((LongMetaValue)imValue).getLongValues();
for ( long b : bytes )
buf.put( (byte)(b & 0xFF) );
for ( int j = bytes.length; j < TIFF_INLINE_VALUE_MAX_SIZE;
++j )
buf.put( (byte)0 );
break;
}
case META_DATE:
case META_STRING: {
try {
final String[] strings = imValue.getValues();
final byte[] b = strings[0].getBytes( "ISO-8859-1" );
buf.put( b );
buf.put( (byte)0 );
for ( int j = b.length + 1 /* for null */;
j < TIFF_INLINE_VALUE_MAX_SIZE; ++j )
buf.put( (byte)0 );
}
catch ( UnsupportedEncodingException e ) {
throw new IllegalStateException( e );
}
break;
}
case META_SSHORT:
case META_USHORT: {
final long[] shorts =
((LongMetaValue)imValue).getLongValues();
for ( long s : shorts )
buf.putShort( (short)(s & 0xFFFF) );
if ( valueSize < TIFF_INLINE_VALUE_MAX_SIZE )
buf.putShort( (short)0 );
break;
}
case META_SLONG:
case META_ULONG: {
final long[] longs =
((LongMetaValue)imValue).getLongValues();
for ( long l : longs )
buf.putInt( (int)(l & 0x00000000FFFFFFFF) );
break;
}
case META_SRATIONAL:
case META_URATIONAL: {
final Rational[] rats =
((RationalMetaValue)imValue).getRationalValues();
for ( Rational r : rats ) {
buf.putInt( r.numerator() );
buf.putInt( r.denominator() );
}
break;
}
case META_FLOAT: {
final float[] floats =
((FloatMetaValue)imValue).getFloatValues();
for ( float f : floats )
buf.putFloat( f );
break;
}
case META_DOUBLE: {
final double[] doubles =
((DoubleMetaValue)imValue).getDoubleValues();
for ( double d : doubles )
buf.putDouble( d );
break;
}
case META_UNDEFINED: {
final byte[] bytes =
((UndefinedMetaValue)imValue).getUndefinedValue();
buf.put( bytes );
for ( int j = bytes.length; j < TIFF_INLINE_VALUE_MAX_SIZE;
++j )
buf.put( (byte)0 );
break;
}
default:
throw new IllegalStateException();
}
switch ( tagID ) {
case EXIF_GPS_IFD_POINTER:
encodeSubEXIFDir( GPSDirectory.class, buf );
break;
case EXIF_IFD_POINTER:
encodeSubEXIFDir( SubEXIFDirectory.class, buf );
break;
case EXIF_INTEROPERABILITY_POINTER:
// TODO
break;
case EXIF_MAKER_NOTE:
// TODO
break;
}
if ( valueSize > TIFF_INLINE_VALUE_MAX_SIZE )
buf.position( prevBufPos );
}
buf.putInt( 0 ); // next directory offset
}
/**
* Encode an {@link EXIFDirectory}'s values for a subdirectory into a
* {@link ByteBuffer}.
*
* @param dirClass The {@link Class} of the {@link ImageMetadataDirectory}
* to encode.
* @param buf The {@link ByteBuffer} to use.
*/
private void encodeSubEXIFDir(
Class<? extends ImageMetadataDirectory> dirClass, ByteBuffer buf )
{
final ImageMetadataDirectory subDir =
m_metadata.getDirectoryFor( dirClass );
//
// Subdirectories are encoded at the end of the buffer working
// backwards.
//
m_endPosPlus1 -= calcDirSize( subDir );
//
// Back up and overwrite the subIFD's offset value that is the original
// offset from the original image file since that value is now garbage.
//
buf.position( buf.position() - EXIF_INT_SIZE );
buf.putInt(
m_endPosPlus1 - (m_includeHeader ? EXIF_HEADER_START_SIZE : 0)
);
//
// Remember the buffer position for the parent directory.
//
final int prevBufPos = buf.position();
buf.position( m_endPosPlus1 );
//
// Remember the old m_nextEXIFBigValuePos and compute a new one for the
// subdirectory.
//
final int origNextEXIFBigValuePos = m_nextEXIFBigValuePos;
final int entriesDelta = calcDirEntriesDelta( subDir );
m_nextEXIFBigValuePos =
m_endPosPlus1 + calcIFDSize( subDir )
+ entriesDelta * EXIF_IFD_ENTRY_SIZE;
encodeEXIFDir( subDir, buf, entriesDelta );
//
// Put everything back the way it was.
//
buf.position( prevBufPos );
m_nextEXIFBigValuePos = origNextEXIFBigValuePos;
}
/**
* Checks whether the given tag should be skipped.
*
* @param tagID The tag ID to check.
* @return Returns <code>true</code> only if the tag should be skipped.
*/
private static boolean skipTag( int tagID ) {
switch ( tagID ) {
case EXIF_COLOR_SPACE:
//
// We don't know what the color profile being used is, so
// we can't know what this value should be. Therefore,
// just skip it.
//
case EXIF_INTEROPERABILITY_POINTER:
//
// We don't know what to do with this yet.
//
case EXIF_MAKER_NOTE:
//
// Doing this is extremely difficult.
//
return true;
default:
return false;
}
}
/**
* Initially, this is the position one past the end of the size of the
* {@link ByteBuffer} that EXIF metadata is being encoded to. When a
* subdirectory is encountered, this value is decremented by the size of
* the subdirectory and the subdirectory is encoded starting at the new
* value. Hence, subdirectories are encoded at the end of the buffer
* working backwards.
*/
private int m_endPosPlus1;
/**
* Whether to include the EXIF header in the encoding.
*/
private final boolean m_includeHeader;
/**
* The {@link ImageMetadata} to encode.
*/
private final ImageMetadata m_metadata;
/**
* EXIF metadata values larger than 4 bytes are stored seperately past all
* the directory entries. This is used to keep track of the next available
* position for such a value.
*/
private int m_nextEXIFBigValuePos;
}
/* vim:set et sw=4 ts=4: */