Blob Blame History Raw
// =================================================================================================
// ADOBE SYSTEMS INCORPORATED
// Copyright 2006 Adobe Systems Incorporated
// All Rights Reserved
//
// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms
// of the Adobe license agreement accompanying it.
// =================================================================================================

#include "public/include/XMP_Environment.h"	// ! XMP_Environment.h must be the first included header.
#include "public/include/XMP_Const.h"

#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp"
#include "source/XIO.hpp"

#include <stdio.h>

#if XMP_WinBuild
	#pragma warning ( disable : 4800 )	// forcing value to bool 'true' or 'false' (performance warning)
	#pragma warning ( disable : 4996 )	// '...' was declared deprecated
#endif

// =================================================================================================
/// \file ReconcileIPTC.cpp
/// \brief Utilities to reconcile between XMP and legacy IPTC and PSIR metadata.
///
// =================================================================================================

// =================================================================================================
// NormalizeToCR
// =============

static inline void NormalizeToCR ( std::string * value )
{
	char * strPtr = (char*) value->data();
	char * strEnd = strPtr + value->size();

	for ( ; strPtr < strEnd; ++strPtr ) {
		if ( *strPtr == kLF ) *strPtr = kCR;
	}

}	// NormalizeToCR

// =================================================================================================
// NormalizeToLF
// =============

static inline void NormalizeToLF ( std::string * value )
{
	char * strPtr = (char*) value->data();
	char * strEnd = strPtr + value->size();

	for ( ; strPtr < strEnd; ++strPtr ) {
		if ( *strPtr == kCR ) *strPtr = kLF;
	}

}	// NormalizeToLF

// =================================================================================================
// ComputeIPTCDigest
// =================
//
// Compute a 128 bit (16 byte) MD5 digest of the full IPTC block.

static inline void ComputeIPTCDigest ( const void * iptcPtr, const XMP_Uns32 iptcLen, MD5_Digest * digest )
{
	MD5_CTX context;

	MD5Init ( &context );
	MD5Update ( &context, (XMP_Uns8*)iptcPtr, iptcLen );
	MD5Final ( *digest, &context );

}	// ComputeIPTCDigest;

// =================================================================================================
// PhotoDataUtils::CheckIPTCDigest
// ===============================

int PhotoDataUtils::CheckIPTCDigest ( const void * newPtr, const XMP_Uns32 newLen, const void * oldDigest )
{
	MD5_Digest newDigest;
	ComputeIPTCDigest ( newPtr, newLen, &newDigest );
	if ( memcmp ( &newDigest, oldDigest, 16 ) == 0 ) return kDigestMatches;
	return kDigestDiffers;

}	// PhotoDataUtils::CheckIPTCDigest

// =================================================================================================
// PhotoDataUtils::SetIPTCDigest
// =============================

void PhotoDataUtils::SetIPTCDigest ( void * iptcPtr, XMP_Uns32 iptcLen, PSIR_Manager * psir )
{
	MD5_Digest newDigest;

	ComputeIPTCDigest ( iptcPtr, iptcLen, &newDigest );
	psir->SetImgRsrc ( kPSIR_IPTCDigest, &newDigest, sizeof(newDigest) );

}	// PhotoDataUtils::SetIPTCDigest

// =================================================================================================
// =================================================================================================

// =================================================================================================
// PhotoDataUtils::ImportIPTC_Simple
// =================================

void PhotoDataUtils::ImportIPTC_Simple ( const IPTC_Manager & iptc, SXMPMeta * xmp,
										 XMP_Uns8 id, const char * xmpNS, const char * xmpProp )
{
	std::string utf8Str;
	size_t count = iptc.GetDataSet_UTF8 ( id, &utf8Str );

	if ( count != 0 ) {
		NormalizeToLF ( &utf8Str );
		xmp->SetProperty ( xmpNS, xmpProp, utf8Str.c_str() );
	}

}	// PhotoDataUtils::ImportIPTC_Simple

// =================================================================================================
// PhotoDataUtils::ImportIPTC_LangAlt
// ==================================

void PhotoDataUtils::ImportIPTC_LangAlt ( const IPTC_Manager & iptc, SXMPMeta * xmp,
										  XMP_Uns8 id, const char * xmpNS, const char * xmpProp )
{
	std::string utf8Str;
	size_t count = iptc.GetDataSet_UTF8 ( id, &utf8Str );

	if ( count != 0 ) {
		NormalizeToLF ( &utf8Str );
		xmp->SetLocalizedText ( xmpNS, xmpProp, "", "x-default", utf8Str.c_str() );
	}

}	// PhotoDataUtils::ImportIPTC_LangAlt

// =================================================================================================
// PhotoDataUtils::ImportIPTC_Array
// ================================

void PhotoDataUtils::ImportIPTC_Array ( const IPTC_Manager & iptc, SXMPMeta * xmp,
										XMP_Uns8 id, const char * xmpNS, const char * xmpProp )
{
	std::string utf8Str;
	size_t count = iptc.GetDataSet ( id, 0 );

	xmp->DeleteProperty ( xmpNS, xmpProp );
	
	XMP_OptionBits arrayForm = kXMP_PropArrayIsUnordered;
	if ( XMP_LitMatch ( xmpNS, kXMP_NS_DC ) && XMP_LitMatch ( xmpProp, "creator" ) ) arrayForm = kXMP_PropArrayIsOrdered;

	for ( size_t ds = 0; ds < count; ++ds ) {
		(void) iptc.GetDataSet_UTF8 ( id, &utf8Str, ds );
		NormalizeToLF ( &utf8Str );
		xmp->AppendArrayItem ( xmpNS, xmpProp, arrayForm, utf8Str.c_str() );
	}

}	// PhotoDataUtils::ImportIPTC_Array

// =================================================================================================
// PhotoDataUtils::ImportIPTC_Date
// ===============================
//
// An IPTC (IIM) date is 8 characters, YYYYMMDD. Include the time portion if it is present. The IPTC
// time is HHMMSSxHHMM, where 'x' is '+' or '-'. Be tolerant of some ill-formed dates and times.
// Apparently some non-Adobe apps put strings like "YYYY-MM-DD" or "HH:MM:SSxHH:MM" in the IPTC.
// Allow a missing time zone portion.

// *** The date/time handling differs from the MWG 1.0.1 policy, following a proposed tweak to MWG:
// ***   Exif DateTimeOriginal <-> XMP exif:DateTimeOriginal
// ***   IPTC DateCreated <-> XMP photoshop:DateCreated
// ***   Exif DateTimeDigitized <-> IPTC DigitalCreateDate <-> XMP xmp:CreateDate

void PhotoDataUtils::ImportIPTC_Date ( XMP_Uns8 dateID, const IPTC_Manager & iptc, SXMPMeta * xmp )
{
	XMP_Uns8 timeID;
	XMP_StringPtr xmpNS, xmpProp;
	
	if ( dateID == kIPTC_DateCreated ) {
		timeID  = kIPTC_TimeCreated;
		xmpNS   = kXMP_NS_Photoshop;
		xmpProp = "DateCreated";
	} else if ( dateID == kIPTC_DigitalCreateDate ) {
		timeID  = kIPTC_DigitalCreateTime;
		xmpNS   = kXMP_NS_XMP;
		xmpProp = "CreateDate";
	} else {
		XMP_Throw ( "Unrecognized dateID", kXMPErr_BadParam );
	}
	
	// First gather the date portion.

	IPTC_Manager::DataSetInfo dsInfo;
	size_t count = iptc.GetDataSet ( dateID, &dsInfo );
	if ( count == 0 ) return;

	size_t chPos, digits;
	XMP_DateTime xmpDate;
	memset ( &xmpDate, 0, sizeof(xmpDate) );

	chPos = 0;
	for ( digits = 0; digits < 4; ++digits, ++chPos ) {
		if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break;
		xmpDate.year = (xmpDate.year * 10) + (dsInfo.dataPtr[chPos] - '0');
	}

	if ( dsInfo.dataPtr[chPos] == '-' ) ++chPos;
	for ( digits = 0; digits < 2; ++digits, ++chPos ) {
		if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break;
		xmpDate.month = (xmpDate.month * 10) + (dsInfo.dataPtr[chPos] - '0');
	}
	if ( xmpDate.month < 1 ) xmpDate.month = 1;
	if ( xmpDate.month > 12 ) xmpDate.month = 12;

	if ( dsInfo.dataPtr[chPos] == '-' ) ++chPos;
	for ( digits = 0; digits < 2; ++digits, ++chPos ) {
		if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break;
		xmpDate.day = (xmpDate.day * 10) + (dsInfo.dataPtr[chPos] - '0');
	}
	if ( xmpDate.day < 1 ) xmpDate.day = 1;
	if ( xmpDate.day > 31 ) xmpDate.day = 28;	// Close enough.

	if ( chPos != dsInfo.dataLen ) return;	// The DataSet is ill-formed.
	xmpDate.hasDate = true;

	// Now add the time portion if present.

	count = iptc.GetDataSet ( timeID, &dsInfo );
	if ( count != 0 ) {

		chPos = 0;
		for ( digits = 0; digits < 2; ++digits, ++chPos ) {
			if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break;
			xmpDate.hour = (xmpDate.hour * 10) + (dsInfo.dataPtr[chPos] - '0');
		}
		if ( xmpDate.hour < 0 ) xmpDate.hour = 0;
		if ( xmpDate.hour > 23 ) xmpDate.hour = 23;

		if ( dsInfo.dataPtr[chPos] == ':' ) ++chPos;
		for ( digits = 0; digits < 2; ++digits, ++chPos ) {
			if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break;
			xmpDate.minute = (xmpDate.minute * 10) + (dsInfo.dataPtr[chPos] - '0');
		}
		if ( xmpDate.minute < 0 ) xmpDate.minute = 0;
		if ( xmpDate.minute > 59 ) xmpDate.minute = 59;

		if ( dsInfo.dataPtr[chPos] == ':' ) ++chPos;
		for ( digits = 0; digits < 2; ++digits, ++chPos ) {
			if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break;
			xmpDate.second = (xmpDate.second * 10) + (dsInfo.dataPtr[chPos] - '0');
		}
		if ( xmpDate.second < 0 ) xmpDate.second = 0;
		if ( xmpDate.second > 59 ) xmpDate.second = 59;

		xmpDate.hasTime = true;

		if ( (dsInfo.dataPtr[chPos] != ' ') && (dsInfo.dataPtr[chPos] != 0) ) {	// Tolerate a missing TZ.
		
			if ( dsInfo.dataPtr[chPos] == '+' ) {
				xmpDate.tzSign = kXMP_TimeEastOfUTC;
			} else if ( dsInfo.dataPtr[chPos] == '-' ) {
				xmpDate.tzSign = kXMP_TimeWestOfUTC;
			} else if ( chPos != dsInfo.dataLen ) {
				return;	// The DataSet is ill-formed.
			}
	
			++chPos;	// Move past the time zone sign.
			for ( digits = 0; digits < 2; ++digits, ++chPos ) {
				if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break;
				xmpDate.tzHour = (xmpDate.tzHour * 10) + (dsInfo.dataPtr[chPos] - '0');
			}
			if ( xmpDate.tzHour < 0 ) xmpDate.tzHour = 0;
			if ( xmpDate.tzHour > 23 ) xmpDate.tzHour = 23;
	
			if ( dsInfo.dataPtr[chPos] == ':' ) ++chPos;
			for ( digits = 0; digits < 2; ++digits, ++chPos ) {
				if ( (chPos >= dsInfo.dataLen) || (dsInfo.dataPtr[chPos] < '0') || (dsInfo.dataPtr[chPos] > '9') ) break;
				xmpDate.tzMinute = (xmpDate.tzMinute * 10) + (dsInfo.dataPtr[chPos] - '0');
			}
			if ( xmpDate.tzMinute < 0 ) xmpDate.tzMinute = 0;
			if ( xmpDate.tzMinute > 59 ) xmpDate.tzMinute = 59;
	
			if ( chPos != dsInfo.dataLen ) return;	// The DataSet is ill-formed.
			xmpDate.hasTimeZone = true;
			
		}

	}

	// Finally, set the XMP property.

	xmp->SetProperty_Date ( xmpNS, xmpProp, xmpDate );

}	// PhotoDataUtils::ImportIPTC_Date

// =================================================================================================
// ImportIPTC_IntellectualGenre
// ============================
//
// Import DataSet 2:04. In the IIM this is a 3 digit number, a colon, and an optional text name.
// Even though the number is the more formal part, the IPTC4XMP rule is that the name is imported to
// XMP and the number is dropped. Also, even though IIMv4.1 says that 2:04 is repeatable, the XMP
// property to which it is mapped is simple.

static void ImportIPTC_IntellectualGenre ( const IPTC_Manager & iptc, SXMPMeta * xmp )
{
	std::string utf8Str;
	size_t count = iptc.GetDataSet_UTF8 ( kIPTC_IntellectualGenre, &utf8Str );

	if ( count == 0 ) return;
	NormalizeToLF ( &utf8Str );

	XMP_StringPtr namePtr = utf8Str.c_str() + 4;

	if ( utf8Str.size() <= 4 ) {
		// No name in the IIM. Look up the number in our list of known genres.
		int i;
		XMP_StringPtr numPtr = utf8Str.c_str();
		for ( i = 0; kIntellectualGenreMappings[i].refNum != 0; ++i ) {
			if ( strncmp ( numPtr, kIntellectualGenreMappings[i].refNum, 3 ) == 0 ) break;
		}
		if ( kIntellectualGenreMappings[i].refNum == 0 ) return;
		namePtr = kIntellectualGenreMappings[i].name;
	}

	xmp->SetProperty ( kXMP_NS_IPTCCore, "IntellectualGenre", namePtr );

}	// ImportIPTC_IntellectualGenre

// =================================================================================================
// ImportIPTC_SubjectCode
// ======================
//
// Import all 2:12 DataSets into an unordered array. In the IIM each DataSet is composed of 5 colon
// separated sections: a provider name, an 8 digit reference number, and 3 optional names for the
// levels of the reference number hierarchy. The IPTC4XMP mapping rule is that only the reference
// number is imported to XMP.

static void ImportIPTC_SubjectCode ( const IPTC_Manager & iptc, SXMPMeta * xmp )
{
	std::string utf8Str;
	size_t count = iptc.GetDataSet_UTF8 ( kIPTC_SubjectCode, 0 );

	for ( size_t ds = 0; ds < count; ++ds ) {

		(void) iptc.GetDataSet_UTF8 ( kIPTC_SubjectCode, &utf8Str, ds );

		char * refNumPtr = (char*) utf8Str.c_str();
		for ( ; (*refNumPtr != ':') && (*refNumPtr != 0); ++refNumPtr ) {}
		if ( *refNumPtr == 0 ) continue;	// This DataSet is ill-formed.

		char * refNumEnd = refNumPtr + 1;
		for ( ; (*refNumEnd != ':') && (*refNumEnd != 0); ++refNumEnd ) {}
		if ( (refNumEnd - refNumPtr) != 8 ) continue;	// This DataSet is ill-formed.
		*refNumEnd = 0;	// Ensure a terminating nul for the reference number portion.

		xmp->AppendArrayItem ( kXMP_NS_IPTCCore, "SubjectCode", kXMP_PropArrayIsUnordered, refNumPtr );

	}

}	// ImportIPTC_SubjectCode

// =================================================================================================
// PhotoDataUtils::Import2WayIPTC
// ==============================

void PhotoDataUtils::Import2WayIPTC ( const IPTC_Manager & iptc, SXMPMeta * xmp, int iptcDigestState )
{
	if ( iptcDigestState == kDigestMatches ) return;	// Ignore the IPTC if the digest matches.

	std::string oldStr, newStr;
	IPTC_Writer oldIPTC;

	if ( iptcDigestState == kDigestDiffers ) {
		PhotoDataUtils::ExportIPTC ( *xmp, &oldIPTC );	// Predict old IPTC DataSets based on the existing XMP.
	}
	
	size_t newCount;
	IPTC_Manager::DataSetInfo newInfo, oldInfo;
	
	for ( size_t i = 0; kKnownDataSets[i].dsNum != 255; ++i ) {

		const DataSetCharacteristics & thisDS = kKnownDataSets[i];
		if ( thisDS.mapForm >= kIPTC_Map3Way ) continue;	// The mapping is handled elsewhere, or not at all.
		
		bool haveXMP = xmp->DoesPropertyExist ( thisDS.xmpNS, thisDS.xmpProp );
		newCount = PhotoDataUtils::GetNativeInfo ( iptc, thisDS.dsNum, iptcDigestState, haveXMP, &newInfo );
		if ( newCount == 0 ) continue;	// GetNativeInfo returns 0 for ignored local text.
		
		if ( iptcDigestState == kDigestMissing ) {
			if ( haveXMP ) continue;	// Keep the existing XMP.
		} else if ( ! PhotoDataUtils::IsValueDifferent ( iptc, oldIPTC, thisDS.dsNum ) ) {
			continue;	// Don't import values that match the previous export.
		}
		
		// The IPTC wins. Delete any existing XMP and import the DataSet.
		
		xmp->DeleteProperty ( thisDS.xmpNS, thisDS.xmpProp );

		try {	// Don't let errors with one stop the others.

			switch ( thisDS.mapForm ) {

				case kIPTC_MapSimple :
					ImportIPTC_Simple ( iptc, xmp, thisDS.dsNum, thisDS.xmpNS, thisDS.xmpProp );
					break;

				case kIPTC_MapLangAlt :
					ImportIPTC_LangAlt ( iptc, xmp, thisDS.dsNum, thisDS.xmpNS, thisDS.xmpProp );
					break;

				case kIPTC_MapArray :
					ImportIPTC_Array ( iptc, xmp, thisDS.dsNum, thisDS.xmpNS, thisDS.xmpProp );
					break;

				case kIPTC_MapSpecial :
					if ( thisDS.dsNum == kIPTC_DateCreated ) {
						PhotoDataUtils::ImportIPTC_Date ( thisDS.dsNum, iptc, xmp );
					} else if ( thisDS.dsNum == kIPTC_IntellectualGenre ) {
						ImportIPTC_IntellectualGenre ( iptc, xmp );
					} else if ( thisDS.dsNum == kIPTC_SubjectCode ) {
						ImportIPTC_SubjectCode ( iptc, xmp );
					} else {
						XMP_Assert ( false );	// Catch mapping errors.
					}
					break;

			}

		} catch ( ... ) {

			// Do nothing, let other imports proceed.
			// ? Notify client?

		}

	}

}	// PhotoDataUtils::Import2WayIPTC

// =================================================================================================
// PhotoDataUtils::ImportPSIR
// ==========================
//
// There are only 2 standalone Photoshop image resources for XMP properties:
//    1034 - Copyright Flag - 0/1 Boolean mapped to xmpRights:Marked.
//    1035 - Copyright URL - Local OS text mapped to xmpRights:WebStatement.

// ! Photoshop does not use a true/false/missing model for PSIR 1034. Instead it essentially uses a
// ! yes/don't-know model when importing. A missing or 0 value for PSIR 1034 cause xmpRights:Marked
// ! to be deleted.

void PhotoDataUtils::ImportPSIR ( const PSIR_Manager & psir, SXMPMeta * xmp, int iptcDigestState )
{
	PSIR_Manager::ImgRsrcInfo rsrcInfo;
	bool import;

	if ( iptcDigestState == kDigestMatches ) return;

	try {	// Don't let errors with one stop the others.
		import = psir.GetImgRsrc ( kPSIR_CopyrightFlag, &rsrcInfo );
		if ( import ) import = (! xmp->DoesPropertyExist ( kXMP_NS_XMP_Rights, "Marked" ));
		if ( import && (rsrcInfo.dataLen == 1) && (*((XMP_Uns8*)rsrcInfo.dataPtr) != 0) ) {
			xmp->SetProperty_Bool ( kXMP_NS_XMP_Rights, "Marked", true );
		}
	} catch ( ... ) {
		// Do nothing, let other imports proceed.
		// ? Notify client?
	}

	try {	// Don't let errors with one stop the others.
		import = psir.GetImgRsrc ( kPSIR_CopyrightURL, &rsrcInfo );
		if ( import ) import = (! xmp->DoesPropertyExist ( kXMP_NS_XMP_Rights, "WebStatement" ));
		if ( import ) {
			std::string utf8;
			if ( ReconcileUtils::IsUTF8 ( rsrcInfo.dataPtr, rsrcInfo.dataLen ) ) {
				utf8.assign ( (char*)rsrcInfo.dataPtr, rsrcInfo.dataLen );
			} else if ( ! ignoreLocalText ) {
				ReconcileUtils::LocalToUTF8 ( rsrcInfo.dataPtr, rsrcInfo.dataLen, &utf8 );
			} else {
				import = false;	// Inhibit the SetProperty call.
			}
			if ( import ) xmp->SetProperty ( kXMP_NS_XMP_Rights, "WebStatement", utf8.c_str() );
		}
	} catch ( ... ) {
		// Do nothing, let other imports proceed.
		// ? Notify client?
	}

}	// PhotoDataUtils::ImportPSIR;

// =================================================================================================
// =================================================================================================

// =================================================================================================
// ExportIPTC_Simple
// =================

static void ExportIPTC_Simple ( const SXMPMeta & xmp, IPTC_Manager * iptc,
								const char * xmpNS, const char * xmpProp, XMP_Uns8 id )
{
	std::string    value;
	XMP_OptionBits xmpFlags;

	bool found = xmp.GetProperty ( xmpNS, xmpProp, &value, &xmpFlags );
	if ( ! found ) {
		iptc->DeleteDataSet ( id );
		return;
	}

	if ( ! XMP_PropIsSimple ( xmpFlags ) ) return;	// ? Complain? Delete the DataSet?

	NormalizeToCR ( &value );

	size_t iptcCount = iptc->GetDataSet ( id, 0 );
	if ( iptcCount > 1 ) iptc->DeleteDataSet ( id );

	iptc->SetDataSet_UTF8 ( id, value.c_str(), (XMP_Uns32)value.size(), 0 );	// ! Don't append a 2nd DataSet!

}	// ExportIPTC_Simple

// =================================================================================================
// ExportIPTC_LangAlt
// ==================

static void ExportIPTC_LangAlt ( const SXMPMeta & xmp, IPTC_Manager * iptc,
								 const char * xmpNS, const char * xmpProp, XMP_Uns8 id )
{
	std::string    value;
	XMP_OptionBits xmpFlags;

	bool found = xmp.GetProperty ( xmpNS, xmpProp, 0, &xmpFlags );
	if ( ! found ) {
		iptc->DeleteDataSet ( id );
		return;
	}

	if ( ! XMP_ArrayIsAltText ( xmpFlags ) ) return;	// ? Complain? Delete the DataSet?

	found = xmp.GetLocalizedText ( xmpNS, xmpProp, "", "x-default", 0, &value, 0 );
	if ( ! found ) {
		iptc->DeleteDataSet ( id );
		return;
	}

	NormalizeToCR ( &value );

	size_t iptcCount = iptc->GetDataSet ( id, 0 );
	if ( iptcCount > 1 ) iptc->DeleteDataSet ( id );

	iptc->SetDataSet_UTF8 ( id, value.c_str(), (XMP_Uns32)value.size(), 0 );	// ! Don't append a 2nd DataSet!

}	// ExportIPTC_LangAlt

// =================================================================================================
// ExportIPTC_Array
// ================
//
// Array exporting needs a bit of care to preserve the detection of XMP-only updates. If the current
// XMP and IPTC array sizes differ, delete the entire IPTC and append all new values. If they match,
// set the individual values in order - which lets SetDataSet apply its no-change optimization.

static void ExportIPTC_Array ( const SXMPMeta & xmp, IPTC_Manager * iptc,
							   const char * xmpNS, const char * xmpProp, XMP_Uns8 id )
{
	std::string    value;
	XMP_OptionBits xmpFlags;

	bool found = xmp.GetProperty ( xmpNS, xmpProp, 0, &xmpFlags );
	if ( ! found ) {
		iptc->DeleteDataSet ( id );
		return;
	}

	if ( ! XMP_PropIsArray ( xmpFlags ) ) return;	// ? Complain? Delete the DataSet?

	XMP_Index xmpCount  = xmp.CountArrayItems ( xmpNS, xmpProp );
	XMP_Index iptcCount = (XMP_Index) iptc->GetDataSet ( id, 0 );

	if ( xmpCount != iptcCount ) iptc->DeleteDataSet ( id );

	for ( XMP_Index ds = 0; ds < xmpCount; ++ds ) {	// ! XMP arrays are indexed from 1, IPTC from 0.

		(void) xmp.GetArrayItem ( xmpNS, xmpProp, ds+1, &value, &xmpFlags );
		if ( ! XMP_PropIsSimple ( xmpFlags ) ) continue;	// ? Complain?

		NormalizeToCR ( &value );

		iptc->SetDataSet_UTF8 ( id, value.c_str(), (XMP_Uns32)value.size(), ds );	// ! Appends if necessary.

	}

}	// ExportIPTC_Array

// =================================================================================================
// ExportIPTC_IntellectualGenre
// ============================
//
// Export DataSet 2:04. In the IIM this is a 3 digit number, a colon, and a text name. Even though
// the number is the more formal part, the IPTC4XMP rule is that the name is imported to XMP and the
// number is dropped. Also, even though IIMv4.1 says that 2:04 is repeatable, the XMP property to
// which it is mapped is simple. Look up the XMP value in a list of known genres to get the number.

static void ExportIPTC_IntellectualGenre ( const SXMPMeta & xmp, IPTC_Manager * iptc )
{
	std::string    xmpValue;
	XMP_OptionBits xmpFlags;

	bool found = xmp.GetProperty ( kXMP_NS_IPTCCore, "IntellectualGenre", &xmpValue, &xmpFlags );
	if ( ! found ) {
		iptc->DeleteDataSet ( kIPTC_IntellectualGenre );
		return;
	}

	if ( ! XMP_PropIsSimple ( xmpFlags ) ) return;	// ? Complain? Delete the DataSet?

	NormalizeToCR ( &xmpValue );

	int i;
	XMP_StringPtr namePtr = xmpValue.c_str();
	for ( i = 0; kIntellectualGenreMappings[i].name != 0; ++i ) {
		if ( strcmp ( namePtr, kIntellectualGenreMappings[i].name ) == 0 ) break;
	}
	if ( kIntellectualGenreMappings[i].name == 0 ) return;	// Not a known genre, don't export it.

	std::string iimValue = kIntellectualGenreMappings[i].refNum;
	iimValue += ':';
	iimValue += xmpValue;

	size_t iptcCount = iptc->GetDataSet ( kIPTC_IntellectualGenre, 0 );
	if ( iptcCount > 1 ) iptc->DeleteDataSet ( kIPTC_IntellectualGenre );

	iptc->SetDataSet_UTF8 ( kIPTC_IntellectualGenre, iimValue.c_str(), (XMP_Uns32)iimValue.size(), 0 );	// ! Don't append a 2nd DataSet!

}	// ExportIPTC_IntellectualGenre

// =================================================================================================
// ExportIPTC_SubjectCode
// ======================
//
// Export 2:12 DataSets from an unordered array. In the IIM each DataSet is composed of 5 colon
// separated sections: a provider name, an 8 digit reference number, and 3 optional names for the
// levels of the reference number hierarchy. The IPTC4XMP mapping rule is that only the reference
// number is imported to XMP. We export with a fixed provider of "IPTC" and no optional names.

static void ExportIPTC_SubjectCode ( const SXMPMeta & xmp, IPTC_Manager * iptc )
{
	std::string    xmpValue, iimValue;
	XMP_OptionBits xmpFlags;

	bool found = xmp.GetProperty ( kXMP_NS_IPTCCore, "SubjectCode", 0, &xmpFlags );
	if ( ! found ) {
		iptc->DeleteDataSet ( kIPTC_SubjectCode );
		return;
	}

	if ( ! XMP_PropIsArray ( xmpFlags ) ) return;	// ? Complain? Delete the DataSet?

	XMP_Index xmpCount  = xmp.CountArrayItems ( kXMP_NS_IPTCCore, "SubjectCode" );
	XMP_Index iptcCount = (XMP_Index) iptc->GetDataSet ( kIPTC_SubjectCode, 0 );

	if ( xmpCount != iptcCount ) iptc->DeleteDataSet ( kIPTC_SubjectCode );

	for ( XMP_Index ds = 0; ds < xmpCount; ++ds ) {	// ! XMP arrays are indexed from 1, IPTC from 0.

		(void) xmp.GetArrayItem ( kXMP_NS_IPTCCore, "SubjectCode", ds+1, &xmpValue, &xmpFlags );
		if ( ! XMP_PropIsSimple ( xmpFlags ) ) continue;	// ? Complain?
		if ( xmpValue.size() != 8 ) continue;	// ? Complain?

		iimValue = "IPTC:";
		iimValue += xmpValue;
		iimValue += ":::";	// Add the separating colons for the empty name portions.

		iptc->SetDataSet_UTF8 ( kIPTC_SubjectCode, iimValue.c_str(), (XMP_Uns32)iimValue.size(), ds );	// ! Appends if necessary.

	}

}	// ExportIPTC_SubjectCode

// =================================================================================================
// ExportIPTC_Date
// ===============
//
// The IPTC date and time are "YYYYMMDD" and "HHMMSSxHHMM" where 'x' is '+' or '-'. Export the IPTC
// time only if already present, or if the XMP has a time portion.

// *** The date/time handling differs from the MWG 1.0 policy, following a proposed tweak to MWG:
// ***   Exif DateTimeOriginal <-> IPTC DateCreated <-> XMP photoshop:DateCreated
// ***   Exif DateTimeDigitized <-> IPTC DigitalCreateDate <-> XMP xmp:CreateDate

static void ExportIPTC_Date ( XMP_Uns8 dateID, const SXMPMeta & xmp, IPTC_Manager * iptc )
{
	XMP_Uns8 timeID;
	XMP_StringPtr xmpNS, xmpProp;
	
	if ( dateID == kIPTC_DateCreated ) {
		timeID  = kIPTC_TimeCreated;
		xmpNS   = kXMP_NS_Photoshop;
		xmpProp = "DateCreated";
	} else if ( dateID == kIPTC_DigitalCreateDate ) {
		timeID  = kIPTC_DigitalCreateTime;
		xmpNS   = kXMP_NS_XMP;
		xmpProp = "CreateDate";
	} else {
		XMP_Throw ( "Unrecognized dateID", kXMPErr_BadParam );
	}

	iptc->DeleteDataSet ( dateID );	// ! Either the XMP does not exist and we want to 
	iptc->DeleteDataSet ( timeID );	// ! delete the IPTC, or we're replacing the IPTC.

	XMP_DateTime xmpValue;
	bool found = xmp.GetProperty_Date ( xmpNS, xmpProp, &xmpValue, 0 );
	if ( ! found ) return;

	char iimValue[16];	// AUDIT: Big enough for "YYYYMMDD" (8) and "HHMMSS+HHMM" (11).

	// Set the IIM date portion as YYYYMMDD with zeroes for unknown parts.
	
	snprintf ( iimValue, sizeof(iimValue), "%04d%02d%02d",	// AUDIT: Use of sizeof(iimValue) is safe.
			   xmpValue.year, xmpValue.month, xmpValue.day );

	iptc->SetDataSet_UTF8 ( dateID, iimValue, 8 );

	// Set the IIM time portion as HHMMSS+HHMM (or -HHMM). Allow a missing time zone.

	if ( xmpValue.hasTimeZone )  {
		snprintf ( iimValue, sizeof(iimValue), "%02d%02d%02d%c%02d%02d",	// AUDIT: Use of sizeof(iimValue) is safe.
				   xmpValue.hour, xmpValue.minute, xmpValue.second,
				   ((xmpValue.tzSign == kXMP_TimeWestOfUTC) ? '-' : '+'), xmpValue.tzHour, xmpValue.tzMinute );
		iptc->SetDataSet_UTF8 ( timeID, iimValue, 11 );
	} else if ( xmpValue.hasTime ) {
		snprintf ( iimValue, sizeof(iimValue), "%02d%02d%02d",	// AUDIT: Use of sizeof(iimValue) is safe.
				   xmpValue.hour, xmpValue.minute, xmpValue.second );
		iptc->SetDataSet_UTF8 ( timeID, iimValue, 6 );
	} else {
		iptc->DeleteDataSet ( timeID );
	}

}	// ExportIPTC_Date

// =================================================================================================
// PhotoDataUtils::ExportIPTC
// ==========================

void PhotoDataUtils::ExportIPTC ( const SXMPMeta & xmp, IPTC_Manager * iptc )
{

	for ( size_t i = 0; kKnownDataSets[i].dsNum != 255; ++i ) {

		try {	// Don't let errors with one stop the others.

			const DataSetCharacteristics & thisDS = kKnownDataSets[i];
			if ( thisDS.mapForm >= kIPTC_UnmappedText ) continue;

			switch ( thisDS.mapForm ) {

				case kIPTC_MapSimple :
					ExportIPTC_Simple ( xmp, iptc, thisDS.xmpNS, thisDS.xmpProp, thisDS.dsNum );
					break;

				case kIPTC_MapLangAlt :
					ExportIPTC_LangAlt ( xmp, iptc, thisDS.xmpNS, thisDS.xmpProp, thisDS.dsNum );
					break;

				case kIPTC_MapArray :
					ExportIPTC_Array ( xmp, iptc, thisDS.xmpNS, thisDS.xmpProp, thisDS.dsNum );
					break;

				case kIPTC_MapSpecial :
					if ( thisDS.dsNum == kIPTC_DateCreated ) {
						ExportIPTC_Date ( thisDS.dsNum, xmp, iptc );
					} else if ( thisDS.dsNum == kIPTC_IntellectualGenre ) {
						ExportIPTC_IntellectualGenre ( xmp, iptc );
					} else if ( thisDS.dsNum == kIPTC_SubjectCode ) {
						ExportIPTC_SubjectCode ( xmp, iptc );
					} else {
						XMP_Assert ( false );	// Catch mapping errors.
					}
					break;
				
				case kIPTC_Map3Way :	// The 3 way case is special for import, not for export.
					if ( thisDS.dsNum == kIPTC_DigitalCreateDate ) {
						// ! Special case: Don't create IIM DigitalCreateDate. This can avoid PSD
						// ! full rewrite due to new mapping from xmp:CreateDate.
						if ( iptc->GetDataSet ( thisDS.dsNum, 0 ) > 0 ) ExportIPTC_Date ( thisDS.dsNum, xmp, iptc );
					} else if ( thisDS.dsNum == kIPTC_Creator ) {
						ExportIPTC_Array ( xmp, iptc, kXMP_NS_DC, "creator", kIPTC_Creator );
					} else if ( thisDS.dsNum == kIPTC_CopyrightNotice ) {
						ExportIPTC_LangAlt ( xmp, iptc, kXMP_NS_DC, "rights", kIPTC_CopyrightNotice );
					} else if ( thisDS.dsNum == kIPTC_Description ) {
						ExportIPTC_LangAlt ( xmp, iptc, kXMP_NS_DC, "description", kIPTC_Description );
					} else {
						XMP_Assert ( false );	// Catch mapping errors.
					}

			}

		} catch ( ... ) {

			// Do nothing, let other exports proceed.
			// ? Notify client?

		}

	}

}	// PhotoDataUtils::ExportIPTC;

// =================================================================================================
// PhotoDataUtils::ExportPSIR
// ==========================
//
// There are only 2 standalone Photoshop image resources for XMP properties:
//    1034 - Copyright Flag - 0/1 Boolean mapped to xmpRights:Marked.
//    1035 - Copyright URL - Local OS text mapped to xmpRights:WebStatement.

// ! We don't bother with the CR<->LF normalization for xmpRights:WebStatement. Very little chance
// ! of having a raw CR character in a URI.

void PhotoDataUtils::ExportPSIR ( const SXMPMeta & xmp, PSIR_Manager * psir )
{
	bool found;
	std::string utf8Value;

	try {	// Don't let errors with one stop the others.
		found = xmp.GetProperty ( kXMP_NS_XMP_Rights, "Marked", &utf8Value, 0 );
		if ( ! found ) {
			psir->DeleteImgRsrc ( kPSIR_CopyrightFlag );
		} else {
			bool copyrighted = SXMPUtils::ConvertToBool ( utf8Value );
			psir->SetImgRsrc ( kPSIR_CopyrightFlag, &copyrighted, 1 );
		}
	} catch ( ... ) {
		// Do nothing, let other exports proceed.
		// ? Notify client?
	}

	try {	// Don't let errors with one stop the others.
		found = xmp.GetProperty ( kXMP_NS_XMP_Rights, "WebStatement", &utf8Value, 0 );
		if ( ! found ) {
			psir->DeleteImgRsrc ( kPSIR_CopyrightURL );
		} else if ( ! ignoreLocalText ) {
			std::string localValue;
			ReconcileUtils::UTF8ToLocal ( utf8Value.c_str(), utf8Value.size(), &localValue );
			psir->SetImgRsrc ( kPSIR_CopyrightURL, localValue.c_str(), (XMP_Uns32)localValue.size() );
		} else if ( ReconcileUtils::IsASCII ( utf8Value.c_str(), utf8Value.size() ) ) {
			psir->SetImgRsrc ( kPSIR_CopyrightURL, utf8Value.c_str(), (XMP_Uns32)utf8Value.size() );
		} else {
			psir->DeleteImgRsrc ( kPSIR_CopyrightURL );
		}
	} catch ( ... ) {
		// Do nothing, let other exports proceed.
		// ? Notify client?
	}

}	// PhotoDataUtils::ExportPSIR;