/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.image.metadata;
import java.io.*;
import java.util.*;
import org.w3c.dom.Element;
import org.w3c.dom.Document;
import com.lightcrafts.image.metadata.providers.*;
import com.lightcrafts.image.metadata.values.*;
import com.lightcrafts.image.ImageInfo;
import com.lightcrafts.image.metadata.makernotes.MakerNotesDirectory;
import com.lightcrafts.image.types.ImageType;
import com.lightcrafts.image.types.JPEGImageType;
import com.lightcrafts.image.types.TIFFImageType;
import com.lightcrafts.utils.CollectionUtil;
import com.lightcrafts.utils.LightCraftsException;
import com.lightcrafts.utils.Version;
import com.lightcrafts.utils.xml.XMLUtil;
import static com.lightcrafts.image.metadata.CoreTags.*;
import static com.lightcrafts.image.metadata.EXIFConstants.*;
import static com.lightcrafts.image.metadata.EXIFTags.*;
import static com.lightcrafts.image.metadata.ImageOrientation.*;
import static com.lightcrafts.image.metadata.IPTCTags.*;
import static com.lightcrafts.image.metadata.TIFFTags.*;
import static com.lightcrafts.image.metadata.XMPConstants.XMP_DC_NS;
import static com.lightcrafts.image.metadata.XMPConstants.XMP_DC_PREFIX;
/**
* <code>ImageMetadata</code> contains one zero or more "directories"
* (EXIF, IPTC, etc.) of metadata for an image.
*
* @author Paul J. Lucas [paul@lightcrafts.com]
*/
public class ImageMetadata implements
ApertureProvider, BitsPerChannelProvider, CaptionProvider,
CaptureDateTimeProvider, Cloneable, ColorTemperatureProvider,
CopyrightProvider, Externalizable, FileDateTimeProvider, FlashProvider,
FocalLengthProvider, ISOProvider, LensProvider, MakeModelProvider,
OrientationProvider, OriginalWidthHeightProvider, RatingProvider,
ResolutionProvider, ShutterSpeedProvider, TitleProvider,
WidthHeightProvider {
////////// public /////////////////////////////////////////////////////////
/**
* Construct a new <code>ImageMetadata</code>.
*/
public ImageMetadata() {
// do nothing
}
/**
* Construct a new <code>ImageMetadata</code>.
*
* @param imageType The {@link ImageType} of the image this metadata is
* for.
*/
public ImageMetadata( ImageType imageType ) {
m_imageType = imageType;
}
/**
* Clear all metadata.
*/
public void clear() {
m_classToDirMap.clear();
}
/**
* Clears the edited flag for all {@link ImageMetaValue}s.
*/
public void clearEdited() {
for ( ImageMetadataDirectory dir : m_classToDirMap.values() )
dir.clearEdited();
}
/**
* Clears the rating of the image.
*
* @see #getRating()
* @see #setRating(int)
*/
public void clearRating() {
//removeValues( CoreDirectory.class, CORE_RATING );
setRating( 0 );
}
/**
* Perform a deep clone of this object.
*
* @return Returns said clone.
*/
@SuppressWarnings({"CloneDoesntDeclareCloneNotSupportedException"})
public Object clone() {
final ImageMetadata copy = new ImageMetadata( getImageType() );
for ( Map.Entry<Class,ImageMetadataDirectory> me
: m_classToDirMap.entrySet() ) {
final Class dirClass = me.getKey();
final ImageMetadataDirectory dir = me.getValue();
copy.m_classToDirMap.put( dirClass, dir.clone() );
}
return copy;
}
/**
* Compares this <code>ImageMetadata</code> to another for equality. Two
* <code>ImageMetadata</code> objects are considered equal if their paths
* are equal.
* <p>
* Note: I don't consider this the correct test for equality, but this is
* what Tom did and now other code relies upon it.
*
* @param object The {@link Object} to compare to.
* @return Returns <code>true</code> only if the other object is also an
* <code>ImageMetadata</code> and the two objects are equal.
* @see #hashCode()
*/
public boolean equals( Object object ) {
if ( object == this )
return true;
if ( object instanceof ImageMetadata ) {
final ImageMetadata thatMD = (ImageMetadata)object;
final String thisPath = getPath();
final String thatPath = thatMD.getPath();
return thisPath == null ? thatPath == null :
thisPath.equals( thatPath );
}
return false;
}
/**
* Find an {@link ImageMetadataDirectory} that implements the given
* provider interface.
*
* @param provider The provider interface to find.
* @return Returns an {@link ImageMetadataDirectory} that implements the
* given provider interface or <code>null</code> if none is found.
* @see #findProvidersOf(Class)
*/
public ImageMetadataDirectory findProviderOf( Class provider ) {
for ( ImageMetadataDirectory dir : getDirectories() )
if ( provider.isInstance( dir ) )
return dir;
return null;
}
/**
* Find all instances of {@link ImageMetadataDirectory} that implement the
* given provider interface.
*
* @param provider The provider interface to find.
* @return Returns an ordered {@link Collection} of all instances of
* {@link ImageMetadataDirectory} that implement the given provider
* interface.
* @see #findProviderOf(Class)
*/
public Collection<ImageMetadataDirectory>
findProvidersOf( Class<? extends ImageMetadataProvider> provider ) {
final ArrayList<ImageMetadataDirectory> providers =
new ArrayList<ImageMetadataDirectory>();
for ( ImageMetadataDirectory dir : getDirectories() )
if ( provider.isInstance( dir ) )
providers.add( dir );
Collections.sort( providers, new ProviderComparator( provider ) );
return providers;
}
/**
* {@inheritDoc}
*/
public float getAperture() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( ApertureProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final float value = ((ApertureProvider)dir).getAperture();
if ( value > 0 )
return value;
}
return 0;
}
/**
* {@inheritDoc}
*/
public String getArtist() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( ArtistProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final String value = ((ArtistProvider)dir).getArtist();
if ( value != null )
return value;
}
return null;
}
/**
* {@inheritDoc}
*/
public int getBitsPerChannel() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( BitsPerChannelProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int value = ((BitsPerChannelProvider)dir).getBitsPerChannel();
if ( value > 0 )
return value;
}
return 0;
}
/**
* Gets the make and model of the camera used.
*
* @param includeModel If <code>true</code>, the model is included.
* @return Returns the make (and possibly model) converted to uppercase and
* seperated by a space or <code>null</code> if not available.
*/
public final String getCameraMake( boolean includeModel ) {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( MakeModelProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final String make =
((MakeModelProvider)dir).getCameraMake( includeModel );
if ( make != null )
return make;
}
return null;
}
/**
* {@inheritDoc}
*/
public String getCaption() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( CaptionProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final String value = ((CaptionProvider)dir).getCaption();
if ( value != null )
return value;
}
return null;
}
/**
* {@inheritDoc}
*/
public Date getCaptureDateTime() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( CaptureDateTimeProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final Date date =
((CaptureDateTimeProvider)dir).getCaptureDateTime();
if ( date != null )
return date;
}
return null;
}
/**
* {@inheritDoc}
*/
public int getColorTemperature() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( ColorTemperatureProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int temp =
((ColorTemperatureProvider)dir).getColorTemperature();
if ( temp > 0 )
return temp;
}
return 0;
}
/**
* {@inheritDoc}
*/
public String getCopyright() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( CopyrightProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final String value = ((CopyrightProvider)dir).getCopyright();
if ( value != null )
return value;
}
return null;
}
/**
* Gets all the directories (if any) of metadata.
*
* @return Returns a {@link Collection} of said directories.
*/
public Collection<ImageMetadataDirectory> getDirectories() {
return m_classToDirMap.values();
}
/**
* Gets the {@link ImageMetadataDirectory} instance for a given
* {@link ImageMetadataDirectory} {@link Class}.
*
* @param dirClass The {@link Class} of a class derived from
* {@link ImageMetadataDirectory}.
* @return Returns the {@link ImageMetadataDirectory} for the given
* {@link Class} if found; <code>null</code> otherwise.
*/
public ImageMetadataDirectory getDirectoryFor(
Class<? extends ImageMetadataDirectory> dirClass )
{
return getDirectoryFor( dirClass, false );
}
/**
* Gets the {@link ImageMetadataDirectory} instance for a given
* {@link ImageMetadataDirectory} {@link Class}.
*
* @param dirClass The sought {@link Class} of a class derived from
* {@link ImageMetadataDirectory}.
* @param create If <code>true</code>, creates the
* {@link ImageMetadataDirectory} if it doesn't exist.
* @return Returns the {@link ImageMetadataDirectory} for the given
* {@link Class} if found or created; <code>null</code> otherwise.
*/
public ImageMetadataDirectory getDirectoryFor(
Class<? extends ImageMetadataDirectory> dirClass, boolean create )
{
synchronized ( m_classToDirMap ) {
ImageMetadataDirectory dir = m_classToDirMap.get( dirClass );
if ( dir == null && create ) {
try {
dir = dirClass.newInstance();
dir.setOwningMetadata( this );
}
catch ( Exception e ) {
throw new IllegalStateException( e );
}
m_classToDirMap.put( dirClass, dir );
}
return dir;
}
}
/**
* Gets the absolute {@link File} of the image.
*
* @return Returns said {@link File} or <code>null</code> if unavailable.
* @see #getPath()
*/
public File getFile() {
final String path = getPath();
return path != null ? new File( path ) : null;
}
/**
* {@inheritDoc}
*/
public Date getFileDateTime() {
final CoreDirectory dir =
(CoreDirectory)getDirectoryFor( CoreDirectory.class );
return dir != null ? dir.getFileDateTime() : null;
}
/**
* {@inheritDoc}
*/
public String getFlash() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( FlashProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final String flash = ((FlashProvider)dir).getFlash();
if ( flash != null )
return flash;
}
return null;
}
/**
* {@inheritDoc}
*/
public float getFocalLength() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( FocalLengthProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final float value = ((FocalLengthProvider)dir).getFocalLength();
if ( value > 0 )
return value;
}
return 0;
}
/**
* {@inheritDoc}
*/
public int getImageHeight() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( WidthHeightProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int height = ((WidthHeightProvider)dir).getImageHeight();
if ( height > 0 )
return height;
}
return 0;
}
/**
* Gets the {@link ImageType} of the image this metadata is for.
*
* @return Returns said type or <code>null</code> if it could not be
* determined.
*/
public synchronized ImageType getImageType() {
if ( m_imageType == null ) {
final File file = getFile();
if ( file != null ) {
final ImageInfo info = ImageInfo.getInstanceFor( file );
try {
m_imageType = info.getImageType();
}
catch ( IOException e ) {
// ignore
}
catch ( LightCraftsException e ) {
// ignore
}
}
}
return m_imageType;
}
/**
* {@inheritDoc}
*/
public int getImageWidth() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( WidthHeightProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int width = ((WidthHeightProvider)dir).getImageWidth();
if ( width > 0 )
return width;
}
return 0;
}
/**
* {@inheritDoc}
*/
public int getOriginalImageHeight() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( OriginalWidthHeightProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int height =
((OriginalWidthHeightProvider)dir).getOriginalImageHeight();
if ( height > 0 )
return height;
}
return 0;
}
/**
* {@inheritDoc}
*/
public int getOriginalImageWidth() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( OriginalWidthHeightProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int width =
((OriginalWidthHeightProvider)dir).getOriginalImageWidth();
if ( width > 0 )
return width;
}
return 0;
}
/**
* {@inheritDoc}
*/
public int getISO() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( ISOProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int iso = ((ISOProvider)dir).getISO();
if ( iso > 0 )
return iso;
}
return 0;
}
/**
* {@inheritDoc}
*/
public String getLens() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( LensProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final String lens = ((LensProvider)dir).getLens();
if ( lens != null )
return lens;
}
return null;
}
/**
* {@inheritDoc}
*/
public ImageOrientation getOrientation() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( OrientationProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final ImageOrientation orientation =
((OrientationProvider)dir).getOrientation();
if ( orientation != ORIENTATION_UNKNOWN )
return orientation;
}
return ORIENTATION_UNKNOWN;
}
/**
* Gets the original orientation of an image, that is the orientation
* embedded in its own metadata and not merged from any XMP file.
*
* @return Returns said orientation.
*/
public ImageOrientation getOriginalOrientation() {
final ImageMetaValue value =
getValue( CoreDirectory.class, CORE_ORIGINAL_ORIENTATION );
return value != null ?
ImageOrientation.getOrientationFor( value.getIntValue() ) :
ORIENTATION_LANDSCAPE;
}
/**
* Gets the absolute path to the original image file.
*
* @return Returns said path or <code>null</code> if unavailable.
* @see #getFile()
*/
public synchronized String getPath() {
final CoreDirectory coreDir =
(CoreDirectory)getDirectoryFor( CoreDirectory.class );
return coreDir != null ? coreDir.getPath() : null;
}
/**
* Gets the rating of this image.
*
* @return Returns said rating in the range 1-5 if it's rated, otherwise
* returns 0.
* @see #clearRating()
* @see #setRating(int)
*/
public int getRating() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( RatingProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int rating = ((RatingProvider)dir).getRating();
if ( rating != 0 )
return rating;
}
return 0;
}
/**
* {@inheritDoc}
*/
public double getResolution() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( ResolutionProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final double value = ((ResolutionProvider)dir).getResolution();
if ( value > 0 )
return value;
}
return 0;
}
/**
* {@inheritDoc}
*/
public int getResolutionUnit() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( ResolutionProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final int value = ((ResolutionProvider)dir).getResolutionUnit();
if ( value != RESOLUTION_UNIT_NONE )
return value;
}
return RESOLUTION_UNIT_NONE;
}
/**
* {@inheritDoc}
*/
public float getShutterSpeed() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( ShutterSpeedProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final float speed = ((ShutterSpeedProvider)dir).getShutterSpeed();
if ( speed > 0 )
return speed;
}
return 0;
}
/**
* {@inheritDoc}
*/
public String getTitle() {
final Collection<ImageMetadataDirectory> dirs =
findProvidersOf( TitleProvider.class );
for ( ImageMetadataDirectory dir : dirs ) {
final String value = ((TitleProvider)dir).getTitle();
if ( value != null )
return value;
}
return null;
}
/**
* Gets the value for a particular tag ID in a particular
* {@link ImageMetadataDirectory}. If the tag ID isn't found in the given
* directory, each directory along the static parent chain is checked
* also.
*
* @param dirClass The {@link Class} of a class derived from
* {@link ImageMetadataDirectory}.
* @param tagID The ID of the tag to get the value for.
* @return Returns the relevant {@link ImageMetaValue} or <code>null</code>
* if there is no value for the given tag ID in the given
* {@link ImageMetadataDirectory} or any static parent thereof.
* @see ImageMetadataDirectory#getStaticParent()
*/
public ImageMetaValue getValue(
Class<? extends ImageMetadataDirectory> dirClass, int tagID )
{
final ImageMetadataDirectory dir = getDirectoryFor( dirClass );
return dir != null ? dir.getValue( tagID ) : null;
}
/**
* Returns the hash code for this <code>ImageMetadata</code>.
* <p>
* Note: I don't consider using the path to be the correct way to compute
* the hash code, but this is what Tom did and now hother code relies upon
* it.
*
* @return Returns said hash code.
* @see #equals(Object)
*/
public int hashCode() {
final String path = getPath();
return path != null ? path.hashCode() : super.hashCode();
}
/**
* Returns whether there is some metadata.
*
* @return Returns <code>true</code> only if there is no metadata.
*/
public boolean isEmpty() {
return m_classToDirMap.isEmpty();
}
/**
* Merge the metadata from another {@link ImageMetadata} object into this
* one.
*
* @param fromMetadata The other {@link ImageMetadata} to merge from.
*/
public void mergeFrom( ImageMetadata fromMetadata ) {
for ( ImageMetadataDirectory fromDir : fromMetadata.getDirectories() ) {
final ImageMetadataDirectory toDir =
getDirectoryFor( fromDir.getClass(), true );
toDir.mergeFrom( fromDir );
}
//
// As a special case, if the "from" metadata has an orientation,
// replace the orientation in EXIF and TIFF directories, otherwise
// there will be inconsistent orientations.
//
final ImageOrientation orientation = fromMetadata.getOrientation();
if ( orientation != ORIENTATION_UNKNOWN ) {
final ImageMetaValue value =
new UnsignedShortMetaValue( orientation.getTIFFConstant() );
putValue( EXIFDirectory.class, EXIF_ORIENTATION, value, false );
putValue( TIFFDirectory.class, TIFF_ORIENTATION, value, false );
}
}
/**
* Prepare the EXIF, subEXIF, and TIFF directories for export. This means
* adding some fields, setting the relevant fields for the correct image
* width/height, and removing others that are no longer correct given the
* changes.
* <p>
* The original metadata is unchanged; instead, a modified clone is
* returned.
*
* @param exportImageType The type of image that will be exported to. It
* must be either {@link JPEGImageType#INSTANCE} or
* {@link TIFFImageType#INSTANCE}.
* @param includeOrientation If <code>true</code>, include orientation
* metadata.
* @return Returns the prepared <code>ImageMetadata</code>.
* @see #prepForExport(ImageType,int,int,int,int,boolean)
* @see #prepForXMP(boolean)
*/
public ImageMetadata prepForExport( ImageType exportImageType,
boolean includeOrientation ) {
final ImageMetaValue widthValue =
getValue( CoreDirectory.class, CORE_IMAGE_WIDTH );
final ImageMetaValue heightValue =
getValue( CoreDirectory.class, CORE_IMAGE_HEIGHT );
final int width = widthValue != null ? widthValue.getIntValue() : 0;
final int height = heightValue != null ? heightValue.getIntValue() : 0;
return prepForExport(
exportImageType, width, height,
(int)getResolution(), getResolutionUnit(), includeOrientation
);
}
/**
* Prepare the EXIF, subEXIF, and TIFF directories for export. This means
* adding some fields, setting the relevant fields for the correct image
* width/height, and removing others that are no longer correct given the
* changes.
* <p>
* The original metadata is unchanged; instead, a modified clone is
* returned.
*
* @param exportImageType The type of image that will be exported to. It
* must be either {@link JPEGImageType#INSTANCE} or
* {@link TIFFImageType#INSTANCE}.
* @param imageWidth The exported image width.
* @param imageHeight The exported image height.
* @param resolution The resolution (in pixels per unit).
* @param resolutionUnit The resolution unit; must be either
* {@link #RESOLUTION_UNIT_CM} or {@link #RESOLUTION_UNIT_INCH}.
* @param includeOrientation If <code>true</code>, include orientation
* metadata.
* @return Returns the prepared <code>ImageMetadata</code>.
* @see #prepForExport(ImageType,boolean)
* @see #prepForXMP(boolean)
*/
public ImageMetadata prepForExport( ImageType exportImageType,
int imageWidth, int imageHeight,
int resolution, int resolutionUnit,
boolean includeOrientation ) {
final boolean toJPEG = exportImageType == JPEGImageType.INSTANCE;
//
// We clone the metadata first because we don't want to modify the
// original.
//
final ImageMetadata metadata = (ImageMetadata)clone();
ImageMetadataDirectory tiffDir =
metadata.getDirectoryFor( TIFFDirectory.class );
final ImageMetadataDirectory ciffDir =
metadata.getDirectoryFor( CIFFDirectory.class );
if ( ciffDir != null ) {
//
// If there's CIFF metadata, convert it to TIFF/EXIF, then merge it
// into the existing metadata.
//
final ImageMetadata ciffMetadata =
((CIFFDirectory)ciffDir).convertMetadata( toJPEG );
metadata.mergeFrom( ciffMetadata );
metadata.removeDirectory( CIFFDirectory.class );
} else if ( tiffDir == null ) {
//
// There's no TIFF metadata; see if there's DNG metadata.
//
tiffDir = metadata.getDirectoryFor( DNGDirectory.class );
}
final ImageMetadataDirectory exifDir =
metadata.getDirectoryFor( EXIFDirectory.class, true );
if ( toJPEG ) {
//
// If a TIFF directory is present, move its EXIF-overlapping tag
// values to the EXIF directory.
//
if ( tiffDir != null )
ImageMetadataDirectory.moveValuesFromTo( tiffDir, exifDir );
//
// TIFF metadata inside JPEG files is actually in the EXIF metadata
// in the EXIF/TIFF-overlapping tags, so just point tiffDir at
// exifDir.
//
tiffDir = exifDir;
} else {
if ( tiffDir == null ) {
//
// There's still no TIFF metadata: create it.
//
tiffDir = metadata.getDirectoryFor( TIFFDirectory.class, true );
}
//
// EXIF metadata inside TIFF files likes to be in a single
// directory. If an EXIF subdirectory exists, move all of its
// values over to the main EXIF directory.
//
final ImageMetadataDirectory subEXIFDir =
metadata.getDirectoryFor( SubEXIFDirectory.class );
if ( subEXIFDir != null ) {
for ( Iterator<Map.Entry<Integer,ImageMetaValue>>
i = subEXIFDir.iterator(); i.hasNext(); ) {
final Map.Entry<Integer,ImageMetaValue> me = i.next();
exifDir.putValue( me.getKey(), me.getValue() );
}
metadata.removeDirectory( SubEXIFDirectory.class );
}
//
// Move those tags that are common between TIFF and EXIF metadata
// from the EXIF directory to the TIFF directory since, for a TIFF
// file, they *must* be there.
//
ImageMetadataDirectory.moveValuesFromTo( exifDir, tiffDir );
}
////////// Values that are always put.
exifDir.putValue(
EXIF_EXIF_VERSION,
new UndefinedMetaValue( EXIFDirectory.EXIF_VERSION )
);
tiffDir.putValue(
TIFF_HOST_COMPUTER,
new StringMetaValue(
System.getProperty( "os.name" ) + ' ' +
System.getProperty( "os.version" )
)
);
tiffDir.putValue(
TIFF_SOFTWARE, new StringMetaValue( Version.getApplicationName() )
);
////////// Orientation.
exifDir.removeValue( EXIF_ORIENTATION );
if ( includeOrientation )
tiffDir.putValue(
TIFF_ORIENTATION,
new UnsignedShortMetaValue(
metadata.getOrientation().getTIFFConstant()
)
);
////////// Rating.
exifDir.removeValue( EXIF_MS_RATING );
final int rating = getRating();
if ( rating > 0 )
tiffDir.putValue(
TIFF_MS_RATING, new UnsignedShortMetaValue( rating )
);
else
tiffDir.removeValue( TIFF_MS_RATING );
////////// Image width/height.
exifDir.removeValue( EXIF_PIXEL_X_DIMENSION );
exifDir.removeValue( EXIF_PIXEL_Y_DIMENSION );
if ( imageWidth > 0 && imageHeight > 0 ) {
UnsignedShortMetaValue widthValue =
new UnsignedShortMetaValue( imageWidth );
UnsignedShortMetaValue heightValue =
new UnsignedShortMetaValue( imageHeight );
tiffDir.putValue( TIFF_IMAGE_WIDTH, widthValue );
tiffDir.putValue( TIFF_IMAGE_LENGTH, heightValue );
if ( exifDir != tiffDir ) {
//
// We must create distinct values since these need to have an
// EXIFDirectory as their owner while not altering the owner of
// the values added to the TIFFDirectory.
//
widthValue = new UnsignedShortMetaValue( imageWidth );
heightValue = new UnsignedShortMetaValue( imageHeight );
exifDir.putValue( EXIF_IMAGE_WIDTH, widthValue );
exifDir.putValue( EXIF_IMAGE_HEIGHT, heightValue );
}
} else {
tiffDir.removeValue( TIFF_IMAGE_WIDTH );
tiffDir.removeValue( TIFF_IMAGE_LENGTH );
}
////////// Image resolution.
if ( resolution > 0 && resolutionUnit != RESOLUTION_UNIT_NONE ) {
final UnsignedRationalMetaValue xResolutionValue =
new UnsignedRationalMetaValue( resolution, 1 );
final UnsignedRationalMetaValue yResolutionValue =
new UnsignedRationalMetaValue( resolution, 1 );
tiffDir.putValue( TIFF_X_RESOLUTION, xResolutionValue );
tiffDir.putValue( TIFF_Y_RESOLUTION, yResolutionValue );
tiffDir.putValue(
TIFF_RESOLUTION_UNIT,
new UnsignedShortMetaValue( resolutionUnit )
);
}
////////// Copy overlapping IPTC metadata.
final ImageMetadataDirectory iptcDir =
metadata.getDirectoryFor( IPTCDirectory.class );
if ( iptcDir != null ) {
final int[][] iptcMap = {
{ IPTC_BY_LINE , TIFF_ARTIST },
{ IPTC_COPYRIGHT_NOTICE, TIFF_COPYRIGHT },
{ IPTC_OBJECT_NAME , TIFF_DOCUMENT_NAME },
{ IPTC_CAPTION_ABSTRACT, TIFF_IMAGE_DESCRIPTION }
};
for ( int[] tagIDs : iptcMap ) {
final ImageMetaValue value = iptcDir.getValue( tagIDs[0] );
if ( value != null ) {
//
// It's necessary to clone the value so each has the right
// owning directory and therefore will get the right tag
// name for XMP export.
//
tiffDir.putValue( tagIDs[1], value.clone() );
}
}
}
////////// Remove other metadata because it makes no sense to export.
exifDir.removeValue( EXIF_CFA_PATTERN );
exifDir.removeValue( EXIF_COMPONENTS_CONFIGURATION );
exifDir.removeValue( EXIF_COMPRESSED_BITS_PER_PIXEL );
exifDir.removeValue( EXIF_IFD_POINTER ); // re-added later if needed
exifDir.removeValue( EXIF_INTEROPERABILITY_POINTER );
exifDir.removeValue( EXIF_JPEG_INTERCHANGE_FORMAT );
exifDir.removeValue( EXIF_JPEG_INTERCHANGE_FORMAT_LENGTH );
exifDir.removeValue( EXIF_MAKER_NOTE );
exifDir.removeValue( EXIF_SPATIAL_FREQUENCY_RESPONSE );
exifDir.removeValue( EXIF_SUBJECT_AREA );
tiffDir.removeValue( TIFF_CELL_LENGTH );
tiffDir.removeValue( TIFF_CELL_WIDTH );
tiffDir.removeValue( TIFF_CLIP_PATH );
tiffDir.removeValue( TIFF_COLOR_MAP );
tiffDir.removeValue( TIFF_COMPRESSION );
tiffDir.removeValue( TIFF_DOT_RANGE );
tiffDir.removeValue( TIFF_EXTRA_SAMPLES );
tiffDir.removeValue( TIFF_FILL_ORDER );
tiffDir.removeValue( TIFF_FREE_BYTE_COUNTS );
tiffDir.removeValue( TIFF_FREE_OFFSETS );
tiffDir.removeValue( TIFF_GRAY_RESPONSE_CURVE );
tiffDir.removeValue( TIFF_GRAY_RESPONSE_UNIT );
tiffDir.removeValue( TIFF_HALFTONE_HINTS );
tiffDir.removeValue( TIFF_INDEXED );
tiffDir.removeValue( TIFF_JPEG_AC_TABLES );
tiffDir.removeValue( TIFF_JPEG_DC_TABLES );
tiffDir.removeValue( TIFF_JPEG_INTERCHANGE_FORMAT );
tiffDir.removeValue( TIFF_JPEG_INTERCHANGE_FORMAT_LENGTH );
tiffDir.removeValue( TIFF_JPEG_LOSSLESS_PREDICTORS );
tiffDir.removeValue( TIFF_JPEG_POINT_TRANSFORMS );
tiffDir.removeValue( TIFF_JPEG_PROC );
tiffDir.removeValue( TIFF_JPEG_Q_TABLES );
tiffDir.removeValue( TIFF_JPEG_RESTART_INTERVAL );
tiffDir.removeValue( TIFF_LIGHTZONE );
tiffDir.removeValue( TIFF_NEW_SUBFILE_TYPE );
tiffDir.removeValue( TIFF_OPI_PROXY );
tiffDir.removeValue( TIFF_PHOTOSHOP_IMAGE_RESOURCES );
tiffDir.removeValue( TIFF_PLANAR_CONFIGURATION );
tiffDir.removeValue( TIFF_PREDICTOR );
tiffDir.removeValue( TIFF_PRIMARY_CHROMATICITIES );
tiffDir.removeValue( TIFF_REFERENCE_BLACK_WHITE );
tiffDir.removeValue( TIFF_ROWS_PER_STRIP );
tiffDir.removeValue( TIFF_SAMPLE_FORMAT );
tiffDir.removeValue( TIFF_SAMPLES_PER_PIXEL );
tiffDir.removeValue( TIFF_STRIP_BYTE_COUNTS );
tiffDir.removeValue( TIFF_SUBFILE_TYPE );
tiffDir.removeValue( TIFF_STRIP_OFFSETS );
tiffDir.removeValue( TIFF_SUB_IFDS );
tiffDir.removeValue( TIFF_T4_OPTIONS );
tiffDir.removeValue( TIFF_T6_OPTIONS );
tiffDir.removeValue( TIFF_THRESHHOLDING );
tiffDir.removeValue( TIFF_TILE_BYTE_COUNTS );
tiffDir.removeValue( TIFF_TILE_OFFSETS );
tiffDir.removeValue( TIFF_TRANSFER_FUNCTION );
tiffDir.removeValue( TIFF_TRANSFER_RANGE );
tiffDir.removeValue( TIFF_WHITE_POINT );
tiffDir.removeValue( TIFF_X_CLIP_PATH_UNITS );
tiffDir.removeValue( TIFF_X_POSITION );
tiffDir.removeValue( TIFF_YCBCR_COEFFICIENTS );
tiffDir.removeValue( TIFF_YCBCR_POSITIONING );
tiffDir.removeValue( TIFF_YCBCR_SUBSAMPLING );
tiffDir.removeValue( TIFF_Y_CLIP_PATH_UNITS );
tiffDir.removeValue( TIFF_Y_POSITION );
if ( toJPEG ) {
//
// EXIF metadata inside JPEG files likes to be split across a main
// and a subdirectory. In particular, the tags having IDs > 0x829A
// (EXIF_EXPOSURE_TIME), except for 0x8769 (EXIF_IFD_POINTER) and
// 0x8825 (EXIF_GPS_IFD_POINTER), need to be in the subdirectory.
//
ImageMetadataDirectory subEXIFDir = null;
for ( Iterator<Map.Entry<Integer,ImageMetaValue>>
i = exifDir.iterator(); i.hasNext(); ) {
final Map.Entry<Integer,ImageMetaValue> me = i.next();
final int tagID = me.getKey();
switch ( tagID ) {
case EXIF_GPS_IFD_POINTER:
case EXIF_IFD_POINTER:
continue;
}
if ( tagID >= EXIF_SUBEXIF_TAG_ID_START ) {
if ( subEXIFDir == null )
subEXIFDir = metadata.getDirectoryFor(
SubEXIFDirectory.class, true
);
subEXIFDir.putValue( tagID, me.getValue() );
i.remove();
}
}
if ( subEXIFDir != null ) {
//
// We need a sub-EXIF IFD pointer in the EXIF directory. (The
// real value is filled-in by the EXIF encoder.)
//
exifDir.putValue(
EXIF_IFD_POINTER, new UnsignedLongMetaValue( 0 )
);
}
//
// JPEG doesn't have a TIFF directory.
//
metadata.removeDirectory( TIFFDirectory.class );
}
//
// See if GPS metadata is present: if so, add a GPS IFD pointer to the
// EXIF directory. (The real value is filled-in by the EXIF encoder.)
//
final ImageMetadataDirectory gpsDir =
metadata.getDirectoryFor( GPSDirectory.class );
if ( gpsDir != null )
exifDir.putValue(
EXIF_GPS_IFD_POINTER, new UnsignedLongMetaValue( 0 )
);
//
// Remove other directories that aren't present in either JPEG or TIFF.
//
metadata.removeDirectory( DNGDirectory.class );
//
// Remove all maker notes since we currently don't export them.
//
for ( Iterator<ImageMetadataDirectory> i =
metadata.getDirectories().iterator(); i.hasNext(); ) {
final ImageMetadataDirectory dir = i.next();
if ( dir instanceof MakerNotesDirectory )
i.remove();
}
CoreDirectory.syncEditableMetadata( metadata );
CoreDirectory.syncImageDimensions( metadata );
return metadata;
}
/**
* Prepare the EXIF, subEXIF, and TIFF directories for export as XMP. This
* means adding some fields, setting the relevant fields for the correct
* image width/height, and removing others that are no longer correct given
* the changes.
* <p>
* The original metadata is unchanged; instead, a modified clone is
* returned.
*
* @param useActualOrientation If <code>true</code>, use the actual
* orientation of the image; otherwise just use landscape.
* @return Returns the prepared <code>ImageMetadata</code>.
* @see #prepForExport(ImageType,boolean)
*/
public ImageMetadata prepForXMP( boolean useActualOrientation ) {
final ImageMetadata metadata =
prepForExport( TIFFImageType.INSTANCE, false );
//
// Remove tags that make no sense in XMP.
//
metadata.removeValues(
EXIFDirectory.class,
EXIF_IFD_POINTER,
EXIF_GPS_IFD_POINTER,
EXIF_ICC_PROFILE,
EXIF_MS_RATING,
EXIF_ORIENTATION,
EXIF_SUB_IFDS
);
metadata.removeValues(
TIFFDirectory.class,
TIFF_EXIF_IFD_POINTER,
TIFF_GPS_IFD_POINTER,
TIFF_ICC_PROFILE,
TIFF_MS_RATING,
TIFF_RICH_TIFF_IPTC,
TIFF_SUB_IFDS,
TIFF_XMP_PACKET
);
//
// If we are to use the actual orientation, use the authoritative value
// in the Core directory; otherwise, just use landscape.
//
final ImageOrientation orientation = useActualOrientation ?
metadata.getOrientation() :
ImageOrientation.ORIENTATION_LANDSCAPE;
metadata.putValue(
TIFFDirectory.class, TIFF_ORIENTATION,
new UnsignedShortMetaValue( orientation.getTIFFConstant() )
);
return metadata;
}
/**
* Puts the given {@link ImageMetadataDirectory} into this
* <code>ImageMetadata</code> replacing any previous one.
*
* @param dir The {@link ImageMetadataDirectory} to put.
* @return Returns the previous {@link ImageMetadataDirectory} or
* <code>null</code> if there was no previos directory.
*/
public ImageMetadataDirectory putDirectory( ImageMetadataDirectory dir ) {
final Class dirClass = dir.getClass();
return m_classToDirMap.put( dirClass, dir );
}
/**
* Puts a key/value pair into the given {@link ImageMetadataDirectory}.
*
* @param dirClass The class of the {@link ImageMetadataDirectory}
* @param tagID The metadata tag ID (the key).
* @param value The {@link ImageMetaValue} to put.
* @return Returns <code>true</code> only if the directory existed (or was
* created) and the value was put into it.
* @see #putValue(Class,int,ImageMetaValue,boolean)
*/
public boolean putValue( Class<? extends ImageMetadataDirectory> dirClass,
int tagID, ImageMetaValue value ) {
return putValue( dirClass, tagID, value, true );
}
/**
* Puts a key/value pair into the given {@link ImageMetadataDirectory}.
*
* @param dirClass The class of the {@link ImageMetadataDirectory}
* @param tagID The metadata tag ID (the key).
* @param value The {@link ImageMetaValue} to put.
* @param create If <code>true</code>, creates the
* {@link ImageMetadataDirectory} if it doesn't exist.
* @return Returns <code>true</code> only if the directory existed (or was
* created) and the value was put into it.
* @see #putValue(Class,int,ImageMetaValue)
*/
public boolean putValue( Class<? extends ImageMetadataDirectory> dirClass,
int tagID, ImageMetaValue value, boolean create ) {
final ImageMetadataDirectory dir = getDirectoryFor( dirClass, create );
if ( dir != null ) {
dir.putValue( tagID, value );
return true;
}
return false;
}
/**
* Remove the given {@link ImageMetadataDirectory}.
*
* @param dirClass The {@link Class} of the {@link ImageMetadataDirectory}
* to remove.
* @return Returns the removed {@link ImageMetadataDirectory} or
* <code>null</code> if there was no such directory to remove.
*/
public ImageMetadataDirectory removeDirectory( Class dirClass ) {
return m_classToDirMap.remove( dirClass );
}
/**
* Remove all directories that have no metadata.
*/
public void removeAllEmptyDirectories() {
for ( Iterator<Map.Entry<Class,ImageMetadataDirectory>>
i = m_classToDirMap.entrySet().iterator(); i.hasNext(); ) {
final Map.Entry<Class,ImageMetadataDirectory> me = i.next();
final ImageMetadataDirectory dir = me.getValue();
if ( dir.isEmpty() )
i.remove();
}
}
/**
* Remove all string values that are empty in all directories.
*/
public void removeAllEmptyStringValues() {
for ( ImageMetadataDirectory dir : m_classToDirMap.values() )
dir.removeAllEmptyStringValues();
CoreDirectory.syncEditableMetadata( this );
}
/**
* Removes the values for a set of tag IDs in a particular
* {@link ImageMetadataDirectory}. If a tag ID isn't found in the given
* directory, each directory along the static parent chain is checked
* also.
*
* @param dirClass The {@link Class} of a class derived from
* {@link ImageMetadataDirectory}.
* @param tagIDs The set of IDs of the tags to remove.
* @see ImageMetadataDirectory#getStaticParent()
*/
public void removeValues( Class<? extends ImageMetadataDirectory> dirClass,
int... tagIDs ) {
final ImageMetadataDirectory dir = getDirectoryFor( dirClass );
if ( dir != null )
for ( int tagID : tagIDs )
dir.removeValue( tagID );
}
/**
* Sets the {@link ImageType} of the image this metadata is for.
*
* @param imageType The new {@link ImageType}.
*/
public synchronized void setImageType( ImageType imageType ) {
m_imageType = imageType;
}
/**
* Sets the orientation of the image.
*
* @param orientation The new {@link ImageOrientation}.
*/
public void setOrientation( ImageOrientation orientation ) {
final ImageMetadataDirectory dir =
getDirectoryFor( CoreDirectory.class, true );
dir.setValue( CORE_IMAGE_ORIENTATION, orientation.getTIFFConstant() );
}
/**
* Sets the rating of the image.
*
* @param rating The rating; must be in the range 0-5.
* @see #clearRating()
* @see #getRating()
*/
public void setRating( int rating ) {
if ( rating < 0 || rating > 5 )
throw new IllegalArgumentException( "rating must be between 0-5" );
final ImageMetadataDirectory dir =
getDirectoryFor( CoreDirectory.class, true );
dir.setValue( CORE_RATING, rating );
}
/**
* Convert all the metadata into a single {@link String} for debugging
* purposes.
*
* @return Returns said {@link String}.
*/
public String toString() {
final StringBuilder sb = new StringBuilder();
for ( ImageMetadataDirectory dir : getDirectories() ) {
sb.append( dir.toString() );
sb.append( '\n' );
}
return sb.toString();
}
/**
* Convert all the metadata into an XMP XML document.
*
* @param useActualOrientation If <code>true</code>, use the actual
* orientation of the image; otherwise just use landscape.
* @param includeXMPPacket If <code>true</code>, XMP packet processing
* instructions are included in the new document.
* @param dirClass The set of directories to include or <code>null</code>
* for all.
* @return Returns said document.
*/
public Document toXMP( boolean useActualOrientation,
boolean includeXMPPacket,
Class<? extends ImageMetadataDirectory>... dirClass )
{
final ImageMetadata metadata = prepForXMP( useActualOrientation );
final Document doc = XMPUtil.createEmptyXMPDocument( includeXMPPacket );
metadata.toXMP( doc, dirClass );
return doc;
}
/**
* Convert all the metadata into an XMP XML document.
*
* @param xmpDoc The XMP XML document to use.
* @param dirClass The set of directories to include or <code>null</code>
* for all.
*/
public void toXMP( Document xmpDoc,
Class<? extends ImageMetadataDirectory>... dirClass ) {
final Set<Class<? extends ImageMetadataDirectory>> dirSet =
CollectionUtil.asSet( dirClass );
final Element rdfElement = XMPUtil.getRDFElementOf( xmpDoc );
for ( ImageMetadataDirectory dir : getDirectories() ) {
if ( dirSet != null && !dirSet.isEmpty() &&
!dirSet.contains( dir.getClass() ) )
continue;
final Collection<Element> rdfDescElements = dir.toXMP( xmpDoc );
if ( rdfDescElements != null )
for ( Element element : rdfDescElements )
rdfElement.appendChild( element );
}
final Element dcRDFDescElement = toDublinCoreXMP( xmpDoc );
if ( dcRDFDescElement != null )
rdfElement.appendChild( dcRDFDescElement );
}
/**
* {@inheritDoc}
*/
public void readExternal( ObjectInput in ) throws IOException {
for ( int count = in.readShort(); count > 0; --count ) {
try {
final Class dirClass = Class.forName( in.readUTF() );
//noinspection unchecked
getDirectoryFor( dirClass, true ).readExternal( in );
}
catch ( ClassNotFoundException e ) {
final IOException ioe = new IOException();
ioe.initCause( e );
throw ioe;
}
}
}
/**
* @serialData The number of {@link ImageMetadataDirectory} objects
* (<code>short</code>) followed by pairs of each directory's class's name
* (<code>String</code>) and {@link ImageMetadataDirectory}.
*/
public void writeExternal( ObjectOutput out ) throws IOException {
out.writeShort( m_classToDirMap.size() );
for ( Map.Entry<Class,ImageMetadataDirectory>
me : m_classToDirMap.entrySet() ) {
final Class dirClass = me.getKey();
final ImageMetadataDirectory dir = me.getValue();
out.writeUTF( dirClass.getName() );
dir.writeExternal( out );
}
}
////////// private ////////////////////////////////////////////////////////
/**
* A <code>ProviderComparator</code> is-a {@link Comparator} for comparing
* {@link ImageMetadataDirectory} objects by their priority for being a
* provider for some metadata.
*/
private static final class ProviderComparator
implements Comparator<ImageMetadataDirectory> {
////////// public /////////////////////////////////////////////////////
/**
* Compares two {@link ImageMetadataDirectory} objects for priority
* order in providing metadata as a particular
* {@link ImageMetadataProvider}.
*
* @param dir1 The first {@link ImageMetadataDirectory} to be compared.
* @param dir2 The second {@link ImageMetadataDirectory} to be compared.
* @return Returns a negative integer, zero, or a positive integer as
* the first directory's provider priority is greater than, equal to,
* or less than the second.
*/
public int compare( ImageMetadataDirectory dir1,
ImageMetadataDirectory dir2 ) {
return dir2.getProviderPriorityFor( m_provider ) -
dir1.getProviderPriorityFor( m_provider );
}
////////// package ////////////////////////////////////////////////////
/**
* Construct a <code>ProviderComparator</code>.
*
* @param provider The provider interface to use for comparisons.
*/
ProviderComparator( Class<? extends ImageMetadataProvider> provider ) {
m_provider = provider;
}
////////// private ////////////////////////////////////////////////////
private final Class<? extends ImageMetadataProvider> m_provider;
}
/**
* Convert relevant metadata into an XMP Dublin Core RDF element.
*
* @param xmpDoc The XMP XML document to use.
* @return Returns said {@link Element} or <code>null</code>
*/
private Element toDublinCoreXMP( Document xmpDoc ) {
final String artist = getArtist();
final String description = getCaption();
final String rights = getCopyright();
final String title = getTitle();
if ( artist == null && description == null && rights == null &&
title == null )
return null;
final Element dcRDFDescElement =
XMPUtil.createRDFDescription( xmpDoc, XMP_DC_NS, XMP_DC_PREFIX );
if ( artist != null ) {
final Element creatorElement = xmpDoc.createElementNS(
XMP_DC_NS, XMP_DC_PREFIX + ":creator"
);
XMLUtil.setTextContentOf( creatorElement, artist );
dcRDFDescElement.appendChild( creatorElement );
}
if ( description != null ) {
final Element descriptionElement = xmpDoc.createElementNS(
XMP_DC_NS, XMP_DC_PREFIX + ":description"
);
XMLUtil.setTextContentOf( descriptionElement, description );
dcRDFDescElement.appendChild( descriptionElement );
}
if ( rights != null ) {
final Element rightsElement = xmpDoc.createElementNS(
XMP_DC_NS, XMP_DC_PREFIX + ":rights"
);
XMLUtil.setTextContentOf( rightsElement, rights );
dcRDFDescElement.appendChild( rightsElement );
}
if ( title != null ) {
final Element titleElement = xmpDoc.createElementNS(
XMP_DC_NS, XMP_DC_PREFIX + ":title"
);
XMLUtil.setTextContentOf( titleElement, title );
dcRDFDescElement.appendChild( titleElement );
}
return dcRDFDescElement;
}
/**
* The map from directory classes to instances thereof.
*/
private final Map<Class,ImageMetadataDirectory> m_classToDirMap =
new HashMap<Class,ImageMetadataDirectory>();
/**
* The type of the image file this metadata is for.
*/
private ImageType m_imageType;
////////// main (for testing) /////////////////////////////////////////////
public static void main( String[] args ) throws Exception {
final FileOutputStream fos = new FileOutputStream( "/tmp/out" );
final ObjectOutputStream oos = new ObjectOutputStream( fos );
final ImageInfo info = ImageInfo.getInstanceFor( new File( args[0] ) );
ImageMetadata metadata = info.getMetadata();
metadata.writeExternal( oos );
oos.close();
fos.close();
final FileInputStream fis = new FileInputStream( "/tmp/out" );
final ObjectInputStream ois = new ObjectInputStream( fis );
metadata = new ImageMetadata();
metadata.readExternal( ois );
ois.close();
fis.close();
System.out.println( metadata.toString() );
}
}
/* vim:set et sw=4 ts=4: */