summaryrefslogtreecommitdiff
path: root/gpr/source/lib/xmp_core/XMPUtils.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'gpr/source/lib/xmp_core/XMPUtils.cpp')
-rw-r--r--gpr/source/lib/xmp_core/XMPUtils.cpp2000
1 files changed, 2000 insertions, 0 deletions
diff --git a/gpr/source/lib/xmp_core/XMPUtils.cpp b/gpr/source/lib/xmp_core/XMPUtils.cpp
new file mode 100644
index 0000000..af65dc9
--- /dev/null
+++ b/gpr/source/lib/xmp_core/XMPUtils.cpp
@@ -0,0 +1,2000 @@
+// =================================================================================================
+// Copyright 2003 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" // ! This must be the first include!
+#include "XMPCore_Impl.hpp"
+
+#include "XMPUtils.hpp"
+
+#include "md5.h"
+
+#include <map>
+
+#include <time.h>
+#include <string.h>
+#include <stdlib.h>
+#include <locale.h>
+#include <errno.h>
+
+#include <stdio.h> // For snprintf.
+
+#if XMP_WinBuild
+ #pragma warning ( disable : 4800 ) // forcing value to bool 'true' or 'false' (performance warning)
+ #pragma warning ( disable : 4996 ) // '...' was declared deprecated
+#endif
+
+// =================================================================================================
+// Local Types and Constants
+// =========================
+
+static const char * sBase64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+// =================================================================================================
+// Local Utilities
+// ===============
+
+// -------------------------------------------------------------------------------------------------
+// ANSI Time Functions
+// -------------------
+//
+// A bit of hackery to use the best available time functions. Mac, UNIX and iOS have thread safe versions
+// of gmtime and localtime.
+
+#if XMP_MacBuild | XMP_UNIXBuild | XMP_iOSBuild
+
+ typedef time_t ansi_tt;
+ typedef struct tm ansi_tm;
+
+ #define ansi_time time
+ #define ansi_mktime mktime
+ #define ansi_difftime difftime
+
+ #define ansi_gmtime gmtime_r
+ #define ansi_localtime localtime_r
+
+#elif XMP_WinBuild
+
+ // ! VS.Net 2003 (VC7) does not provide thread safe versions of gmtime and localtime.
+ // ! VS.Net 2005 (VC8) inverts the parameters for the safe versions of gmtime and localtime.
+
+ typedef time_t ansi_tt;
+ typedef struct tm ansi_tm;
+
+ #define ansi_time time
+ #define ansi_mktime mktime
+ #define ansi_difftime difftime
+
+ #if defined(_MSC_VER) && (_MSC_VER >= 1400)
+ #define ansi_gmtime(tt,tm) gmtime_s ( tm, tt )
+ #define ansi_localtime(tt,tm) localtime_s ( tm, tt )
+ #else
+ static inline void ansi_gmtime ( const ansi_tt * ttTime, ansi_tm * tmTime )
+ {
+ ansi_tm * tmx = gmtime ( ttTime ); // ! Hope that there is no race!
+ if ( tmx == 0 ) XMP_Throw ( "Failure from ANSI C gmtime function", kXMPErr_ExternalFailure );
+ *tmTime = *tmx;
+ }
+ static inline void ansi_localtime ( const ansi_tt * ttTime, ansi_tm * tmTime )
+ {
+ ansi_tm * tmx = localtime ( ttTime ); // ! Hope that there is no race!
+ if ( tmx == 0 ) XMP_Throw ( "Failure from ANSI C localtime function", kXMPErr_ExternalFailure );
+ *tmTime = *tmx;
+ }
+ #endif
+
+#endif
+
+// -------------------------------------------------------------------------------------------------
+// VerifyDateTimeFlags
+// -------------------
+
+static void
+VerifyDateTimeFlags ( XMP_DateTime * dt )
+{
+
+ if ( (dt->year != 0) || (dt->month != 0) || (dt->day != 0) ) dt->hasDate = true;
+ if ( (dt->hour != 0) || (dt->minute != 0) || (dt->second != 0) || (dt->nanoSecond != 0) ) dt->hasTime = true;
+ if ( (dt->tzSign != 0) || (dt->tzHour != 0) || (dt->tzMinute != 0) ) dt->hasTimeZone = true;
+ if ( dt->hasTimeZone ) dt->hasTime = true; // ! Don't combine with above line, UTC has zero values.
+
+} // VerifyDateTimeFlags
+
+// -------------------------------------------------------------------------------------------------
+// IsLeapYear
+// ----------
+
+static bool
+IsLeapYear ( long year )
+{
+
+ if ( year < 0 ) year = -year + 1; // Fold the negative years, assuming there is a year 0.
+
+ if ( (year % 4) != 0 ) return false; // Not a multiple of 4.
+ if ( (year % 100) != 0 ) return true; // A multiple of 4 but not a multiple of 100.
+ if ( (year % 400) == 0 ) return true; // A multiple of 400.
+
+ return false; // A multiple of 100 but not a multiple of 400.
+
+} // IsLeapYear
+
+// -------------------------------------------------------------------------------------------------
+// DaysInMonth
+// -----------
+
+static int
+DaysInMonth ( XMP_Int32 year, XMP_Int32 month )
+{
+
+ static short daysInMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+ // Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
+
+ int days = daysInMonth [ month ];
+ if ( (month == 2) && IsLeapYear ( year ) ) days += 1;
+
+ return days;
+
+} // DaysInMonth
+
+// -------------------------------------------------------------------------------------------------
+// AdjustTimeOverflow
+// ------------------
+
+static void
+AdjustTimeOverflow ( XMP_DateTime * time )
+{
+ enum { kBillion = 1000*1000*1000L };
+
+ // ----------------------------------------------------------------------------------------------
+ // To be safe against pathalogical overflow we first adjust from month to second, then from
+ // nanosecond back up to month. This leaves each value closer to zero before propagating into it.
+ // For example if the hour and minute are both near max, adjusting minutes first can cause the
+ // hour to overflow.
+
+ // ! Photoshop 8 creates "time only" values with zeros for year, month, and day.
+
+ if ( (time->year != 0) || (time->month != 0) || (time->day != 0) ) {
+
+ while ( time->month < 1 ) {
+ time->year -= 1;
+ time->month += 12;
+ }
+
+ while ( time->month > 12 ) {
+ time->year += 1;
+ time->month -= 12;
+ }
+
+ while ( time->day < 1 ) {
+ time->month -= 1;
+ if ( time->month < 1 ) { // ! Keep the months in range for indexing daysInMonth!
+ time->year -= 1;
+ time->month += 12;
+ }
+ time->day += DaysInMonth ( time->year, time->month ); // ! Decrement month before so index here is right!
+ }
+
+ while ( time->day > DaysInMonth ( time->year, time->month ) ) {
+ time->day -= DaysInMonth ( time->year, time->month ); // ! Increment month after so index here is right!
+ time->month += 1;
+ if ( time->month > 12 ) {
+ time->year += 1;
+ time->month -= 12;
+ }
+ }
+
+ }
+
+ while ( time->hour < 0 ) {
+ time->day -= 1;
+ time->hour += 24;
+ }
+
+ while ( time->hour >= 24 ) {
+ time->day += 1;
+ time->hour -= 24;
+ }
+
+ while ( time->minute < 0 ) {
+ time->hour -= 1;
+ time->minute += 60;
+ }
+
+ while ( time->minute >= 60 ) {
+ time->hour += 1;
+ time->minute -= 60;
+ }
+
+ while ( time->second < 0 ) {
+ time->minute -= 1;
+ time->second += 60;
+ }
+
+ while ( time->second >= 60 ) {
+ time->minute += 1;
+ time->second -= 60;
+ }
+
+ while ( time->nanoSecond < 0 ) {
+ time->second -= 1;
+ time->nanoSecond += kBillion;
+ }
+
+ while ( time->nanoSecond >= kBillion ) {
+ time->second += 1;
+ time->nanoSecond -= kBillion;
+ }
+
+ while ( time->second < 0 ) {
+ time->minute -= 1;
+ time->second += 60;
+ }
+
+ while ( time->second >= 60 ) {
+ time->minute += 1;
+ time->second -= 60;
+ }
+
+ while ( time->minute < 0 ) {
+ time->hour -= 1;
+ time->minute += 60;
+ }
+
+ while ( time->minute >= 60 ) {
+ time->hour += 1;
+ time->minute -= 60;
+ }
+
+ while ( time->hour < 0 ) {
+ time->day -= 1;
+ time->hour += 24;
+ }
+
+ while ( time->hour >= 24 ) {
+ time->day += 1;
+ time->hour -= 24;
+ }
+
+ if ( (time->year != 0) || (time->month != 0) || (time->day != 0) ) {
+
+ while ( time->month < 1 ) { // Make sure the months are OK first, for DaysInMonth.
+ time->year -= 1;
+ time->month += 12;
+ }
+
+ while ( time->month > 12 ) {
+ time->year += 1;
+ time->month -= 12;
+ }
+
+ while ( time->day < 1 ) {
+ time->month -= 1;
+ if ( time->month < 1 ) {
+ time->year -= 1;
+ time->month += 12;
+ }
+ time->day += DaysInMonth ( time->year, time->month );
+ }
+
+ while ( time->day > DaysInMonth ( time->year, time->month ) ) {
+ time->day -= DaysInMonth ( time->year, time->month );
+ time->month += 1;
+ if ( time->month > 12 ) {
+ time->year += 1;
+ time->month -= 12;
+ }
+ }
+
+ }
+
+} // AdjustTimeOverflow
+
+// -------------------------------------------------------------------------------------------------
+// GatherInt
+// ---------
+//
+// Gather into a 64-bit value in order to easily check for overflow. Using a 32-bit value and
+// checking for negative isn't reliable, the "*10" part can wrap around to a low positive value.
+
+static XMP_Int32
+GatherInt ( XMP_StringPtr strValue, size_t * _pos, const char * errMsg )
+{
+ size_t pos = *_pos;
+ XMP_Int64 value = 0;
+
+ enum { kMaxSInt32 = 0x7FFFFFFF };
+
+ for ( char ch = strValue[pos]; ('0' <= ch) && (ch <= '9'); ++pos, ch = strValue[pos] ) {
+ value = (value * 10) + (ch - '0');
+ if ( value > kMaxSInt32 ) XMP_Throw ( errMsg, kXMPErr_BadValue );
+ }
+
+ if ( pos == *_pos ) XMP_Throw ( errMsg, kXMPErr_BadParam );
+ *_pos = pos;
+ return (XMP_Int32)value;
+
+} // GatherInt
+
+// -------------------------------------------------------------------------------------------------
+
+static void FormatFullDateTime ( XMP_DateTime & tempDate, char * buffer, size_t bufferLen )
+{
+
+ AdjustTimeOverflow ( &tempDate ); // Make sure all time parts are in range.
+
+ if ( (tempDate.second == 0) && (tempDate.nanoSecond == 0) ) {
+
+ // Output YYYY-MM-DDThh:mmTZD.
+ snprintf ( buffer, bufferLen, "%.4d-%02d-%02dT%02d:%02d", // AUDIT: Callers pass sizeof(buffer).
+ tempDate.year, tempDate.month, tempDate.day, tempDate.hour, tempDate.minute );
+
+ } else if ( tempDate.nanoSecond == 0 ) {
+
+ // Output YYYY-MM-DDThh:mm:ssTZD.
+ snprintf ( buffer, bufferLen, "%.4d-%02d-%02dT%02d:%02d:%02d", // AUDIT: Callers pass sizeof(buffer).
+ tempDate.year, tempDate.month, tempDate.day,
+ tempDate.hour, tempDate.minute, tempDate.second );
+
+ } else {
+
+ // Output YYYY-MM-DDThh:mm:ss.sTZD.
+ snprintf ( buffer, bufferLen, "%.4d-%02d-%02dT%02d:%02d:%02d.%09d", // AUDIT: Callers pass sizeof(buffer).
+ tempDate.year, tempDate.month, tempDate.day,
+ tempDate.hour, tempDate.minute, tempDate.second, tempDate.nanoSecond );
+ buffer[bufferLen - 1] = 0; // AUDIT warning C6053: make sure string is terminated. buffer is already filled with 0 from caller
+ for ( size_t i = strlen(buffer)-1; buffer[i] == '0'; --i ) buffer[i] = 0; // Trim excess digits.
+ }
+
+} // FormatFullDateTime
+
+// -------------------------------------------------------------------------------------------------
+// DecodeBase64Char
+// ----------------
+
+// The decode mapping:
+//
+// encoded encoded raw
+// char value value
+// ------- ------- -----
+// A .. Z 0x41 .. 0x5A 0 .. 25
+// a .. z 0x61 .. 0x7A 26 .. 51
+// 0 .. 9 0x30 .. 0x39 52 .. 61
+// + 0x2B 62
+// / 0x2F 63
+
+static unsigned char
+DecodeBase64Char ( XMP_Uns8 ch )
+{
+
+ if ( ('A' <= ch) && (ch <= 'Z') ) {
+ ch = ch - 'A';
+ } else if ( ('a' <= ch) && (ch <= 'z') ) {
+ ch = ch - 'a' + 26;
+ } else if ( ('0' <= ch) && (ch <= '9') ) {
+ ch = ch - '0' + 52;
+ } else if ( ch == '+' ) {
+ ch = 62;
+ } else if ( ch == '/' ) {
+ ch = 63;
+ } else if ( (ch == ' ') || (ch == kTab) || (ch == kLF) || (ch == kCR) ) {
+ ch = 0xFF; // Will be ignored by the caller.
+ } else {
+ XMP_Throw ( "Invalid base-64 encoded character", kXMPErr_BadParam );
+ }
+
+ return ch;
+
+} // DecodeBase64Char ();
+
+// -------------------------------------------------------------------------------------------------
+// EstimateSizeForJPEG
+// -------------------
+//
+// Estimate the serialized size for the subtree of an XMP_Node. Support for PackageForJPEG.
+
+static size_t
+EstimateSizeForJPEG ( const XMP_Node * xmpNode )
+{
+
+ size_t estSize = 0;
+ size_t nameSize = xmpNode->name.size();
+ bool includeName = (! XMP_PropIsArray ( xmpNode->parent->options ));
+
+ if ( XMP_PropIsSimple ( xmpNode->options ) ) {
+
+ if ( includeName ) estSize += (nameSize + 3); // Assume attribute form.
+ estSize += xmpNode->value.size();
+
+ } else if ( XMP_PropIsArray ( xmpNode->options ) ) {
+
+ // The form of the value portion is: <rdf:Xyz><rdf:li>...</rdf:li>...</rdf:Xyx>
+ if ( includeName ) estSize += (2*nameSize + 5);
+ size_t arraySize = xmpNode->children.size();
+ estSize += 9 + 10; // The rdf:Xyz tags.
+ estSize += arraySize * (8 + 9); // The rdf:li tags.
+ for ( size_t i = 0; i < arraySize; ++i ) {
+ estSize += EstimateSizeForJPEG ( xmpNode->children[i] );
+ }
+
+ } else {
+
+ // The form is: <headTag rdf:parseType="Resource">...fields...</tailTag>
+ if ( includeName ) estSize += (2*nameSize + 5);
+ estSize += 25; // The rdf:parseType="Resource" attribute.
+ size_t fieldCount = xmpNode->children.size();
+ for ( size_t i = 0; i < fieldCount; ++i ) {
+ estSize += EstimateSizeForJPEG ( xmpNode->children[i] );
+ }
+
+ }
+
+ return estSize;
+
+} // EstimateSizeForJPEG
+
+// -------------------------------------------------------------------------------------------------
+// MoveOneProperty
+// ---------------
+
+static bool MoveOneProperty ( XMPMeta & stdXMP, XMPMeta * extXMP,
+ XMP_StringPtr schemaURI, XMP_StringPtr propName )
+{
+
+ XMP_Node * propNode = 0;
+ XMP_NodePtrPos stdPropPos;
+
+ XMP_Node * stdSchema = FindSchemaNode ( &stdXMP.tree, schemaURI, kXMP_ExistingOnly, 0 );
+ if ( stdSchema != 0 ) {
+ propNode = FindChildNode ( stdSchema, propName, kXMP_ExistingOnly, &stdPropPos );
+ }
+ if ( propNode == 0 ) return false;
+
+ XMP_Node * extSchema = FindSchemaNode ( &extXMP->tree, schemaURI, kXMP_CreateNodes );
+
+ propNode->parent = extSchema;
+
+ extSchema->options &= ~kXMP_NewImplicitNode;
+ extSchema->children.push_back ( propNode );
+
+ stdSchema->children.erase ( stdPropPos );
+ DeleteEmptySchema ( stdSchema );
+
+ return true;
+
+} // MoveOneProperty
+
+// -------------------------------------------------------------------------------------------------
+// CreateEstimatedSizeMap
+// ----------------------
+
+#ifndef Trace_PackageForJPEG
+ #define Trace_PackageForJPEG 0
+#endif
+
+typedef std::pair < XMP_VarString*, XMP_VarString* > StringPtrPair;
+typedef std::multimap < size_t, StringPtrPair > PropSizeMap;
+
+static void CreateEstimatedSizeMap ( XMPMeta & stdXMP, PropSizeMap * propSizes )
+{
+ #if Trace_PackageForJPEG
+ printf ( " Creating top level property map:\n" );
+ #endif
+
+ for ( size_t s = stdXMP.tree.children.size(); s > 0; --s ) {
+
+ XMP_Node * stdSchema = stdXMP.tree.children[s-1];
+
+ for ( size_t p = stdSchema->children.size(); p > 0; --p ) {
+
+ XMP_Node * stdProp = stdSchema->children[p-1];
+ if ( (stdSchema->name == kXMP_NS_XMP_Note) &&
+ (stdProp->name == "xmpNote:HasExtendedXMP") ) continue; // ! Don't move xmpNote:HasExtendedXMP.
+
+ size_t propSize = EstimateSizeForJPEG ( stdProp );
+ StringPtrPair namePair ( &stdSchema->name, &stdProp->name );
+ PropSizeMap::value_type mapValue ( propSize, namePair );
+
+ (void) propSizes->insert ( propSizes->upper_bound ( propSize ), mapValue );
+ #if Trace_PackageForJPEG
+ printf ( " %d bytes, %s in %s\n", propSize, stdProp->name.c_str(), stdSchema->name.c_str() );
+ #endif
+
+ }
+
+ }
+
+} // CreateEstimatedSizeMap
+
+// -------------------------------------------------------------------------------------------------
+// MoveLargestProperty
+// -------------------
+
+static size_t MoveLargestProperty ( XMPMeta & stdXMP, XMPMeta * extXMP, PropSizeMap & propSizes )
+{
+ XMP_Assert ( ! propSizes.empty() );
+
+ #if 0
+ // *** Xocde 2.3 on Mac OS X 10.4.7 seems to have a bug where this does not pick the last
+ // *** item in the map. We'll just avoid it on all platforms until thoroughly tested.
+ PropSizeMap::iterator lastPos = propSizes.end();
+ --lastPos; // Move to the actual last item.
+ #else
+ PropSizeMap::iterator lastPos = propSizes.begin();
+ PropSizeMap::iterator nextPos = lastPos;
+ for ( ++nextPos; nextPos != propSizes.end(); ++nextPos ) lastPos = nextPos;
+ #endif
+
+ size_t propSize = lastPos->first;
+ const char * schemaURI = lastPos->second.first->c_str();
+ const char * propName = lastPos->second.second->c_str();
+
+ #if Trace_PackageForJPEG
+ printf ( " Move %s, %d bytes\n", propName, propSize );
+ #endif
+
+ bool moved = MoveOneProperty ( stdXMP, extXMP, schemaURI, propName );
+ XMP_Assert ( moved );
+
+ propSizes.erase ( lastPos );
+ return propSize;
+
+} // MoveLargestProperty
+
+// =================================================================================================
+// Class Static Functions
+// ======================
+
+// -------------------------------------------------------------------------------------------------
+// Initialize
+// ----------
+
+/* class static */ bool
+XMPUtils::Initialize()
+{
+
+ if ( WhiteSpaceStrPtr == NULL ) {
+ WhiteSpaceStrPtr = new std::string();
+ WhiteSpaceStrPtr->append( " \t\n\r" );
+ }
+ return true;
+
+} // Initialize
+
+// -------------------------------------------------------------------------------------------------
+// Terminate
+// ---------
+
+/* class static */ void
+XMPUtils::Terminate() RELEASE_NO_THROW
+{
+
+ delete WhiteSpaceStrPtr;
+ WhiteSpaceStrPtr = NULL;
+ return;
+
+} // Terminate
+
+// -------------------------------------------------------------------------------------------------
+// ComposeArrayItemPath
+// --------------------
+//
+// Return "arrayName[index]".
+
+/* class static */ void
+XMPUtils::ComposeArrayItemPath ( XMP_StringPtr schemaNS,
+ XMP_StringPtr arrayName,
+ XMP_Index itemIndex,
+ XMP_VarString * _fullPath )
+{
+ XMP_Assert ( schemaNS != 0 ); // Enforced by wrapper.
+ XMP_Assert ( (arrayName != 0) && (*arrayName != 0) ); // Enforced by wrapper.
+ XMP_Assert ( _fullPath != 0 ); // Enforced by wrapper.
+
+ XMP_ExpandedXPath expPath; // Just for side effects to check namespace and basic path.
+ ExpandXPath ( schemaNS, arrayName, &expPath );
+
+ if ( (itemIndex < 0) && (itemIndex != kXMP_ArrayLastItem) ) XMP_Throw ( "Array index out of bounds", kXMPErr_BadParam );
+
+ XMP_StringLen reserveLen = strlen(arrayName) + 2 + 32; // Room plus padding.
+
+ XMP_VarString fullPath; // ! Allow for arrayName to be the incoming _fullPath.c_str().
+ fullPath.reserve ( reserveLen );
+ fullPath = arrayName;
+
+ if ( itemIndex == kXMP_ArrayLastItem ) {
+ fullPath += "[last()]";
+ } else {
+ // AUDIT: Using sizeof(buffer) for the snprintf length is safe.
+ char buffer [32]; // A 32 byte buffer is plenty, even for a 64-bit integer.
+ snprintf ( buffer, sizeof(buffer), "[%d]", itemIndex );
+ fullPath += buffer;
+ }
+
+ *_fullPath = fullPath;
+
+} // ComposeArrayItemPath
+
+// -------------------------------------------------------------------------------------------------
+// ComposeStructFieldPath
+// ----------------------
+//
+// Return "structName/ns:fieldName".
+
+/* class static */ void
+XMPUtils::ComposeStructFieldPath ( XMP_StringPtr schemaNS,
+ XMP_StringPtr structName,
+ XMP_StringPtr fieldNS,
+ XMP_StringPtr fieldName,
+ XMP_VarString * _fullPath )
+{
+ XMP_Assert ( (schemaNS != 0) && (fieldNS != 0) ); // Enforced by wrapper.
+ XMP_Assert ( (structName != 0) && (*structName != 0) ); // Enforced by wrapper.
+ XMP_Assert ( (fieldName != 0) && (*fieldName != 0) ); // Enforced by wrapper.
+ XMP_Assert ( _fullPath != 0 ); // Enforced by wrapper.
+
+ XMP_ExpandedXPath expPath; // Just for side effects to check namespace and basic path.
+ ExpandXPath ( schemaNS, structName, &expPath );
+
+ XMP_ExpandedXPath fieldPath;
+ ExpandXPath ( fieldNS, fieldName, &fieldPath );
+ if ( fieldPath.size() != 2 ) XMP_Throw ( "The fieldName must be simple", kXMPErr_BadXPath );
+
+ XMP_StringLen reserveLen = strlen(structName) + fieldPath[kRootPropStep].step.size() + 1;
+
+ XMP_VarString fullPath; // ! Allow for arrayName to be the incoming _fullPath.c_str().
+ fullPath.reserve ( reserveLen );
+ fullPath = structName;
+ fullPath += '/';
+ fullPath += fieldPath[kRootPropStep].step;
+
+ *_fullPath = fullPath;
+
+} // ComposeStructFieldPath
+
+// -------------------------------------------------------------------------------------------------
+// ComposeQualifierPath
+// --------------------
+//
+// Return "propName/?ns:qualName".
+
+/* class static */ void
+XMPUtils::ComposeQualifierPath ( XMP_StringPtr schemaNS,
+ XMP_StringPtr propName,
+ XMP_StringPtr qualNS,
+ XMP_StringPtr qualName,
+ XMP_VarString * _fullPath )
+{
+ XMP_Assert ( (schemaNS != 0) && (qualNS != 0) ); // Enforced by wrapper.
+ XMP_Assert ( (propName != 0) && (*propName != 0) ); // Enforced by wrapper.
+ XMP_Assert ( (qualName != 0) && (*qualName != 0) ); // Enforced by wrapper.
+ XMP_Assert ( _fullPath != 0 ); // Enforced by wrapper.
+
+ XMP_ExpandedXPath expPath; // Just for side effects to check namespace and basic path.
+ ExpandXPath ( schemaNS, propName, &expPath );
+
+ XMP_ExpandedXPath qualPath;
+ ExpandXPath ( qualNS, qualName, &qualPath );
+ if ( qualPath.size() != 2 ) XMP_Throw ( "The qualifier name must be simple", kXMPErr_BadXPath );
+
+ XMP_StringLen reserveLen = strlen(propName) + qualPath[kRootPropStep].step.size() + 2;
+
+ XMP_VarString fullPath; // ! Allow for arrayName to be the incoming _fullPath.c_str().
+ fullPath.reserve ( reserveLen );
+ fullPath = propName;
+ fullPath += "/?";
+ fullPath += qualPath[kRootPropStep].step;
+
+ *_fullPath = fullPath;
+
+} // ComposeQualifierPath
+
+// -------------------------------------------------------------------------------------------------
+// ComposeLangSelector
+// -------------------
+//
+// Return "arrayName[?xml:lang="lang"]".
+
+// *** #error "handle quotes in the lang - or verify format"
+
+/* class static */ void
+XMPUtils::ComposeLangSelector ( XMP_StringPtr schemaNS,
+ XMP_StringPtr arrayName,
+ XMP_StringPtr _langName,
+ XMP_VarString * _fullPath )
+{
+ XMP_Assert ( schemaNS != 0 ); // Enforced by wrapper.
+ XMP_Assert ( (arrayName != 0) && (*arrayName != 0) ); // Enforced by wrapper.
+ XMP_Assert ( (_langName != 0) && (*_langName != 0) ); // Enforced by wrapper.
+ XMP_Assert ( _fullPath != 0 ); // Enforced by wrapper.
+
+ XMP_ExpandedXPath expPath; // Just for side effects to check namespace and basic path.
+ ExpandXPath ( schemaNS, arrayName, &expPath );
+
+ XMP_VarString langName ( _langName );
+ NormalizeLangValue ( &langName );
+
+ XMP_StringLen reserveLen = strlen(arrayName) + langName.size() + 14;
+
+ XMP_VarString fullPath; // ! Allow for arrayName to be the incoming _fullPath.c_str().
+ fullPath.reserve ( reserveLen );
+ fullPath = arrayName;
+ fullPath += "[?xml:lang=\"";
+ fullPath += langName;
+ fullPath += "\"]";
+
+ *_fullPath = fullPath;
+
+} // ComposeLangSelector
+
+// -------------------------------------------------------------------------------------------------
+// ComposeFieldSelector
+// --------------------
+//
+// Return "arrayName[ns:fieldName="fieldValue"]".
+
+// *** #error "handle quotes in the value"
+
+/* class static */ void
+XMPUtils::ComposeFieldSelector ( XMP_StringPtr schemaNS,
+ XMP_StringPtr arrayName,
+ XMP_StringPtr fieldNS,
+ XMP_StringPtr fieldName,
+ XMP_StringPtr fieldValue,
+ XMP_VarString * _fullPath )
+{
+ XMP_Assert ( (schemaNS != 0) && (fieldNS != 0) && (fieldValue != 0) ); // Enforced by wrapper.
+ XMP_Assert ( (*arrayName != 0) && (*fieldName != 0) ); // Enforced by wrapper.
+ XMP_Assert ( _fullPath != 0 ); // Enforced by wrapper.
+
+ XMP_ExpandedXPath expPath; // Just for side effects to check namespace and basic path.
+ ExpandXPath ( schemaNS, arrayName, &expPath );
+
+ XMP_ExpandedXPath fieldPath;
+ ExpandXPath ( fieldNS, fieldName, &fieldPath );
+ if ( fieldPath.size() != 2 ) XMP_Throw ( "The fieldName must be simple", kXMPErr_BadXPath );
+
+ XMP_StringLen reserveLen = strlen(arrayName) + fieldPath[kRootPropStep].step.size() + strlen(fieldValue) + 5;
+
+ XMP_VarString fullPath; // ! Allow for arrayName to be the incoming _fullPath.c_str().
+ fullPath.reserve ( reserveLen );
+ fullPath = arrayName;
+ fullPath += '[';
+ fullPath += fieldPath[kRootPropStep].step;
+ fullPath += "=\"";
+ fullPath += fieldValue;
+ fullPath += "\"]";
+
+ *_fullPath = fullPath;
+
+} // ComposeFieldSelector
+
+// -------------------------------------------------------------------------------------------------
+// ConvertFromBool
+// ---------------
+
+/* class static */ void
+XMPUtils::ConvertFromBool ( bool binValue,
+ XMP_VarString * strValue )
+{
+ XMP_Assert ( strValue != 0 ); // Enforced by wrapper.
+
+ if ( binValue ) {
+ *strValue = kXMP_TrueStr;
+ } else {
+ *strValue = kXMP_FalseStr;
+ }
+
+} // ConvertFromBool
+
+// -------------------------------------------------------------------------------------------------
+// ConvertFromInt
+// --------------
+
+/* class static */ void
+XMPUtils::ConvertFromInt ( XMP_Int32 binValue,
+ XMP_StringPtr format,
+ XMP_VarString * strValue )
+{
+ XMP_Assert ( (format != 0) && (strValue != 0) ); // Enforced by wrapper.
+
+ strValue->erase();
+ if ( *format == 0 ) format = "%d";
+
+ // AUDIT: Using sizeof(buffer) for the snprintf length is safe.
+ char buffer [32]; // Big enough for a 64-bit integer;
+ snprintf ( buffer, sizeof(buffer), format, binValue );
+
+ *strValue = buffer;
+
+} // ConvertFromInt
+
+// -------------------------------------------------------------------------------------------------
+// ConvertFromInt64
+// ----------------
+
+/* class static */ void
+XMPUtils::ConvertFromInt64 ( XMP_Int64 binValue,
+ XMP_StringPtr format,
+ XMP_VarString * strValue )
+{
+ XMP_Assert ( (format != 0) && (strValue != 0) ); // Enforced by wrapper.
+
+ strValue->erase();
+ if ( *format == 0 ) format = "%lld";
+
+ // AUDIT: Using sizeof(buffer) for the snprintf length is safe.
+ char buffer [32]; // Big enough for a 64-bit integer;
+ snprintf ( buffer, sizeof(buffer), format, binValue );
+
+ *strValue = buffer;
+
+} // ConvertFromInt64
+
+// -------------------------------------------------------------------------------------------------
+// ConvertFromFloat
+// ----------------
+
+/* class static */ void
+XMPUtils::ConvertFromFloat ( double binValue,
+ XMP_StringPtr format,
+ XMP_VarString * strValue )
+{
+ XMP_Assert ( (format != 0) && (strValue != 0) ); // Enforced by wrapper.
+
+ strValue->erase();
+ if ( *format == 0 ) format = "%f";
+
+ // AUDIT: Using sizeof(buffer) for the snprintf length is safe.
+ char buffer [64]; // Ought to be plenty big enough.
+ snprintf ( buffer, sizeof(buffer), format, binValue );
+
+ *strValue = buffer;
+
+} // ConvertFromFloat
+
+// -------------------------------------------------------------------------------------------------
+// ConvertFromDate
+// ---------------
+//
+// Format a date-time string according to ISO 8601 and http://www.w3.org/TR/NOTE-datetime:
+// YYYY
+// YYYY-MM
+// YYYY-MM-DD
+// YYYY-MM-DDThh:mmTZD
+// YYYY-MM-DDThh:mm:ssTZD
+// YYYY-MM-DDThh:mm:ss.sTZD
+//
+// YYYY = four-digit year
+// MM = two-digit month (01=January, etc.)
+// DD = two-digit day of month (01 through 31)
+// hh = two digits of hour (00 through 23)
+// mm = two digits of minute (00 through 59)
+// ss = two digits of second (00 through 59)
+// s = one or more digits representing a decimal fraction of a second
+// TZD = time zone designator (Z or +hh:mm or -hh:mm)
+//
+// Note that ISO 8601 does not seem to allow years less than 1000 or greater than 9999. We allow
+// any year, even negative ones. The year is formatted as "%.4d". The TZD is also optional in XMP,
+// even though required in the W3C profile. Finally, Photoshop 8 (CS) sometimes created time-only
+// values so we tolerate that.
+
+/* class static */ void
+XMPUtils::ConvertFromDate ( const XMP_DateTime & _inValue,
+ XMP_VarString * strValue )
+{
+ XMP_Assert ( strValue != 0 ); // Enforced by wrapper.
+
+ char buffer [100]; // Plenty long enough.
+ memset( buffer, 0, 100);
+
+ // Pick the format, use snprintf to format into a local buffer, assign to static output string.
+ // Don't use AdjustTimeOverflow at the start, that will wipe out zero month or day values.
+
+ // ! Photoshop 8 creates "time only" values with zeros for year, month, and day.
+
+ XMP_DateTime binValue = _inValue;
+ VerifyDateTimeFlags ( &binValue );
+
+ // Temporary fix for bug 1269463, silently fix out of range month or day.
+
+ if ( binValue.month == 0 ) {
+ if ( (binValue.day != 0) || binValue.hasTime ) binValue.month = 1;
+ } else {
+ if ( binValue.month < 1 ) binValue.month = 1;
+ if ( binValue.month > 12 ) binValue.month = 12;
+ }
+
+ if ( binValue.day == 0 ) {
+ if ( binValue.hasTime ) binValue.day = 1;
+ } else {
+ if ( binValue.day < 1 ) binValue.day = 1;
+ if ( binValue.day > 31 ) binValue.day = 31;
+ }
+
+ // Now carry on with the original logic.
+
+ if ( binValue.month == 0 ) {
+
+ // Output YYYY if all else is zero, otherwise output a full string for the quasi-bogus
+ // "time only" values from Photoshop CS.
+ if ( (binValue.day == 0) && (! binValue.hasTime) ) {
+ snprintf ( buffer, sizeof(buffer), "%.4d", binValue.year ); // AUDIT: Using sizeof for snprintf length is safe.
+ } else if ( (binValue.year == 0) && (binValue.day == 0) ) {
+ FormatFullDateTime ( binValue, buffer, sizeof(buffer) );
+ } else {
+ XMP_Throw ( "Invalid partial date", kXMPErr_BadParam);
+ }
+
+ } else if ( binValue.day == 0 ) {
+
+ // Output YYYY-MM.
+ if ( (binValue.month < 1) || (binValue.month > 12) ) XMP_Throw ( "Month is out of range", kXMPErr_BadParam);
+ if ( binValue.hasTime ) XMP_Throw ( "Invalid partial date, non-zeros after zero month and day", kXMPErr_BadParam);
+ snprintf ( buffer, sizeof(buffer), "%.4d-%02d", binValue.year, binValue.month ); // AUDIT: Using sizeof for snprintf length is safe.
+
+ } else if ( ! binValue.hasTime ) {
+
+ // Output YYYY-MM-DD.
+ if ( (binValue.month < 1) || (binValue.month > 12) ) XMP_Throw ( "Month is out of range", kXMPErr_BadParam);
+ if ( (binValue.day < 1) || (binValue.day > 31) ) XMP_Throw ( "Day is out of range", kXMPErr_BadParam);
+ snprintf ( buffer, sizeof(buffer), "%.4d-%02d-%02d", binValue.year, binValue.month, binValue.day ); // AUDIT: Using sizeof for snprintf length is safe.
+
+ } else {
+
+ FormatFullDateTime ( binValue, buffer, sizeof(buffer) );
+
+ }
+
+ strValue->assign ( buffer );
+
+ if ( binValue.hasTimeZone ) {
+
+ if ( (binValue.tzHour < 0) || (binValue.tzHour > 23) ||
+ (binValue.tzMinute < 0 ) || (binValue.tzMinute > 59) ||
+ (binValue.tzSign < -1) || (binValue.tzSign > +1) ||
+ ((binValue.tzSign == 0) && ((binValue.tzHour != 0) || (binValue.tzMinute != 0))) ) {
+ XMP_Throw ( "Invalid time zone values", kXMPErr_BadParam );
+ }
+
+ if ( binValue.tzSign == 0 ) {
+ *strValue += 'Z';
+ } else {
+ snprintf ( buffer, sizeof(buffer), "+%02d:%02d", binValue.tzHour, binValue.tzMinute ); // AUDIT: Using sizeof for snprintf length is safe.
+ if ( binValue.tzSign < 0 ) buffer[0] = '-';
+ *strValue += buffer;
+ }
+
+ }
+
+} // ConvertFromDate
+
+// -------------------------------------------------------------------------------------------------
+// ConvertToBool
+// -------------
+//
+// Formally the string value should be "True" or "False", but we should be more flexible here. Map
+// the string to lower case. Allow any of "true", "false", "t", "f", "1", or "0".
+
+/* class static */ bool
+XMPUtils::ConvertToBool ( XMP_StringPtr strValue )
+{
+ if ( (strValue == 0) || (*strValue == 0) ) XMP_Throw ( "Empty convert-from string", kXMPErr_BadValue );
+
+ bool result = false;
+ XMP_VarString strObj ( strValue );
+
+ for ( XMP_VarStringPos ch = strObj.begin(); ch != strObj.end(); ++ch ) {
+ if ( ('A' <= *ch) && (*ch <= 'Z') ) *ch += 0x20;
+ }
+
+ if ( (strObj == "true") || (strObj == "t") || (strObj == "1") ) {
+ result = true;
+ } else if ( (strObj == "false") || (strObj == "f") || (strObj == "0") ) {
+ result = false;
+ } else {
+ XMP_Throw ( "Invalid Boolean string", kXMPErr_BadParam );
+ }
+
+ return result;
+
+} // ConvertToBool
+
+// -------------------------------------------------------------------------------------------------
+// ConvertToInt
+// ------------
+
+/* class static */ XMP_Int32
+XMPUtils::ConvertToInt ( XMP_StringPtr strValue )
+{
+ if ( (strValue == 0) || (*strValue == 0) ) XMP_Throw ( "Empty convert-from string", kXMPErr_BadValue );
+
+ int count;
+ char nextCh;
+ XMP_Int32 result;
+
+ if ( ! XMP_LitNMatch ( strValue, "0x", 2 ) ) {
+ count = sscanf ( strValue, "%d%c", &result, &nextCh );
+ } else {
+ count = sscanf ( strValue, "%x%c", &result, &nextCh );
+ }
+
+ if ( count != 1 ) XMP_Throw ( "Invalid integer string", kXMPErr_BadParam );
+
+ return result;
+
+} // ConvertToInt
+
+// -------------------------------------------------------------------------------------------------
+// ConvertToInt64
+// --------------
+
+/* class static */ XMP_Int64
+XMPUtils::ConvertToInt64 ( XMP_StringPtr strValue )
+{
+ if ( (strValue == 0) || (*strValue == 0) ) XMP_Throw ( "Empty convert-from string", kXMPErr_BadValue );
+
+ int count;
+ char nextCh;
+ XMP_Int64 result;
+
+ if ( ! XMP_LitNMatch ( strValue, "0x", 2 ) ) {
+ count = sscanf ( strValue, "%lld%c", &result, &nextCh );
+ } else {
+ count = sscanf ( strValue, "%llx%c", &result, &nextCh );
+ }
+
+ if ( count != 1 ) XMP_Throw ( "Invalid integer string", kXMPErr_BadParam );
+
+ return result;
+
+} // ConvertToInt64
+
+// -------------------------------------------------------------------------------------------------
+// ConvertToFloat
+// --------------
+
+/* class static */ double
+XMPUtils::ConvertToFloat ( XMP_StringPtr strValue )
+{
+ if ( (strValue == 0) || (*strValue == 0) ) XMP_Throw ( "Empty convert-from string", kXMPErr_BadValue );
+
+ XMP_VarString oldLocale; // Try to make sure number conversion uses '.' as the decimal point.
+ XMP_StringPtr oldLocalePtr = setlocale ( LC_ALL, 0 );
+ if ( oldLocalePtr != 0 ) {
+ oldLocale.assign ( oldLocalePtr ); // Save the locale to be reset when exiting.
+ setlocale ( LC_ALL, "C" );
+ }
+
+ errno = 0;
+ char * numEnd;
+ double result = strtod ( strValue, &numEnd );
+ int errnoSave = errno; // The setlocale call below might change errno.
+
+ if ( ! oldLocale.empty() ) setlocale ( LC_ALL, oldLocale.c_str() ); // ! Reset locale before possible throw!
+ if ( (errnoSave != 0) || (*numEnd != 0) ) XMP_Throw ( "Invalid float string", kXMPErr_BadParam );
+
+ return result;
+
+} // ConvertToFloat
+
+// -------------------------------------------------------------------------------------------------
+// ConvertToDate
+// -------------
+//
+// Parse a date-time string according to ISO 8601 and http://www.w3.org/TR/NOTE-datetime:
+// YYYY
+// YYYY-MM
+// YYYY-MM-DD
+// YYYY-MM-DDThh:mmTZD
+// YYYY-MM-DDThh:mm:ssTZD
+// YYYY-MM-DDThh:mm:ss.sTZD
+//
+// YYYY = four-digit year
+// MM = two-digit month (01=January, etc.)
+// DD = two-digit day of month (01 through 31)
+// hh = two digits of hour (00 through 23)
+// mm = two digits of minute (00 through 59)
+// ss = two digits of second (00 through 59)
+// s = one or more digits representing a decimal fraction of a second
+// TZD = time zone designator (Z or +hh:mm or -hh:mm)
+//
+// Note that ISO 8601 does not seem to allow years less than 1000 or greater than 9999. We allow
+// any year, even negative ones. The year is formatted as "%.4d". The TZD is also optional in XMP,
+// even though required in the W3C profile. Finally, Photoshop 8 (CS) sometimes created time-only
+// values so we tolerate that.
+
+// *** Put the ISO format comments in the header documentation.
+
+/* class static */ void
+XMPUtils::ConvertToDate ( XMP_StringPtr strValue,
+ XMP_DateTime * binValue )
+{
+ if ( (strValue == 0) || (*strValue == 0) ) XMP_Throw ( "Empty convert-from string", kXMPErr_BadValue);
+
+ size_t pos = 0;
+ XMP_Int32 temp;
+
+ XMP_Assert ( sizeof(*binValue) == sizeof(XMP_DateTime) );
+ (void) memset ( binValue, 0, sizeof(*binValue) ); // AUDIT: Safe, using sizeof destination.
+
+ size_t strSize = strlen ( strValue );
+ bool timeOnly = ( (strValue[0] == 'T') ||
+ ((strSize >= 2) && (strValue[1] == ':')) ||
+ ((strSize >= 3) && (strValue[2] == ':')) );
+
+ if ( ! timeOnly ) {
+
+ binValue->hasDate = true;
+
+ if ( strValue[0] == '-' ) pos = 1;
+
+ temp = GatherInt ( strValue, &pos, "Invalid year in date string" ); // Extract the year.
+ if ( (strValue[pos] != 0) && (strValue[pos] != '-') ) XMP_Throw ( "Invalid date string, after year", kXMPErr_BadParam );
+ if ( strValue[0] == '-' ) temp = -temp;
+ binValue->year = temp;
+ if ( strValue[pos] == 0 ) return;
+
+ ++pos;
+ temp = GatherInt ( strValue, &pos, "Invalid month in date string" ); // Extract the month.
+ if ( (strValue[pos] != 0) && (strValue[pos] != '-') ) XMP_Throw ( "Invalid date string, after month", kXMPErr_BadParam );
+ binValue->month = temp;
+ if ( strValue[pos] == 0 ) return;
+
+ ++pos;
+ temp = GatherInt ( strValue, &pos, "Invalid day in date string" ); // Extract the day.
+ if ( (strValue[pos] != 0) && (strValue[pos] != 'T') ) XMP_Throw ( "Invalid date string, after day", kXMPErr_BadParam );
+ binValue->day = temp;
+ if ( strValue[pos] == 0 ) return;
+
+ // Allow year, month, and day to all be zero; implies the date portion is missing.
+ if ( (binValue->year != 0) || (binValue->month != 0) || (binValue->day != 0) ) {
+ // Temporary fix for bug 1269463, silently fix out of range month or day.
+ // if ( (binValue->month < 1) || (binValue->month > 12) ) XMP_Throw ( "Month is out of range", kXMPErr_BadParam );
+ // if ( (binValue->day < 1) || (binValue->day > 31) ) XMP_Throw ( "Day is out of range", kXMPErr_BadParam );
+ if ( binValue->month < 1 ) binValue->month = 1;
+ if ( binValue->month > 12 ) binValue->month = 12;
+ if ( binValue->day < 1 ) binValue->day = 1;
+ if ( binValue->day > 31 ) binValue->day = 31;
+ }
+
+ }
+
+ // If we get here there is more of the string, otherwise we would have returned above.
+
+ if ( strValue[pos] == 'T' ) {
+ ++pos;
+ } else if ( ! timeOnly ) {
+ XMP_Throw ( "Invalid date string, missing 'T' after date", kXMPErr_BadParam );
+ }
+
+ binValue->hasTime = true;
+
+ temp = GatherInt ( strValue, &pos, "Invalid hour in date string" ); // Extract the hour.
+ if ( strValue[pos] != ':' ) XMP_Throw ( "Invalid date string, after hour", kXMPErr_BadParam );
+ if ( temp > 23 ) temp = 23; // *** 1269463: XMP_Throw ( "Hour is out of range", kXMPErr_BadParam );
+ binValue->hour = temp;
+ // Don't check for done, we have to work up to the time zone.
+
+ ++pos;
+ temp = GatherInt ( strValue, &pos, "Invalid minute in date string" ); // And the minute.
+ if ( (strValue[pos] != ':') && (strValue[pos] != 'Z') &&
+ (strValue[pos] != '+') && (strValue[pos] != '-') && (strValue[pos] != 0) ) XMP_Throw ( "Invalid date string, after minute", kXMPErr_BadParam );
+ if ( temp > 59 ) temp = 59; // *** 1269463: XMP_Throw ( "Minute is out of range", kXMPErr_BadParam );
+ binValue->minute = temp;
+ // Don't check for done, we have to work up to the time zone.
+
+ if ( strValue[pos] == ':' ) {
+
+ ++pos;
+ temp = GatherInt ( strValue, &pos, "Invalid whole seconds in date string" ); // Extract the whole seconds.
+ if ( (strValue[pos] != '.') && (strValue[pos] != 'Z') &&
+ (strValue[pos] != '+') && (strValue[pos] != '-') && (strValue[pos] != 0) ) {
+ XMP_Throw ( "Invalid date string, after whole seconds", kXMPErr_BadParam );
+ }
+ if ( temp > 59 ) temp = 59; // *** 1269463: XMP_Throw ( "Whole second is out of range", kXMPErr_BadParam );
+ binValue->second = temp;
+ // Don't check for done, we have to work up to the time zone.
+
+ if ( strValue[pos] == '.' ) {
+
+ ++pos;
+ size_t digits = pos; // Will be the number of digits later.
+
+ temp = GatherInt ( strValue, &pos, "Invalid fractional seconds in date string" ); // Extract the fractional seconds.
+ if ( (strValue[pos] != 'Z') && (strValue[pos] != '+') && (strValue[pos] != '-') && (strValue[pos] != 0) ) {
+ XMP_Throw ( "Invalid date string, after fractional second", kXMPErr_BadParam );
+ }
+
+ digits = pos - digits;
+ for ( ; digits > 9; --digits ) temp = temp / 10;
+ for ( ; digits < 9; ++digits ) temp = temp * 10;
+
+ if ( temp >= 1000*1000*1000 ) XMP_Throw ( "Fractional second is out of range", kXMPErr_BadParam );
+ binValue->nanoSecond = temp;
+ // Don't check for done, we have to work up to the time zone.
+
+ }
+
+ }
+
+ if ( strValue[pos] == 0 ) return;
+
+ binValue->hasTimeZone = true;
+
+ if ( strValue[pos] == 'Z' ) {
+
+ ++pos;
+
+ } else {
+
+ if ( strValue[pos] == '+' ) {
+ binValue->tzSign = kXMP_TimeEastOfUTC;
+ } else if ( strValue[pos] == '-' ) {
+ binValue->tzSign = kXMP_TimeWestOfUTC;
+ } else {
+ XMP_Throw ( "Time zone must begin with 'Z', '+', or '-'", kXMPErr_BadParam );
+ }
+
+ ++pos;
+ temp = GatherInt ( strValue, &pos, "Invalid time zone hour in date string" ); // Extract the time zone hour.
+ if ( strValue[pos] != ':' ) XMP_Throw ( "Invalid date string, after time zone hour", kXMPErr_BadParam );
+ if ( temp > 23 ) XMP_Throw ( "Time zone hour is out of range", kXMPErr_BadParam );
+ binValue->tzHour = temp;
+
+ ++pos;
+ temp = GatherInt ( strValue, &pos, "Invalid time zone minute in date string" ); // Extract the time zone minute.
+ if ( temp > 59 ) XMP_Throw ( "Time zone minute is out of range", kXMPErr_BadParam );
+ binValue->tzMinute = temp;
+
+ }
+
+ if ( strValue[pos] != 0 ) XMP_Throw ( "Invalid date string, extra chars at end", kXMPErr_BadParam );
+
+} // ConvertToDate
+
+// -------------------------------------------------------------------------------------------------
+// EncodeToBase64
+// --------------
+//
+// Encode a string of raw data bytes in base 64 according to RFC 2045. For the encoding definition
+// see section 6.8 in <http://www.ietf.org/rfc/rfc2045.txt>. Although it isn't needed for RDF, we
+// do insert a linefeed character as a newline for every 76 characters of encoded output.
+
+/* class static */ void
+XMPUtils::EncodeToBase64 ( XMP_StringPtr rawStr,
+ XMP_StringLen rawLen,
+ XMP_VarString * encodedStr )
+{
+ if ( (rawStr == 0) && (rawLen != 0) ) XMP_Throw ( "Null raw data buffer", kXMPErr_BadParam );
+ XMP_Assert ( encodedStr != 0 ); // Enforced by wrapper.
+
+ encodedStr->erase();
+ if ( rawLen == 0 ) return;
+
+ char encChunk[4];
+
+ unsigned long in, out;
+ unsigned char c1, c2, c3;
+ unsigned long merge;
+
+ const size_t outputSize = (rawLen / 3) * 4; // Approximate, might be small.
+
+ encodedStr->reserve ( outputSize );
+
+ // ----------------------------------------------------------------------------------------
+ // Each 6 bits of input produces 8 bits of output, so 3 input bytes become 4 output bytes.
+ // Process the whole chunks of 3 bytes first, then deal with any remainder. Be careful with
+ // the loop comparison, size-2 could be negative!
+
+ for ( in = 0, out = 0; (in+2) < rawLen; in += 3, out += 4 ) {
+
+ c1 = rawStr[in];
+ c2 = rawStr[in+1];
+ c3 = rawStr[in+2];
+
+ merge = (c1 << 16) + (c2 << 8) + c3;
+
+ encChunk[0] = sBase64Chars [ merge >> 18 ];
+ encChunk[1] = sBase64Chars [ (merge >> 12) & 0x3F ];
+ encChunk[2] = sBase64Chars [ (merge >> 6) & 0x3F ];
+ encChunk[3] = sBase64Chars [ merge & 0x3F ];
+
+ if ( out >= 76 ) {
+ encodedStr->append ( 1, kLF );
+ out = 0;
+ }
+ encodedStr->append ( encChunk, 4 );
+
+ }
+
+ // ------------------------------------------------------------------------------------------
+ // The output must always be a multiple of 4 bytes. If there is a 1 or 2 byte input remainder
+ // we need to create another chunk. Zero pad with bits to a 6 bit multiple, then add one or
+ // two '=' characters to pad out to 4 bytes.
+
+ switch ( rawLen - in ) {
+
+ case 0: // Done, no remainder.
+ break;
+
+ case 1: // One input byte remains.
+
+ c1 = rawStr[in];
+ merge = c1 << 16;
+
+ encChunk[0] = sBase64Chars [ merge >> 18 ];
+ encChunk[1] = sBase64Chars [ (merge >> 12) & 0x3F ];
+ encChunk[2] = encChunk[3] = '=';
+
+ if ( out >= 76 ) encodedStr->append ( 1, kLF );
+ encodedStr->append ( encChunk, 4 );
+ break;
+
+ case 2: // Two input bytes remain.
+
+ c1 = rawStr[in];
+ c2 = rawStr[in+1];
+ merge = (c1 << 16) + (c2 << 8);
+
+ encChunk[0] = sBase64Chars [ merge >> 18 ];
+ encChunk[1] = sBase64Chars [ (merge >> 12) & 0x3F ];
+ encChunk[2] = sBase64Chars [ (merge >> 6) & 0x3F ];
+ encChunk[3] = '=';
+
+ if ( out >= 76 ) encodedStr->append ( 1, kLF );
+ encodedStr->append ( encChunk, 4 );
+ break;
+
+ }
+
+} // EncodeToBase64
+
+// -------------------------------------------------------------------------------------------------
+// DecodeFromBase64
+// ----------------
+//
+// Decode a string of raw data bytes from base 64 according to RFC 2045. For the encoding definition
+// see section 6.8 in <http://www.ietf.org/rfc/rfc2045.txt>. RFC 2045 talks about ignoring all "bad"
+// input but warning about non-whitespace. For XMP use we ignore space, tab, LF, and CR. Any other
+// bad input is rejected.
+
+/* class static */ void
+XMPUtils::DecodeFromBase64 ( XMP_StringPtr encodedStr,
+ XMP_StringLen encodedLen,
+ XMP_VarString * rawStr )
+{
+ if ( (encodedStr == 0) && (encodedLen != 0) ) XMP_Throw ( "Null encoded data buffer", kXMPErr_BadParam );
+ XMP_Assert ( rawStr != 0 ); // Enforced by wrapper.
+
+ rawStr->erase();
+ if ( encodedLen == 0 ) return;
+
+ unsigned char ch, rawChunk[3];
+ unsigned long inStr, inChunk, inLimit, merge, padding;
+
+ XMP_StringLen outputSize = (encodedLen / 4) * 3; // Only a close approximation.
+
+ rawStr->reserve ( outputSize );
+
+
+ // ----------------------------------------------------------------------------------------
+ // Each 8 bits of input produces 6 bits of output, so 4 input bytes become 3 output bytes.
+ // Process all but the last 4 data bytes first, then deal with the final chunk. Whitespace
+ // in the input must be ignored. The first loop finds where the last 4 data bytes start and
+ // counts the number of padding equal signs.
+
+ padding = 0;
+ for ( inStr = 0, inLimit = encodedLen; (inStr < 4) && (inLimit > 0); ) {
+ inLimit -= 1; // ! Don't do in the loop control, the decr/test order is wrong.
+ ch = encodedStr[inLimit];
+ if ( ch == '=' ) {
+ padding += 1; // The equal sign padding is a data byte.
+ } else if ( DecodeBase64Char(ch) == 0xFF ) {
+ continue; // Ignore whitespace, don't increment inStr.
+ } else {
+ inStr += 1;
+ }
+ }
+
+ // ! Be careful to count whitespace that is immediately before the final data. Otherwise
+ // ! middle portion will absorb the final data and mess up the final chunk processing.
+
+ while ( (inLimit > 0) && (DecodeBase64Char(encodedStr[inLimit-1]) == 0xFF) ) --inLimit;
+
+ if ( inStr == 0 ) return; // Nothing but whitespace.
+ if ( padding > 2 ) XMP_Throw ( "Invalid encoded string", kXMPErr_BadParam );
+
+ // -------------------------------------------------------------------------------------------
+ // Now process all but the last chunk. The limit ensures that we have at least 4 data bytes
+ // left when entering the output loop, so the inner loop will succeed without overrunning the
+ // end of the data. At the end of the outer loop we might be past inLimit though.
+
+ inStr = 0;
+ while ( inStr < inLimit ) {
+
+ merge = 0;
+ for ( inChunk = 0; inChunk < 4; ++inStr ) { // ! Yes, increment inStr on each pass.
+ ch = DecodeBase64Char ( encodedStr [inStr] );
+ if ( ch == 0xFF ) continue; // Ignore whitespace.
+ merge = (merge << 6) + ch;
+ inChunk += 1;
+ }
+
+ rawChunk[0] = (unsigned char) (merge >> 16);
+ rawChunk[1] = (unsigned char) ((merge >> 8) & 0xFF);
+ rawChunk[2] = (unsigned char) (merge & 0xFF);
+
+ rawStr->append ( (char*)rawChunk, 3 );
+
+ }
+
+ // -------------------------------------------------------------------------------------------
+ // Process the final, possibly partial, chunk of data. The input is always a multiple 4 bytes,
+ // but the raw data can be any length. The number of padding '=' characters determines if the
+ // final chunk has 1, 2, or 3 raw data bytes.
+
+ XMP_Assert ( inStr < encodedLen );
+
+ merge = 0;
+ for ( inChunk = 0; inChunk < 4-padding; ++inStr ) { // ! Yes, increment inStr on each pass.
+ ch = DecodeBase64Char ( encodedStr[inStr] );
+ if ( ch == 0xFF ) continue; // Ignore whitespace.
+ merge = (merge << 6) + ch;
+ inChunk += 1;
+ }
+
+ if ( padding == 2 ) {
+
+ rawChunk[0] = (unsigned char) (merge >> 4);
+ rawStr->append ( (char*)rawChunk, 1 );
+
+ } else if ( padding == 1 ) {
+
+ rawChunk[0] = (unsigned char) (merge >> 10);
+ rawChunk[1] = (unsigned char) ((merge >> 2) & 0xFF);
+ rawStr->append ( (char*)rawChunk, 2 );
+
+ } else {
+
+ rawChunk[0] = (unsigned char) (merge >> 16);
+ rawChunk[1] = (unsigned char) ((merge >> 8) & 0xFF);
+ rawChunk[2] = (unsigned char) (merge & 0xFF);
+ rawStr->append ( (char*)rawChunk, 3 );
+
+ }
+
+} // DecodeFromBase64
+
+// -------------------------------------------------------------------------------------------------
+// PackageForJPEG
+// --------------
+
+/* class static */ void
+XMPUtils::PackageForJPEG ( const XMPMeta & origXMP,
+ XMP_VarString * stdStr,
+ XMP_VarString * extStr,
+ XMP_VarString * digestStr )
+{
+ XMP_Assert ( (stdStr != 0) && (extStr != 0) && (digestStr != 0) ); // ! Enforced by wrapper.
+
+ enum { kStdXMPLimit = 65000 };
+ static const char * kPacketTrailer = "<?xpacket end=\"w\"?>";
+ static size_t kTrailerLen = strlen ( kPacketTrailer );
+
+ XMP_VarString tempStr;
+ XMPMeta stdXMP, extXMP;
+ XMP_OptionBits keepItSmall = kXMP_UseCompactFormat | kXMP_OmitAllFormatting;
+
+ stdStr->erase();
+ extStr->erase();
+ digestStr->erase();
+
+ // Try to serialize everything. Note that we're making internal calls to SerializeToBuffer, so
+ // we'll be getting back the pointer and length for its internal string.
+
+ origXMP.SerializeToBuffer ( &tempStr, keepItSmall, 1, "", "", 0 );
+ #if Trace_PackageForJPEG
+ printf ( "\nXMPUtils::PackageForJPEG - Full serialize %d bytes\n", tempStr.size() );
+ #endif
+
+ if ( tempStr.size() > kStdXMPLimit ) {
+
+ // Couldn't fit everything, make a copy of the input XMP and make sure there is no xmp:Thumbnails property.
+
+ stdXMP.tree.options = origXMP.tree.options;
+ stdXMP.tree.name = origXMP.tree.name;
+ stdXMP.tree.value = origXMP.tree.value;
+ CloneOffspring ( &origXMP.tree, &stdXMP.tree );
+
+ if ( stdXMP.DoesPropertyExist ( kXMP_NS_XMP, "Thumbnails" ) ) {
+ stdXMP.DeleteProperty ( kXMP_NS_XMP, "Thumbnails" );
+ stdXMP.SerializeToBuffer ( &tempStr, keepItSmall, 1, "", "", 0 );
+ #if Trace_PackageForJPEG
+ printf ( " Delete xmp:Thumbnails, %d bytes left\n", tempStr.size() );
+ #endif
+ }
+
+ }
+
+ if ( tempStr.size() > kStdXMPLimit ) {
+
+ // Still doesn't fit, move all of the Camera Raw namespace. Add a dummy value for xmpNote:HasExtendedXMP.
+
+ stdXMP.SetProperty ( kXMP_NS_XMP_Note, "HasExtendedXMP", "123456789-123456789-123456789-12", 0 );
+
+ XMP_NodePtrPos crSchemaPos;
+ XMP_Node * crSchema = FindSchemaNode ( &stdXMP.tree, kXMP_NS_CameraRaw, kXMP_ExistingOnly, &crSchemaPos );
+
+ if ( crSchema != 0 ) {
+ crSchema->parent = &extXMP.tree;
+ extXMP.tree.children.push_back ( crSchema );
+ stdXMP.tree.children.erase ( crSchemaPos );
+ stdXMP.SerializeToBuffer ( &tempStr, keepItSmall, 1, "", "", 0 );
+ #if Trace_PackageForJPEG
+ printf ( " Move Camera Raw schema, %d bytes left\n", tempStr.size() );
+ #endif
+ }
+
+ }
+
+ if ( tempStr.size() > kStdXMPLimit ) {
+
+ // Still doesn't fit, move photoshop:History.
+
+ bool moved = MoveOneProperty ( stdXMP, &extXMP, kXMP_NS_Photoshop, "photoshop:History" );
+
+ if ( moved ) {
+ stdXMP.SerializeToBuffer ( &tempStr, keepItSmall, 1, "", "", 0 );
+ #if Trace_PackageForJPEG
+ printf ( " Move photoshop:History, %d bytes left\n", tempStr.size() );
+ #endif
+ }
+
+ }
+
+ if ( tempStr.size() > kStdXMPLimit ) {
+
+ // Still doesn't fit, move top level properties in order of estimated size. This is done by
+ // creating a multi-map that maps the serialized size to the string pair for the schema URI
+ // and top level property name. Since maps are inherently ordered, a reverse iteration of
+ // the map can be done to move the largest things first. We use a double loop to keep going
+ // until the serialization actually fits, in case the estimates are off.
+
+ PropSizeMap propSizes;
+ CreateEstimatedSizeMap ( stdXMP, &propSizes );
+
+ #if Trace_PackageForJPEG
+ if ( ! propSizes.empty() ) {
+ printf ( " Top level property map, smallest to largest:\n" );
+ PropSizeMap::iterator mapPos = propSizes.begin();
+ PropSizeMap::iterator mapEnd = propSizes.end();
+ for ( ; mapPos != mapEnd; ++mapPos ) {
+ size_t propSize = mapPos->first;
+ const char * schemaName = mapPos->second.first->c_str();
+ const char * propName = mapPos->second.second->c_str();
+ printf ( " %d bytes, %s in %s\n", propSize, propName, schemaName );
+ }
+ }
+ #endif
+
+ #if 0 // Trace_PackageForJPEG *** Xcode 2.3 on 10.4.7 has bugs in backwards iteration
+ if ( ! propSizes.empty() ) {
+ printf ( " Top level property map, largest to smallest:\n" );
+ PropSizeMap::iterator mapPos = propSizes.end();
+ PropSizeMap::iterator mapBegin = propSizes.begin();
+ for ( --mapPos; true; --mapPos ) {
+ size_t propSize = mapPos->first;
+ const char * schemaName = mapPos->second.first->c_str();
+ const char * propName = mapPos->second.second->c_str();
+ printf ( " %d bytes, %s in %s\n", propSize, propName, schemaName );
+ if ( mapPos == mapBegin ) break;
+ }
+ }
+ #endif
+
+ // Outer loop to make sure enough is actually moved.
+
+ while ( (tempStr.size() > kStdXMPLimit) && (! propSizes.empty()) ) {
+
+ // Inner loop, move what seems to be enough according to the estimates.
+
+ size_t tempLen = tempStr.size();
+ while ( (tempLen > kStdXMPLimit) && (! propSizes.empty()) ) {
+
+ size_t propSize = MoveLargestProperty ( stdXMP, &extXMP, propSizes );
+ XMP_Assert ( propSize > 0 );
+
+ if ( propSize > tempLen ) propSize = tempLen; // ! Don't go negative.
+ tempLen -= propSize;
+
+ }
+
+ // Reserialize the remaining standard XMP.
+
+ stdXMP.SerializeToBuffer ( &tempStr, keepItSmall, 1, "", "", 0 );
+
+ }
+
+ }
+
+ if ( tempStr.size() > kStdXMPLimit ) {
+ // Still doesn't fit, throw an exception and let the client decide what to do.
+ // ! This should never happen with the policy of moving any and all top level properties.
+ XMP_Throw ( "Can't reduce XMP enough for JPEG file", kXMPErr_TooLargeForJPEG );
+ }
+
+ // Set the static output strings.
+
+ if ( extXMP.tree.children.empty() ) {
+
+ // Just have the standard XMP.
+ *stdStr = tempStr;
+
+ } else {
+
+ // Have extended XMP. Serialize it, compute the digest, reset xmpNote:HasExtendedXMP, and
+ // reserialize the standard XMP.
+
+ extXMP.SerializeToBuffer ( &tempStr, (keepItSmall | kXMP_OmitPacketWrapper), 0, "", "", 0 );
+ *extStr = tempStr;
+
+ XMP_Uns8 digest [16];
+
+ {
+ context_md5_t ctx;
+
+ MD5Init(&ctx);
+ MD5Update(&ctx, (unsigned char*)tempStr.c_str(), (unsigned int)tempStr.size() );
+ MD5Final(digest, &ctx);
+ }
+
+ digestStr->reserve ( 32 );
+ for ( size_t i = 0; i < 16; ++i ) {
+ XMP_Uns8 byte = digest[i];
+ digestStr->push_back ( kHexDigits [ byte>>4 ] );
+ digestStr->push_back ( kHexDigits [ byte&0xF ] );
+ }
+
+ stdXMP.SetProperty ( kXMP_NS_XMP_Note, "HasExtendedXMP", digestStr->c_str(), 0 );
+ stdXMP.SerializeToBuffer ( &tempStr, keepItSmall, 1, "", "", 0 );
+ *stdStr = tempStr;
+
+ }
+
+ // Adjust the standard XMP padding to be up to 2KB.
+
+ XMP_Assert ( (stdStr->size() > kTrailerLen) && (stdStr->size() <= kStdXMPLimit) );
+ const char * packetEnd = stdStr->c_str() + stdStr->size() - kTrailerLen;
+ XMP_Assert ( XMP_LitMatch ( packetEnd, kPacketTrailer ) );
+
+ size_t extraPadding = kStdXMPLimit - stdStr->size(); // ! Do this before erasing the trailer.
+ if ( extraPadding > 2047 ) extraPadding = 2047;
+ stdStr->erase ( stdStr->size() - kTrailerLen );
+ stdStr->append ( extraPadding, ' ' );
+ stdStr->append ( kPacketTrailer );
+
+} // PackageForJPEG
+
+// -------------------------------------------------------------------------------------------------
+// MergeFromJPEG
+// -------------
+//
+// Copy all of the top level properties from extendedXMP to fullXMP, replacing any duplicates.
+// Delete the xmpNote:HasExtendedXMP property from fullXMP.
+
+/* class static */ void
+XMPUtils::MergeFromJPEG ( XMPMeta * fullXMP,
+ const XMPMeta & extendedXMP )
+{
+
+ XMP_OptionBits apFlags = (kXMPTemplate_ReplaceExistingProperties | kXMPTemplate_IncludeInternalProperties);
+ XMPUtils::ApplyTemplate ( fullXMP, extendedXMP, apFlags );
+ fullXMP->DeleteProperty ( kXMP_NS_XMP_Note, "HasExtendedXMP" );
+
+} // MergeFromJPEG
+
+// -------------------------------------------------------------------------------------------------
+// CurrentDateTime
+// ---------------
+
+/* class static */ void
+XMPUtils::CurrentDateTime ( XMP_DateTime * xmpTime )
+{
+ XMP_Assert ( xmpTime != 0 ); // ! Enforced by wrapper.
+
+ ansi_tt binTime = ansi_time(0);
+ if ( binTime == -1 ) XMP_Throw ( "Failure from ANSI C time function", kXMPErr_ExternalFailure );
+ ansi_tm currTime;
+ ansi_localtime ( &binTime, &currTime );
+
+ xmpTime->year = currTime.tm_year + 1900;
+ xmpTime->month = currTime.tm_mon + 1;
+ xmpTime->day = currTime.tm_mday;
+ xmpTime->hasDate = true;
+
+ xmpTime->hour = currTime.tm_hour;
+ xmpTime->minute = currTime.tm_min;
+ xmpTime->second = currTime.tm_sec;
+ xmpTime->nanoSecond = 0;
+ xmpTime->hasTime = true;
+
+ xmpTime->tzSign = 0;
+ xmpTime->tzHour = 0;
+ xmpTime->tzMinute = 0;
+ xmpTime->hasTimeZone = false; // ! Needed for SetTimeZone.
+ XMPUtils::SetTimeZone ( xmpTime );
+
+} // CurrentDateTime
+
+// -------------------------------------------------------------------------------------------------
+// SetTimeZone
+// -----------
+//
+// Sets just the time zone part of the time. Useful for determining the local time zone or for
+// converting a "zone-less" time to a proper local time. The ANSI C time functions are smart enough
+// to do all the right stuff, as long as we call them properly!
+
+/* class static */ void
+XMPUtils::SetTimeZone ( XMP_DateTime * xmpTime )
+{
+ XMP_Assert ( xmpTime != 0 ); // ! Enforced by wrapper.
+
+ VerifyDateTimeFlags ( xmpTime );
+
+ if ( xmpTime->hasTimeZone ) {
+ XMP_Throw ( "SetTimeZone can only be used on zone-less times", kXMPErr_BadParam );
+ }
+
+ // Create ansi_tt form of the input time. Need the ansi_tm form to make the ansi_tt form.
+
+ ansi_tt ttTime;
+ ansi_tm tmLocal, tmUTC;
+
+ if ( (xmpTime->year == 0) && (xmpTime->month == 0) && (xmpTime->day == 0) ) {
+ ansi_tt now = ansi_time(0);
+ if ( now == -1 ) XMP_Throw ( "Failure from ANSI C time function", kXMPErr_ExternalFailure );
+ ansi_localtime ( &now, &tmLocal );
+ } else {
+ tmLocal.tm_year = xmpTime->year - 1900;
+ while ( tmLocal.tm_year < 70 ) tmLocal.tm_year += 4; // ! Some versions of mktime barf on years before 1970.
+ tmLocal.tm_mon = xmpTime->month - 1;
+ tmLocal.tm_mday = xmpTime->day;
+ }
+
+ tmLocal.tm_hour = xmpTime->hour;
+ tmLocal.tm_min = xmpTime->minute;
+ tmLocal.tm_sec = xmpTime->second;
+ tmLocal.tm_isdst = -1; // Don't know if daylight time is in effect.
+
+ ttTime = ansi_mktime ( &tmLocal );
+ if ( ttTime == -1 ) XMP_Throw ( "Failure from ANSI C mktime function", kXMPErr_ExternalFailure );
+
+ // Convert back to a localized ansi_tm time and get the corresponding UTC ansi_tm time.
+
+ ansi_localtime ( &ttTime, &tmLocal );
+ ansi_gmtime ( &ttTime, &tmUTC );
+
+ // Get the offset direction and amount.
+
+ ansi_tm tmx = tmLocal; // ! Note that mktime updates the ansi_tm parameter, messing up difftime!
+ ansi_tm tmy = tmUTC;
+ tmx.tm_isdst = tmy.tm_isdst = 0;
+ ansi_tt ttx = ansi_mktime ( &tmx );
+ ansi_tt tty = ansi_mktime ( &tmy );
+ double diffSecs;
+
+ if ( (ttx != -1) && (tty != -1) ) {
+ diffSecs = ansi_difftime ( ttx, tty );
+ } else {
+ #if XMP_MacBuild | XMP_iOSBuild
+ // Looks like Apple's mktime is buggy - see W1140533. But the offset is visible.
+ diffSecs = tmLocal.tm_gmtoff;
+ #else
+ // Win and UNIX don't have a visible offset. Make sure we know about the failure,
+ // then try using the current date/time as a close fallback.
+ ttTime = ansi_time(0);
+ if ( ttTime == -1 ) XMP_Throw ( "Failure from ANSI C time function", kXMPErr_ExternalFailure );
+ ansi_localtime ( &ttTime, &tmx );
+ ansi_gmtime ( &ttTime, &tmy );
+ tmx.tm_isdst = tmy.tm_isdst = 0;
+ ttx = ansi_mktime ( &tmx );
+ tty = ansi_mktime ( &tmy );
+ if ( (ttx == -1) || (tty == -1) ) XMP_Throw ( "Failure from ANSI C mktime function", kXMPErr_ExternalFailure );
+ diffSecs = ansi_difftime ( ttx, tty );
+ #endif
+ }
+
+ if ( diffSecs > 0.0 ) {
+ xmpTime->tzSign = kXMP_TimeEastOfUTC;
+ } else if ( diffSecs == 0.0 ) {
+ xmpTime->tzSign = kXMP_TimeIsUTC;
+ } else {
+ xmpTime->tzSign = kXMP_TimeWestOfUTC;
+ diffSecs = -diffSecs;
+ }
+ xmpTime->tzHour = XMP_Int32 ( diffSecs / 3600.0 );
+ xmpTime->tzMinute = XMP_Int32 ( (diffSecs / 60.0) - (xmpTime->tzHour * 60.0) );
+
+ xmpTime->hasTimeZone = xmpTime->hasTime = true;
+
+ // *** Save the tm_isdst flag in a qualifier?
+
+ XMP_Assert ( (0 <= xmpTime->tzHour) && (xmpTime->tzHour <= 23) );
+ XMP_Assert ( (0 <= xmpTime->tzMinute) && (xmpTime->tzMinute <= 59) );
+ XMP_Assert ( (-1 <= xmpTime->tzSign) && (xmpTime->tzSign <= +1) );
+ XMP_Assert ( (xmpTime->tzSign == 0) ? ((xmpTime->tzHour == 0) && (xmpTime->tzMinute == 0)) :
+ ((xmpTime->tzHour != 0) || (xmpTime->tzMinute != 0)) );
+
+} // SetTimeZone
+
+// -------------------------------------------------------------------------------------------------
+// ConvertToUTCTime
+// ----------------
+
+/* class static */ void
+XMPUtils::ConvertToUTCTime ( XMP_DateTime * time )
+{
+ XMP_Assert ( time != 0 ); // ! Enforced by wrapper.
+
+ VerifyDateTimeFlags ( time );
+
+ if ( ! time->hasTimeZone ) return; // Do nothing if there is no current time zone.
+
+ XMP_Assert ( (0 <= time->tzHour) && (time->tzHour <= 23) );
+ XMP_Assert ( (0 <= time->tzMinute) && (time->tzMinute <= 59) );
+ XMP_Assert ( (-1 <= time->tzSign) && (time->tzSign <= +1) );
+ XMP_Assert ( (time->tzSign == 0) ? ((time->tzHour == 0) && (time->tzMinute == 0)) :
+ ((time->tzHour != 0) || (time->tzMinute != 0)) );
+
+ if ( time->tzSign == kXMP_TimeEastOfUTC ) {
+ // We are before (east of) GMT, subtract the offset from the time.
+ time->hour -= time->tzHour;
+ time->minute -= time->tzMinute;
+ } else if ( time->tzSign == kXMP_TimeWestOfUTC ) {
+ // We are behind (west of) GMT, add the offset to the time.
+ time->hour += time->tzHour;
+ time->minute += time->tzMinute;
+ }
+
+ AdjustTimeOverflow ( time );
+ time->tzSign = time->tzHour = time->tzMinute = 0;
+
+} // ConvertToUTCTime
+
+// -------------------------------------------------------------------------------------------------
+// ConvertToLocalTime
+// ------------------
+
+/* class static */ void
+XMPUtils::ConvertToLocalTime ( XMP_DateTime * time )
+{
+ XMP_Assert ( time != 0 ); // ! Enforced by wrapper.
+
+ VerifyDateTimeFlags ( time );
+
+ if ( ! time->hasTimeZone ) return; // Do nothing if there is no current time zone.
+
+ XMP_Assert ( (0 <= time->tzHour) && (time->tzHour <= 23) );
+ XMP_Assert ( (0 <= time->tzMinute) && (time->tzMinute <= 59) );
+ XMP_Assert ( (-1 <= time->tzSign) && (time->tzSign <= +1) );
+ XMP_Assert ( (time->tzSign == 0) ? ((time->tzHour == 0) && (time->tzMinute == 0)) :
+ ((time->tzHour != 0) || (time->tzMinute != 0)) );
+
+ ConvertToUTCTime ( time ); // The existing time zone might not be the local one.
+ time->hasTimeZone = false; // ! Needed for SetTimeZone.
+ SetTimeZone ( time ); // Fill in the local timezone offset, then adjust the time.
+
+ if ( time->tzSign > 0 ) {
+ // We are before (east of) GMT, add the offset to the time.
+ time->hour += time->tzHour;
+ time->minute += time->tzMinute;
+ } else if ( time->tzSign < 0 ) {
+ // We are behind (west of) GMT, subtract the offset from the time.
+ time->hour -= time->tzHour;
+ time->minute -= time->tzMinute;
+ }
+
+ AdjustTimeOverflow ( time );
+
+} // ConvertToLocalTime
+
+// -------------------------------------------------------------------------------------------------
+// CompareDateTime
+// ---------------
+
+/* class static */ int
+XMPUtils::CompareDateTime ( const XMP_DateTime & _in_left,
+ const XMP_DateTime & _in_right )
+{
+ int result = 0;
+
+ XMP_DateTime left = _in_left;
+ XMP_DateTime right = _in_right;
+
+ VerifyDateTimeFlags ( &left );
+ VerifyDateTimeFlags ( &right );
+
+ // Can't compare if one has a date and the other does not.
+ if ( left.hasDate != right.hasDate ) return 0; // Throw?
+
+ if ( left.hasTimeZone & right.hasTimeZone ) {
+ // If both times have zones then convert them to UTC, otherwise assume the same zone.
+ ConvertToUTCTime ( &left );
+ ConvertToUTCTime ( &right );
+ }
+
+ if ( left.hasDate ) {
+
+ XMP_Assert ( right.hasDate );
+
+ if ( left.year < right.year ) {
+ result = -1;
+ } else if ( left.year > right.year ) {
+ result = +1;
+ } else if ( left.month < right.month ) {
+ result = -1;
+ } else if ( left.month > right.month ) {
+ result = +1;
+ } else if ( left.day < right.day ) {
+ result = -1;
+ } else if ( left.day > right.day ) {
+ result = +1;
+ }
+
+ if ( result != 0 ) return result;
+
+ }
+
+ if ( left.hasTime & right.hasTime ) {
+
+ // Ignore the time parts if either value is date-only.
+
+ if ( left.hour < right.hour ) {
+ result = -1;
+ } else if ( left.hour > right.hour ) {
+ result = +1;
+ } else if ( left.minute < right.minute ) {
+ result = -1;
+ } else if ( left.minute > right.minute ) {
+ result = +1;
+ } else if ( left.second < right.second ) {
+ result = -1;
+ } else if ( left.second > right.second ) {
+ result = +1;
+ } else if ( left.nanoSecond < right.nanoSecond ) {
+ result = -1;
+ } else if ( left.nanoSecond > right.nanoSecond ) {
+ result = +1;
+ } else {
+ result = 0;
+ }
+
+ }
+
+ return result;
+
+} // CompareDateTime
+
+// =================================================================================================
+
+std::string& XMPUtils::Trim( std::string& string )
+{
+ size_t pos = string.find_last_not_of( *WhiteSpaceStrPtr );
+
+ if ( pos != std::string::npos ) {
+ string.erase( pos + 1 );
+ pos = string.find_first_not_of( *WhiteSpaceStrPtr );
+ if(pos != std::string::npos) string.erase(0, pos);
+ } else {
+ string.erase( string.begin(), string.end() );
+ }
+ return string;
+}
+
+std::string * XMPUtils::WhiteSpaceStrPtr = NULL;
+
+// =================================================================================================