gpr-tools/source/lib/xmp_core/XMPUtils-FileInfo.cpp

1494 wiersze
51 KiB
C++

// =================================================================================================
// 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 <algorithm> // For binary_search.
#include "public/include/XMP_Environment.h" // ! This must be the first include!
#include "XMPCore_Impl.hpp"
#include "XMPUtils.hpp"
#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)
#endif
// =================================================================================================
// Local Types and Constants
// =========================
typedef unsigned long UniCodePoint;
enum UniCharKind {
UCK_normal,
UCK_space,
UCK_comma,
UCK_semicolon,
UCK_quote,
UCK_control
};
typedef enum UniCharKind UniCharKind;
#define UnsByte(c) ((unsigned char)(c))
#define UCP(u) ((UniCodePoint)(u))
// ! Needed on Windows (& PC Linux?) for inequalities with literals ito avoid sign extension.
#ifndef TraceMultiFile
#define TraceMultiFile 0
#endif
// =================================================================================================
// Static Variables
// ================
// =================================================================================================
// Local Utilities
// ===============
// -------------------------------------------------------------------------------------------------
// ClassifyCharacter
// -----------------
static void
ClassifyCharacter ( XMP_StringPtr fullString, size_t offset,
UniCharKind * charKind, size_t * charSize, UniCodePoint * uniChar )
{
*charKind = UCK_normal; // Assume typical case.
unsigned char currByte = UnsByte ( fullString[offset] );
if ( currByte < UnsByte(0x80) ) {
// ----------------------------------------
// We've got a single byte ASCII character.
*charSize = 1;
*uniChar = currByte;
if ( currByte > UnsByte(0x22) ) {
if ( currByte == UnsByte(0x2C) ) {
*charKind = UCK_comma;
} else if ( currByte == UnsByte(0x3B) ) {
*charKind = UCK_semicolon;
}
// [2674672] Discontinue to interpret square brackets
// as Asian quotes in XMPUtils::SeparateArrayItems(..))
// *** else if ( (currByte == UnsByte(0x5B)) || (currByte == UnsByte(0x5D)) ) {
// *** *charKind = UCK_quote; // ! ASCII '[' and ']' are used as quotes in Chinese and Korean.
// *** }
} else { // currByte <= 0x22
if ( currByte == UnsByte(0x22) ) {
*charKind = UCK_quote;
} else if ( currByte == UnsByte(0x21) ) {
*charKind = UCK_normal;
} else if ( currByte == UnsByte(0x20) ) {
*charKind = UCK_space;
} else {
*charKind = UCK_control;
}
}
} else { // currByte >= 0x80
// ---------------------------------------------------------------------------------------
// We've got a multibyte Unicode character. The first byte has the number of bytes and the
// highest order bits. The other bytes each add 6 more bits. Compose the UTF-32 form so we
// can classify directly with the Unicode code points. Order the upperBits tests to be
// fastest for Japan, probably the most common non-ASCII usage.
*charSize = 0;
*uniChar = currByte;
while ( (*uniChar & 0x80) != 0 ) { // Count the leading 1 bits in the byte.
++(*charSize);
*uniChar = *uniChar << 1;
}
XMP_Assert ( (offset + *charSize) <= strlen(fullString) );
*uniChar = *uniChar & 0x7F; // Put the character bits in the bottom of uniChar.
*uniChar = *uniChar >> *charSize;
for ( size_t i = (offset + 1); i < (offset + *charSize); ++i ) {
*uniChar = (*uniChar << 6) | (UnsByte(fullString[i]) & 0x3F);
}
XMP_Uns32 upperBits = *uniChar >> 8; // First filter on just the high order 24 bits.
if ( upperBits == 0xFF ) { // U+FFxx
if ( *uniChar == UCP(0xFF0C) ) {
*charKind = UCK_comma; // U+FF0C, full width comma.
} else if ( *uniChar == UCP(0xFF1B) ) {
*charKind = UCK_semicolon; // U+FF1B, full width semicolon.
} else if ( *uniChar == UCP(0xFF64) ) {
*charKind = UCK_comma; // U+FF64, half width ideographic comma.
}
} else if ( upperBits == 0xFE ) { // U+FE--
if ( *uniChar == UCP(0xFE50) ) {
*charKind = UCK_comma; // U+FE50, small comma.
} else if ( *uniChar == UCP(0xFE51) ) {
*charKind = UCK_comma; // U+FE51, small ideographic comma.
} else if ( *uniChar == UCP(0xFE54) ) {
*charKind = UCK_semicolon; // U+FE54, small semicolon.
}
} else if ( upperBits == 0x30 ) { // U+30--
if ( *uniChar == UCP(0x3000) ) {
*charKind = UCK_space; // U+3000, ideographic space.
} else if ( *uniChar == UCP(0x3001) ) {
*charKind = UCK_comma; // U+3001, ideographic comma.
} else if ( (UCP(0x3008) <= *uniChar) && (*uniChar <= UCP(0x300F)) ) {
*charKind = UCK_quote; // U+3008..U+300F, various quotes.
} else if ( *uniChar == UCP(0x303F) ) {
*charKind = UCK_space; // U+303F, ideographic half fill space.
} else if ( (UCP(0x301D) <= *uniChar) && (*uniChar <= UCP(0x301F)) ) {
*charKind = UCK_quote; // U+301D..U+301F, double prime quotes.
}
} else if ( upperBits == 0x20 ) { // U+20--
if ( (UCP(0x2000) <= *uniChar) && (*uniChar <= UCP(0x200B)) ) {
*charKind = UCK_space; // U+2000..U+200B, en quad through zero width space.
} else if ( *uniChar == UCP(0x2015) ) {
*charKind = UCK_quote; // U+2015, dash quote.
} else if ( (UCP(0x2018) <= *uniChar) && (*uniChar <= UCP(0x201F)) ) {
*charKind = UCK_quote; // U+2018..U+201F, various quotes.
} else if ( *uniChar == UCP(0x2028) ) {
*charKind = UCK_control; // U+2028, line separator.
} else if ( *uniChar == UCP(0x2029) ) {
*charKind = UCK_control; // U+2029, paragraph separator.
} else if ( (*uniChar == UCP(0x2039)) || (*uniChar == UCP(0x203A)) ) {
*charKind = UCK_quote; // U+2039 and U+203A, guillemet quotes.
}
} else if ( upperBits == 0x06 ) { // U+06--
if ( *uniChar == UCP(0x060C) ) {
*charKind = UCK_comma; // U+060C, Arabic comma.
} else if ( *uniChar == UCP(0x061B) ) {
*charKind = UCK_semicolon; // U+061B, Arabic semicolon.
}
} else if ( upperBits == 0x05 ) { // U+05--
if ( *uniChar == UCP(0x055D) ) {
*charKind = UCK_comma; // U+055D, Armenian comma.
}
} else if ( upperBits == 0x03 ) { // U+03--
if ( *uniChar == UCP(0x037E) ) {
*charKind = UCK_semicolon; // U+037E, Greek "semicolon" (really a question mark).
}
} else if ( upperBits == 0x00 ) { // U+00--
if ( (*uniChar == UCP(0x00AB)) || (*uniChar == UCP(0x00BB)) ) {
*charKind = UCK_quote; // U+00AB and U+00BB, guillemet quotes.
}
}
}
} // ClassifyCharacter
// -------------------------------------------------------------------------------------------------
// IsClosingingQuote
// -----------------
static inline bool
IsClosingingQuote ( UniCodePoint uniChar, UniCodePoint openQuote, UniCodePoint closeQuote )
{
if ( (uniChar == closeQuote) ||
( (openQuote == UCP(0x301D)) && ((uniChar == UCP(0x301E)) || (uniChar == UCP(0x301F))) ) ) {
return true;
} else {
return false;
}
} // IsClosingingQuote
// -------------------------------------------------------------------------------------------------
// IsSurroundingQuote
// ------------------
static inline bool
IsSurroundingQuote ( UniCodePoint uniChar, UniCodePoint openQuote, UniCodePoint closeQuote )
{
if ( (uniChar == openQuote) || IsClosingingQuote ( uniChar, openQuote, closeQuote ) ) {
return true;
} else {
return false;
}
} // IsSurroundingQuote
// -------------------------------------------------------------------------------------------------
// GetClosingQuote
// ---------------
static UniCodePoint
GetClosingQuote ( UniCodePoint openQuote )
{
UniCodePoint closeQuote;
switch ( openQuote ) {
case UCP(0x0022) : closeQuote = UCP(0x0022); // ! U+0022 is both opening and closing.
break;
// *** [2674672] Discontinue to interpret square brackets
// *** as Asian quotes in XMPUtils::SeparateArrayItems(..))
// *** case UCP(0x005B) : closeQuote = UCP(0x005D);
// *** break;
case UCP(0x00AB) : closeQuote = UCP(0x00BB); // ! U+00AB and U+00BB are reversible.
break;
case UCP(0x00BB) : closeQuote = UCP(0x00AB);
break;
case UCP(0x2015) : closeQuote = UCP(0x2015); // ! U+2015 is both opening and closing.
break;
case UCP(0x2018) : closeQuote = UCP(0x2019);
break;
case UCP(0x201A) : closeQuote = UCP(0x201B);
break;
case UCP(0x201C) : closeQuote = UCP(0x201D);
break;
case UCP(0x201E) : closeQuote = UCP(0x201F);
break;
case UCP(0x2039) : closeQuote = UCP(0x203A); // ! U+2039 and U+203A are reversible.
break;
case UCP(0x203A) : closeQuote = UCP(0x2039);
break;
case UCP(0x3008) : closeQuote = UCP(0x3009);
break;
case UCP(0x300A) : closeQuote = UCP(0x300B);
break;
case UCP(0x300C) : closeQuote = UCP(0x300D);
break;
case UCP(0x300E) : closeQuote = UCP(0x300F);
break;
case UCP(0x301D) : closeQuote = UCP(0x301F); // ! U+301E also closes U+301D.
break;
default : closeQuote = 0;
break;
}
return closeQuote;
} // GetClosingQuote
// -------------------------------------------------------------------------------------------------
// CodePointToUTF8
// ---------------
static void
CodePointToUTF8 ( UniCodePoint uniChar, XMP_VarString & utf8Str )
{
size_t i, byteCount;
XMP_Uns8 buffer [8];
UniCodePoint cpTemp;
if ( uniChar <= 0x7F ) {
i = 7;
byteCount = 1;
buffer[7] = char(uniChar);
} else {
// ---------------------------------------------------------------------------------------
// Copy the data bits from the low order end to the high order end, include the 0x80 mask.
i = 8;
cpTemp = uniChar;
while ( cpTemp != 0 ) {
-- i; // Exit with i pointing to the last byte stored.
buffer[i] = UnsByte(0x80) | (UnsByte(cpTemp) & 0x3F);
cpTemp = cpTemp >> 6;
}
byteCount = 8 - i; // The total number of bytes needed.
XMP_Assert ( (2 <= byteCount) && (byteCount <= 6) );
// -------------------------------------------------------------------------------------
// Make sure the high order byte can hold the byte count mask, compute and set the mask.
size_t bitCount = 0; // The number of data bits in the first byte.
for ( cpTemp = (buffer[i] & UnsByte(0x3F)); cpTemp != 0; cpTemp = cpTemp >> 1 ) bitCount += 1;
if ( bitCount > (8 - (byteCount + 1)) ) byteCount += 1;
i = 8 - byteCount; // First byte index and mask shift count.
XMP_Assert ( (0 <= i) && (i <= 6) );
buffer[i] |= (UnsByte(0xFF) << i) & UnsByte(0xFF); // AUDIT: Safe, i is between 0 and 6.
}
utf8Str.assign ( (char*)(&buffer[i]), byteCount );
} // CodePointToUTF8
// -------------------------------------------------------------------------------------------------
// ApplyQuotes
// -----------
static void
ApplyQuotes ( XMP_VarString * item, UniCodePoint openQuote, UniCodePoint closeQuote, bool allowCommas )
{
bool prevSpace = false;
size_t charOffset, charLen;
UniCharKind charKind;
UniCodePoint uniChar;
// -----------------------------------------------------------------------------------------
// See if there are any separators in the value. Stop at the first occurrance. This is a bit
// tricky in order to make typical typing work conveniently. The purpose of applying quotes
// is to preserve the values when splitting them back apart. That is CatenateContainerItems
// and SeparateContainerItems must round trip properly. For the most part we only look for
// separators here. Internal quotes, as in -- Irving "Bud" Jones -- won't cause problems in
// the separation. An initial quote will though, it will make the value look quoted.
charOffset = 0;
ClassifyCharacter ( item->c_str(), charOffset, &charKind, &charLen, &uniChar );
if ( charKind != UCK_quote ) {
for ( charOffset = 0; size_t(charOffset) < item->size(); charOffset += charLen ) {
ClassifyCharacter ( item->c_str(), charOffset, &charKind, &charLen, &uniChar );
if ( charKind == UCK_space ) {
if ( prevSpace ) break; // Multiple spaces are a separator.
prevSpace = true;
} else {
prevSpace = false;
if ( (charKind == UCK_semicolon) || (charKind == UCK_control) ) break;
if ( (charKind == UCK_comma) && (! allowCommas) ) break;
}
}
}
if ( size_t(charOffset) < item->size() ) {
// --------------------------------------------------------------------------------------
// Create a quoted copy, doubling any internal quotes that match the outer ones. Internal
// quotes did not stop the "needs quoting" search, but they do need doubling. So we have
// to rescan the front of the string for quotes. Handle the special case of U+301D being
// closed by either U+301E or U+301F.
XMP_VarString newItem;
size_t splitPoint;
for ( splitPoint = 0; splitPoint <= charOffset; ++splitPoint ) {
ClassifyCharacter ( item->c_str(), splitPoint, &charKind, &charLen, &uniChar );
if ( charKind == UCK_quote ) break;
}
CodePointToUTF8 ( openQuote, newItem );
newItem.append ( *item, 0, splitPoint ); // Copy the leading "normal" portion.
for ( charOffset = splitPoint; size_t(charOffset) < item->size(); charOffset += charLen ) {
ClassifyCharacter ( item->c_str(), charOffset, &charKind, &charLen, &uniChar );
newItem.append ( *item, charOffset, charLen );
if ( (charKind == UCK_quote) && IsSurroundingQuote ( uniChar, openQuote, closeQuote ) ) {
newItem.append ( *item, charOffset, charLen );
}
}
XMP_VarString closeStr;
CodePointToUTF8 ( closeQuote, closeStr );
newItem.append ( closeStr );
*item = newItem;
}
} // ApplyQuotes
// -------------------------------------------------------------------------------------------------
// IsInternalProperty
// ------------------
// *** Need static checks of the schema prefixes!
static const char * kExternalxmpDM[] =
{ "xmpDM:album",
"xmpDM:altTapeName",
"xmpDM:altTimecode",
"xmpDM:artist",
"xmpDM:cameraAngle",
"xmpDM:cameraLabel",
"xmpDM:cameraModel",
"xmpDM:cameraMove",
"xmpDM:client",
"xmpDM:comment",
"xmpDM:composer",
"xmpDM:director",
"xmpDM:directorPhotography",
"xmpDM:engineer",
"xmpDM:genre",
"xmpDM:good",
"xmpDM:instrument",
"xmpDM:logComment",
"xmpDM:projectName",
"xmpDM:releaseDate",
"xmpDM:scene",
"xmpDM:shotDate",
"xmpDM:shotDay",
"xmpDM:shotLocation",
"xmpDM:shotName",
"xmpDM:shotNumber",
"xmpDM:shotSize",
"xmpDM:speakerPlacement",
"xmpDM:takeNumber",
"xmpDM:tapeName",
"xmpDM:trackNumber",
"xmpDM:videoAlphaMode",
"xmpDM:videoAlphaPremultipleColor",
0 }; // ! Must have zero sentinel!
typedef const char ** CharStarIterator; // Used for binary search of kExternalxmpDM;
static const char ** kLastExternalxmpDM = 0; // Set on first use.
static int CharStarLess (const char * left, const char * right )
{ return (strcmp ( left, right ) < 0); }
#define IsExternalProperty(s,p) (! IsInternalProperty ( s, p ))
static bool
IsInternalProperty ( const XMP_VarString & schema, const XMP_VarString & prop )
{
bool isInternal = false;
if ( schema == kXMP_NS_DC ) {
if ( (prop == "dc:format") ||
(prop == "dc:language") ) {
isInternal = true;
}
} else if ( schema == kXMP_NS_XMP ) {
if ( (prop == "xmp:BaseURL") ||
(prop == "xmp:CreatorTool") ||
(prop == "xmp:Format") ||
(prop == "xmp:Locale") ||
(prop == "xmp:MetadataDate") ||
(prop == "xmp:ModifyDate") ) {
isInternal = true;
}
} else if ( schema == kXMP_NS_PDF ) {
if ( (prop == "pdf:BaseURL") ||
(prop == "pdf:Creator") ||
(prop == "pdf:ModDate") ||
(prop == "pdf:PDFVersion") ||
(prop == "pdf:Producer") ) {
isInternal = true;
}
} else if ( schema == kXMP_NS_TIFF ) {
isInternal = true; // ! The TIFF properties are internal by default.
if ( (prop == "tiff:ImageDescription") || // ! ImageDescription, Artist, and Copyright are aliased.
(prop == "tiff:Artist") ||
(prop == "tiff:Copyright") ) {
isInternal = false;
}
} else if ( schema == kXMP_NS_EXIF ) {
isInternal = true; // ! The EXIF properties are internal by default.
if ( prop == "exif:UserComment" ) isInternal = false;
} else if ( schema == kXMP_NS_EXIF_Aux ) {
isInternal = true; // ! The EXIF Aux properties are internal by default.
} else if ( schema == kXMP_NS_Photoshop ) {
if ( (prop == "photoshop:ICCProfile") ||
(prop == "photoshop:TextLayers") ) isInternal = true;
} else if ( schema == kXMP_NS_CameraRaw ) {
isInternal = true; // All of crs: is internal, they are processing settings.
} else if ( schema == kXMP_NS_DM ) {
// ! Most of the xmpDM schema is internal, and unknown properties default to internal.
if ( kLastExternalxmpDM == 0 ) {
for ( kLastExternalxmpDM = &kExternalxmpDM[0]; *kLastExternalxmpDM != 0; ++kLastExternalxmpDM ) {}
}
isInternal = (! std::binary_search ( &kExternalxmpDM[0], kLastExternalxmpDM, prop.c_str(), CharStarLess ));
} else if ( schema == kXMP_NS_Script ) {
isInternal = true; // ! Most of the xmpScript schema is internal, and unknown properties default to internal.
if ( (prop == "xmpScript:action") || (prop == "xmpScript:character") || (prop == "xmpScript:dialog") ||
(prop == "xmpScript:sceneSetting") || (prop == "xmpScript:sceneTimeOfDay") ) {
isInternal = false;
}
} else if ( schema == kXMP_NS_BWF ) {
if ( prop == "bext:version" ) isInternal = true;
} else if ( schema == kXMP_NS_AdobeStockPhoto ) {
isInternal = true; // ! The bmsp schema has only internal properties.
} else if ( schema == kXMP_NS_XMP_MM ) {
isInternal = true; // ! The xmpMM schema has only internal properties.
} else if ( schema == kXMP_NS_XMP_Text ) {
isInternal = true; // ! The xmpT schema has only internal properties.
} else if ( schema == kXMP_NS_XMP_PagedFile ) {
isInternal = true; // ! The xmpTPg schema has only internal properties.
} else if ( schema == kXMP_NS_XMP_Graphics ) {
isInternal = true; // ! The xmpG schema has only internal properties.
} else if ( schema == kXMP_NS_XMP_Image ) {
isInternal = true; // ! The xmpGImg schema has only internal properties.
} else if ( schema == kXMP_NS_XMP_Font ) {
isInternal = true; // ! The stFNT schema has only internal properties.
}
return isInternal;
} // IsInternalProperty
// -------------------------------------------------------------------------------------------------
// RemoveSchemaChildren
// --------------------
static void
RemoveSchemaChildren ( XMP_NodePtrPos schemaPos, bool doAll )
{
XMP_Node * schemaNode = *schemaPos;
XMP_Assert ( XMP_NodeIsSchema ( schemaNode->options ) );
// ! Iterate backwards to reduce shuffling as children are erased and to simplify the logic for
// ! denoting the current child. (Erasing child n makes the old n+1 now be n.)
size_t propCount = schemaNode->children.size();
XMP_NodePtrPos beginPos = schemaNode->children.begin();
for ( size_t propNum = propCount-1, propLim = (size_t)(-1); propNum != propLim; --propNum ) {
XMP_NodePtrPos currProp = beginPos + propNum;
if ( doAll || IsExternalProperty ( schemaNode->name, (*currProp)->name ) ) {
delete *currProp; // ! Both delete the node and erase the pointer from the parent.
schemaNode->children.erase ( currProp );
}
}
if ( schemaNode->children.empty() ) {
XMP_Node * tree = schemaNode->parent;
tree->children.erase ( schemaPos );
delete schemaNode;
}
} // RemoveSchemaChildren
// -------------------------------------------------------------------------------------------------
// ItemValuesMatch
// ---------------
//
// Does the value comparisons for array merging as part of XMPUtils::AppendProperties.
static bool
ItemValuesMatch ( const XMP_Node * leftNode, const XMP_Node * rightNode )
{
const XMP_OptionBits leftForm = leftNode->options & kXMP_PropCompositeMask;
const XMP_OptionBits rightForm = leftNode->options & kXMP_PropCompositeMask;
if ( leftForm != rightForm ) return false;
if ( leftForm == 0 ) {
// Simple nodes, check the values and xml:lang qualifiers.
if ( leftNode->value != rightNode->value ) return false;
if ( (leftNode->options & kXMP_PropHasLang) != (rightNode->options & kXMP_PropHasLang) ) return false;
if ( leftNode->options & kXMP_PropHasLang ) {
if ( leftNode->qualifiers[0]->value != rightNode->qualifiers[0]->value ) return false;
}
} else if ( leftForm == kXMP_PropValueIsStruct ) {
// Struct nodes, see if all fields match, ignoring order.
if ( leftNode->children.size() != rightNode->children.size() ) return false;
for ( size_t leftNum = 0, leftLim = leftNode->children.size(); leftNum != leftLim; ++leftNum ) {
const XMP_Node * leftField = leftNode->children[leftNum];
const XMP_Node * rightField = FindConstChild ( rightNode, leftField->name.c_str() );
if ( (rightField == 0) || (! ItemValuesMatch ( leftField, rightField )) ) return false;
}
} else {
// Array nodes, see if the "leftNode" values are present in the "rightNode", ignoring order, duplicates,
// and extra values in the rightNode-> The rightNode is the destination for AppendProperties.
XMP_Assert ( leftForm & kXMP_PropValueIsArray );
for ( size_t leftNum = 0, leftLim = leftNode->children.size(); leftNum != leftLim; ++leftNum ) {
const XMP_Node * leftItem = leftNode->children[leftNum];
size_t rightNum, rightLim;
for ( rightNum = 0, rightLim = rightNode->children.size(); rightNum != rightLim; ++rightNum ) {
const XMP_Node * rightItem = rightNode->children[rightNum];
if ( ItemValuesMatch ( leftItem, rightItem ) ) break;
}
if ( rightNum == rightLim ) return false;
}
}
return true; // All of the checks passed.
} // ItemValuesMatch
// -------------------------------------------------------------------------------------------------
// AppendSubtree
// -------------
//
// The main implementation of XMPUtils::AppendProperties. See the description in TXMPMeta.hpp.
static void
AppendSubtree ( const XMP_Node * sourceNode, XMP_Node * destParent,
const bool mergeCompound, const bool replaceOld, const bool deleteEmpty )
{
XMP_NodePtrPos destPos;
XMP_Node * destNode = FindChildNode ( destParent, sourceNode->name.c_str(), kXMP_ExistingOnly, &destPos );
bool valueIsEmpty = false;
if ( XMP_PropIsSimple ( sourceNode->options ) ) {
valueIsEmpty = sourceNode->value.empty();
} else {
valueIsEmpty = sourceNode->children.empty();
}
if ( valueIsEmpty ) {
if ( deleteEmpty && (destNode != 0) ) {
delete ( destNode );
destParent->children.erase ( destPos );
}
return; // ! Done, empty values are either ignored or cause deletions.
}
if ( destNode == 0 ) {
// The one easy case, the destination does not exist.
destNode = CloneSubtree ( sourceNode, destParent, true /* skipEmpty */ );
XMP_Assert ( (destNode == 0) || (! destNode->value.empty()) || (! destNode->children.empty()) );
return;
}
// If we get here we're going to modify an existing property, either replacing or merging.
XMP_Assert ( (! valueIsEmpty) && (destNode != 0) );
XMP_OptionBits sourceForm = sourceNode->options & kXMP_PropCompositeMask;
XMP_OptionBits destForm = destNode->options & kXMP_PropCompositeMask;
bool replaceThis = replaceOld; // ! Don't modify replaceOld, it gets passed to inner calls.
if ( mergeCompound && (! XMP_PropIsSimple ( sourceForm )) ) replaceThis = false;
if ( replaceThis ) {
destNode->value = sourceNode->value; // *** Should use SetNode.
destNode->options = sourceNode->options;
destNode->RemoveChildren();
destNode->RemoveQualifiers();
CloneOffspring ( sourceNode, destNode, true /* skipEmpty */ );
if ( (! XMP_PropIsSimple ( destNode->options )) && destNode->children.empty() ) {
// Don't keep an empty array or struct. The source might be implicitly empty due to
// all children being empty. In this case CloneOffspring should skip them.
DeleteSubtree ( destPos );
}
return;
}
// From here on are cases for merging arrays or structs.
if ( XMP_PropIsSimple ( sourceForm ) || (sourceForm != destForm) ) return;
if ( sourceForm == kXMP_PropValueIsStruct ) {
// To merge a struct process the fields recursively. E.g. add simple missing fields. The
// recursive call to AppendSubtree will handle deletion for fields with empty values.
for ( size_t sourceNum = 0, sourceLim = sourceNode->children.size(); sourceNum != sourceLim && destNode!= NULL; ++sourceNum ) {
const XMP_Node * sourceField = sourceNode->children[sourceNum];
AppendSubtree ( sourceField, destNode, mergeCompound, replaceOld, deleteEmpty );
if ( deleteEmpty && destNode->children.empty() ) {
delete ( destNode );
destParent->children.erase ( destPos );
}
}
} else if ( sourceForm & kXMP_PropArrayIsAltText ) {
// Merge AltText arrays by the xml:lang qualifiers. Make sure x-default is first. Make a
// special check for deletion of empty values. Meaningful in AltText arrays because the
// xml:lang qualifier provides unambiguous source/dest correspondence.
XMP_Assert ( mergeCompound );
for ( size_t sourceNum = 0, sourceLim = sourceNode->children.size(); sourceNum != sourceLim && destNode!= NULL; ++sourceNum ) {
const XMP_Node * sourceItem = sourceNode->children[sourceNum];
if ( sourceItem->qualifiers.empty() || (sourceItem->qualifiers[0]->name != "xml:lang") ) continue;
XMP_Index destIndex = LookupLangItem ( destNode, sourceItem->qualifiers[0]->value );
if ( sourceItem->value.empty() ) {
if ( deleteEmpty && (destIndex != -1) ) {
delete ( destNode->children[destIndex] );
destNode->children.erase ( destNode->children.begin() + destIndex );
if ( destNode->children.empty() ) {
delete ( destNode );
destParent->children.erase ( destPos );
}
}
} else {
if ( destIndex != -1 ) {
// The source and dest arrays both have this language item.
if ( replaceOld ) { // ! Yes, check replaceOld not replaceThis!
destNode->children[destIndex]->value = sourceItem->value;
}
} else {
// The dest array does not have this language item, add it.
if ( (sourceItem->qualifiers[0]->value != "x-default") || destNode->children.empty() ) {
// Typical case, empty dest array or not "x-default". Non-empty should always have "x-default".
CloneSubtree ( sourceItem, destNode, true /* skipEmpty */ );
} else {
// Edge case, non-empty dest array had no "x-default", insert that at the beginning.
XMP_Node * destItem = new XMP_Node ( destNode, sourceItem->name, sourceItem->value, sourceItem->options );
CloneOffspring ( sourceItem, destItem, true /* skipEmpty */ );
destNode->children.insert ( destNode->children.begin(), destItem );
}
}
}
}
} else if ( sourceForm & kXMP_PropValueIsArray ) {
// Merge other arrays by item values. Don't worry about order or duplicates. Source
// items with empty values do not cause deletion, that conflicts horribly with merging.
for ( size_t sourceNum = 0, sourceLim = sourceNode->children.size(); sourceNum != sourceLim; ++sourceNum ) {
const XMP_Node * sourceItem = sourceNode->children[sourceNum];
size_t destNum, destLim;
for ( destNum = 0, destLim = destNode->children.size(); destNum != destLim; ++destNum ) {
const XMP_Node * destItem = destNode->children[destNum];
if ( ItemValuesMatch ( sourceItem, destItem ) ) break;
}
if ( destNum == destLim ) CloneSubtree ( sourceItem, destNode, true /* skipEmpty */ );
}
}
} // AppendSubtree
// =================================================================================================
// Class Static Functions
// ======================
// -------------------------------------------------------------------------------------------------
// CatenateArrayItems
// ------------------
/* class static */ void
XMPUtils::CatenateArrayItems ( const XMPMeta & xmpObj,
XMP_StringPtr schemaNS,
XMP_StringPtr arrayName,
XMP_StringPtr separator,
XMP_StringPtr quotes,
XMP_OptionBits options,
XMP_VarString * catedStr )
{
XMP_Assert ( (schemaNS != 0) && (arrayName != 0) ); // ! Enforced by wrapper.
XMP_Assert ( (separator != 0) && (quotes != 0) && (catedStr != 0) ); // ! Enforced by wrapper.
size_t strLen, strPos, charLen;
UniCharKind charKind;
UniCodePoint currUCP, openQuote, closeQuote;
const bool allowCommas = ((options & kXMPUtil_AllowCommas) != 0);
const XMP_Node * arrayNode = 0; // ! Move up to avoid gcc complaints.
XMP_OptionBits arrayForm = 0;
const XMP_Node * currItem = 0;
// Make sure the separator is OK. It must be one semicolon surrounded by zero or more spaces.
// Any of the recognized semicolons or spaces are allowed.
strPos = 0;
strLen = strlen ( separator );
bool haveSemicolon = false;
while ( strPos < strLen ) {
ClassifyCharacter ( separator, strPos, &charKind, &charLen, &currUCP );
strPos += charLen;
if ( charKind == UCK_semicolon ) {
if ( haveSemicolon ) XMP_Throw ( "Separator can have only one semicolon", kXMPErr_BadParam );
haveSemicolon = true;
} else if ( charKind != UCK_space ) {
XMP_Throw ( "Separator can have only spaces and one semicolon", kXMPErr_BadParam );
}
};
if ( ! haveSemicolon ) XMP_Throw ( "Separator must have one semicolon", kXMPErr_BadParam );
// Make sure the open and close quotes are a legitimate pair.
strLen = strlen ( quotes );
ClassifyCharacter ( quotes, 0, &charKind, &charLen, &openQuote );
if ( charKind != UCK_quote ) XMP_Throw ( "Invalid quoting character", kXMPErr_BadParam );
if ( charLen == strLen ) {
closeQuote = openQuote;
} else {
strPos = charLen;
ClassifyCharacter ( quotes, strPos, &charKind, &charLen, &closeQuote );
if ( charKind != UCK_quote ) XMP_Throw ( "Invalid quoting character", kXMPErr_BadParam );
if ( (strPos + charLen) != strLen ) XMP_Throw ( "Quoting string too long", kXMPErr_BadParam );
}
if ( closeQuote != GetClosingQuote ( openQuote ) ) XMP_Throw ( "Mismatched quote pair", kXMPErr_BadParam );
// Return an empty result if the array does not exist, hurl if it isn't the right form.
catedStr->erase();
XMP_ExpandedXPath arrayPath;
ExpandXPath ( schemaNS, arrayName, &arrayPath );
arrayNode = FindConstNode ( &xmpObj.tree, arrayPath );
if ( arrayNode == 0 ) return;
arrayForm = arrayNode->options & kXMP_PropCompositeMask;
if ( (! (arrayForm & kXMP_PropValueIsArray)) || (arrayForm & kXMP_PropArrayIsAlternate) ) {
XMP_Throw ( "Named property must be non-alternate array", kXMPErr_BadParam );
}
if ( arrayNode->children.empty() ) return;
// Build the result, quoting the array items, adding separators. Hurl if any item isn't simple.
// Start the result with the first value, then add the rest with a preceeding separator.
currItem = arrayNode->children[0];
if ( (currItem->options & kXMP_PropCompositeMask) != 0 ) XMP_Throw ( "Array items must be simple", kXMPErr_BadParam );
*catedStr = currItem->value;
ApplyQuotes ( catedStr, openQuote, closeQuote, allowCommas );
for ( size_t itemNum = 1, itemLim = arrayNode->children.size(); itemNum != itemLim; ++itemNum ) {
const XMP_Node * item = arrayNode->children[itemNum];
if ( (item->options & kXMP_PropCompositeMask) != 0 ) XMP_Throw ( "Array items must be simple", kXMPErr_BadParam );
XMP_VarString tempStr ( item->value );
ApplyQuotes ( &tempStr, openQuote, closeQuote, allowCommas );
*catedStr += separator;
*catedStr += tempStr;
}
} // CatenateArrayItems
// -------------------------------------------------------------------------------------------------
// SeparateArrayItems
// ------------------
/* class static */ void
XMPUtils::SeparateArrayItems ( XMPMeta * xmpObj,
XMP_StringPtr schemaNS,
XMP_StringPtr arrayName,
XMP_OptionBits options,
XMP_StringPtr catedStr )
{
XMP_Assert ( (schemaNS != 0) && (arrayName != 0) && (catedStr != 0) ); // ! Enforced by wrapper.
XMP_VarString itemValue;
size_t itemStart, itemEnd;
size_t nextSize, charSize = 0; // Avoid VS uninit var warnings.
UniCharKind nextKind, charKind = UCK_normal;
UniCodePoint nextChar, uniChar = 0;
// Extract "special" option bits, verify and normalize the others.
bool preserveCommas = false;
if ( options & kXMPUtil_AllowCommas ) {
preserveCommas = true;
options ^= kXMPUtil_AllowCommas;
}
options = VerifySetOptions ( options, 0 ); // Keep a zero value, has special meaning below.
if ( options & ~kXMP_PropArrayFormMask ) XMP_Throw ( "Options can only provide array form", kXMPErr_BadOptions );
// Find the array node, make sure it is OK. Move the current children aside, to be readded later if kept.
XMP_ExpandedXPath arrayPath;
ExpandXPath ( schemaNS, arrayName, &arrayPath );
XMP_Node * arrayNode = FindNode ( &xmpObj->tree, arrayPath, kXMP_ExistingOnly );
if ( arrayNode != 0 ) {
// The array exists, make sure the form is compatible. Zero arrayForm means take what exists.
XMP_OptionBits arrayForm = arrayNode->options & kXMP_PropArrayFormMask;
if ( (arrayForm == 0) || (arrayForm & kXMP_PropArrayIsAlternate) ) {
XMP_Throw ( "Named property must be non-alternate array", kXMPErr_BadXPath );
}
if ( (options != 0) && (options != arrayForm) ) XMP_Throw ( "Mismatch of specified and existing array form", kXMPErr_BadXPath ); // *** Right error?
} else {
// The array does not exist, try to create it.
arrayNode = FindNode ( &xmpObj->tree, arrayPath, kXMP_CreateNodes, (options | kXMP_PropValueIsArray) );
if ( arrayNode == 0 ) XMP_Throw ( "Failed to create named array", kXMPErr_BadXPath );
}
XMP_NodeOffspring oldChildren ( arrayNode->children );
size_t oldChildCount = oldChildren.size();
arrayNode->children.clear();
// Extract the item values one at a time, until the whole input string is done. Be very careful
// in the extraction about the string positions. They are essentially byte pointers, while the
// contents are UTF-8. Adding or subtracting 1 does not necessarily move 1 Unicode character!
size_t endPos = strlen ( catedStr );
itemEnd = 0;
while ( itemEnd < endPos ) {
// Skip any leading spaces and separation characters. Always skip commas here. They can be
// kept when within a value, but not when alone between values.
for ( itemStart = itemEnd; itemStart < endPos; itemStart += charSize ) {
ClassifyCharacter ( catedStr, itemStart, &charKind, &charSize, &uniChar );
if ( (charKind == UCK_normal) || (charKind == UCK_quote) ) break;
}
if ( itemStart >= endPos ) break;
if ( charKind != UCK_quote ) {
// This is not a quoted value. Scan for the end, create an array item from the substring.
for ( itemEnd = itemStart; itemEnd < endPos; itemEnd += charSize ) {
ClassifyCharacter ( catedStr, itemEnd, &charKind, &charSize, &uniChar );
if ( (charKind == UCK_normal) || (charKind == UCK_quote) ) continue;
if ( (charKind == UCK_comma) && preserveCommas ) continue;
if ( charKind != UCK_space ) break;
if ( (itemEnd + charSize) >= endPos ) break; // Anything left?
ClassifyCharacter ( catedStr, (itemEnd+charSize), &nextKind, &nextSize, &nextChar );
if ( (nextKind == UCK_normal) || (nextKind == UCK_quote) ) continue;
if ( (nextKind == UCK_comma) && preserveCommas ) continue;
break; // Have multiple spaces, or a space followed by a separator.
}
itemValue.assign ( catedStr, itemStart, (itemEnd - itemStart) );
} else {
// Accumulate quoted values into a local string, undoubling internal quotes that
// match the surrounding quotes. Do not undouble "unmatching" quotes.
UniCodePoint openQuote = uniChar;
UniCodePoint closeQuote = GetClosingQuote ( openQuote );
itemStart += charSize; // Skip the opening quote;
itemValue.erase();
for ( itemEnd = itemStart; itemEnd < endPos; itemEnd += charSize ) {
ClassifyCharacter ( catedStr, itemEnd, &charKind, &charSize, &uniChar );
if ( (charKind != UCK_quote) || (! IsSurroundingQuote ( uniChar, openQuote, closeQuote)) ) {
// This is not a matching quote, just append it to the item value.
itemValue.append ( catedStr, itemEnd, charSize );
} else {
// This is a "matching" quote. Is it doubled, or the final closing quote? Tolerate
// various edge cases like undoubled opening (non-closing) quotes, or end of input.
if ( (itemEnd + charSize) < endPos ) {
ClassifyCharacter ( catedStr, itemEnd+charSize, &nextKind, &nextSize, &nextChar );
} else {
nextKind = UCK_semicolon; nextSize = 0; nextChar = 0x3B;
}
if ( uniChar == nextChar ) {
// This is doubled, copy it and skip the double.
itemValue.append ( catedStr, itemEnd, charSize );
itemEnd += nextSize; // Loop will add in charSize.
} else if ( ! IsClosingingQuote ( uniChar, openQuote, closeQuote ) ) {
// This is an undoubled, non-closing quote, copy it.
itemValue.append ( catedStr, itemEnd, charSize );
} else {
// This is an undoubled closing quote, skip it and exit the loop.
itemEnd += charSize;
break;
}
}
} // Loop to accumulate the quoted value.
}
// Add the separated item to the array. Keep a matching old value in case it had separators.
size_t oldChild;
for ( oldChild = 0; oldChild < oldChildCount; ++oldChild ) {
if ( (oldChildren[oldChild] != 0) && (itemValue == oldChildren[oldChild]->value) ) break;
}
XMP_Node * newItem = 0;
if ( oldChild == oldChildCount ) {
newItem = new XMP_Node ( arrayNode, kXMP_ArrayItemName, itemValue.c_str(), 0 );
} else {
newItem = oldChildren[oldChild];
oldChildren[oldChild] = 0; // ! Don't match again, let duplicates be seen.
}
arrayNode->children.push_back ( newItem );
} // Loop through all of the returned items.
// Delete any of the old children that were not kept.
for ( size_t i = 0; i < oldChildCount; ++i ) {
if ( oldChildren[i] != 0 ) delete oldChildren[i];
}
} // SeparateArrayItems
// -------------------------------------------------------------------------------------------------
// ApplyTemplate
// -------------
/* class static */ void
XMPUtils::ApplyTemplate ( XMPMeta * workingXMP,
const XMPMeta & templateXMP,
XMP_OptionBits actions )
{
bool doClear = XMP_OptionIsSet ( actions, kXMPTemplate_ClearUnnamedProperties );
bool doAdd = XMP_OptionIsSet ( actions, kXMPTemplate_AddNewProperties );
bool doReplace = XMP_OptionIsSet ( actions, kXMPTemplate_ReplaceExistingProperties );
bool deleteEmpty = XMP_OptionIsSet ( actions, kXMPTemplate_ReplaceWithDeleteEmpty );
doReplace |= deleteEmpty; // Delete-empty implies Replace.
deleteEmpty &= (! doClear); // Clear implies not delete-empty, but keep the implicit Replace.
bool doAll = XMP_OptionIsSet ( actions, kXMPTemplate_IncludeInternalProperties );
// ! In several places we do loops backwards so that deletions do not perturb the remaining indices.
// ! These loops use ordinals (size .. 1), we must use a zero based index inside the loop.
if ( doClear ) {
// Visit the top level working properties, delete if not in the template.
for ( size_t schemaOrdinal = workingXMP->tree.children.size(); schemaOrdinal > 0; --schemaOrdinal ) {
size_t schemaNum = schemaOrdinal-1; // ! Convert ordinal to index!
XMP_Node * workingSchema = workingXMP->tree.children[schemaNum];
const XMP_Node * templateSchema = FindConstSchema ( &templateXMP.tree, workingSchema->name.c_str() );
if ( templateSchema == 0 ) {
// The schema is not in the template, delete all properties or just all external ones.
if ( doAll ) {
workingSchema->RemoveChildren(); // Remove the properties here, delete the schema below.
} else {
for ( size_t propOrdinal = workingSchema->children.size(); propOrdinal > 0; --propOrdinal ) {
size_t propNum = propOrdinal-1; // ! Convert ordinal to index!
XMP_Node * workingProp = workingSchema->children[propNum];
if ( IsExternalProperty ( workingSchema->name, workingProp->name ) ) {
delete ( workingProp );
workingSchema->children.erase ( workingSchema->children.begin() + propNum );
}
}
}
} else {
// Check each of the working XMP's properties to see if it is in the template.
for ( size_t propOrdinal = workingSchema->children.size(); propOrdinal > 0; --propOrdinal ) {
size_t propNum = propOrdinal-1; // ! Convert ordinal to index!
XMP_Node * workingProp = workingSchema->children[propNum];
if ( (doAll || IsExternalProperty ( workingSchema->name, workingProp->name )) &&
(FindConstChild ( templateSchema, workingProp->name.c_str() ) == 0) ) {
delete ( workingProp );
workingSchema->children.erase ( workingSchema->children.begin() + propNum );
}
}
}
if ( workingSchema->children.empty() ) {
delete ( workingSchema );
workingXMP->tree.children.erase ( workingXMP->tree.children.begin() + schemaNum );
}
}
}
if ( doAdd | doReplace ) {
for ( size_t schemaNum = 0, schemaLim = templateXMP.tree.children.size(); schemaNum < schemaLim; ++schemaNum ) {
const XMP_Node * templateSchema = templateXMP.tree.children[schemaNum];
// Make sure we have an output schema node, then process the top level template properties.
XMP_NodePtrPos workingSchemaPos;
XMP_Node * workingSchema = FindSchemaNode ( &workingXMP->tree, templateSchema->name.c_str(),
kXMP_ExistingOnly, &workingSchemaPos );
if ( workingSchema == 0 ) {
workingSchema = new XMP_Node ( &workingXMP->tree, templateSchema->name, templateSchema->value, kXMP_SchemaNode );
workingXMP->tree.children.push_back ( workingSchema );
workingSchemaPos = workingXMP->tree.children.end() - 1;
}
for ( size_t propNum = 0, propLim = templateSchema->children.size(); propNum < propLim; ++propNum ) {
const XMP_Node * templateProp = templateSchema->children[propNum];
if ( doAll || IsExternalProperty ( templateSchema->name, templateProp->name ) ) {
AppendSubtree ( templateProp, workingSchema, doAdd, doReplace, deleteEmpty );
}
}
if ( workingSchema->children.empty() ) {
delete ( workingSchema );
workingXMP->tree.children.erase ( workingSchemaPos );
}
}
}
} // ApplyTemplate
// -------------------------------------------------------------------------------------------------
// RemoveProperties
// ----------------
/* class static */ void
XMPUtils::RemoveProperties ( XMPMeta * xmpObj,
XMP_StringPtr schemaNS,
XMP_StringPtr propName,
XMP_OptionBits options )
{
XMP_Assert ( (schemaNS != 0) && (propName != 0) ); // ! Enforced by wrapper.
const bool doAll = XMP_TestOption (options, kXMPUtil_DoAllProperties );
const bool includeAliases = XMP_TestOption ( options, kXMPUtil_IncludeAliases );
if ( *propName != 0 ) {
// Remove just the one indicated property. This might be an alias, the named schema might
// not actually exist. So don't lookup the schema node.
if ( *schemaNS == 0 ) XMP_Throw ( "Property name requires schema namespace", kXMPErr_BadParam );
XMP_ExpandedXPath expPath;
ExpandXPath ( schemaNS, propName, &expPath );
XMP_NodePtrPos propPos;
XMP_Node * propNode = FindNode ( &(xmpObj->tree), expPath, kXMP_ExistingOnly, kXMP_NoOptions, &propPos );
if ( propNode != 0 ) {
if ( doAll || IsExternalProperty ( expPath[kSchemaStep].step, expPath[kRootPropStep].step ) ) {
XMP_Node * parent = propNode->parent; // *** Should have XMP_Node::RemoveChild(pos).
delete propNode; // ! Both delete the node and erase the pointer from the parent.
parent->children.erase ( propPos );
DeleteEmptySchema ( parent );
}
}
} else if ( *schemaNS != 0 ) {
// Remove all properties from the named schema. Optionally include aliases, in which case
// there might not be an actual schema node.
XMP_NodePtrPos schemaPos;
XMP_Node * schemaNode = FindSchemaNode ( &xmpObj->tree, schemaNS, kXMP_ExistingOnly, &schemaPos );
if ( schemaNode != 0 ) RemoveSchemaChildren ( schemaPos, doAll );
if ( includeAliases ) {
// We're removing the aliases also. Look them up by their namespace prefix. Yes, the
// alias map is sorted so we could process just that portion. But that takes more code
// and the extra speed isn't worth it. (Plus this way we avoid a dependence on the map
// implementation.) Lookup the XMP node from the alias, to make sure the actual exists.
XMP_StringPtr nsPrefix;
XMP_StringLen nsLen;
(void) XMPMeta::GetNamespacePrefix ( schemaNS, &nsPrefix, &nsLen );
XMP_AliasMapPos currAlias = sRegisteredAliasMap->begin();
XMP_AliasMapPos endAlias = sRegisteredAliasMap->end();
for ( ; currAlias != endAlias; ++currAlias ) {
if ( strncmp ( currAlias->first.c_str(), nsPrefix, nsLen ) == 0 ) {
XMP_NodePtrPos actualPos;
XMP_Node * actualProp = FindNode ( &xmpObj->tree, currAlias->second, kXMP_ExistingOnly, kXMP_NoOptions, &actualPos );
if ( actualProp != 0 ) {
XMP_Node * rootProp = actualProp;
while ( ! XMP_NodeIsSchema ( rootProp->parent->options ) ) rootProp = rootProp->parent;
if ( doAll || IsExternalProperty ( rootProp->parent->name, rootProp->name ) ) {
XMP_Node * parent = actualProp->parent;
delete actualProp; // ! Both delete the node and erase the pointer from the parent.
parent->children.erase ( actualPos );
DeleteEmptySchema ( parent );
}
}
}
}
}
} else {
// Remove all appropriate properties from all schema. In this case we don't have to be
// concerned with aliases, they are handled implicitly from the actual properties.
// ! Iterate backwards to reduce shuffling if schema are erased and to simplify the logic
// ! for denoting the current schema. (Erasing schema n makes the old n+1 now be n.)
size_t schemaCount = xmpObj->tree.children.size();
XMP_NodePtrPos beginPos = xmpObj->tree.children.begin();
for ( size_t schemaNum = schemaCount-1, schemaLim = (size_t)(-1); schemaNum != schemaLim; --schemaNum ) {
XMP_NodePtrPos currSchema = beginPos + schemaNum;
RemoveSchemaChildren ( currSchema, doAll );
}
}
} // RemoveProperties
// -------------------------------------------------------------------------------------------------
// DuplicateSubtree
// ----------------
/* class static */ void
XMPUtils::DuplicateSubtree ( const XMPMeta & source,
XMPMeta * dest,
XMP_StringPtr sourceNS,
XMP_StringPtr sourceRoot,
XMP_StringPtr destNS,
XMP_StringPtr destRoot,
XMP_OptionBits options )
{
IgnoreParam(options);
bool fullSourceTree = false;
bool fullDestTree = false;
XMP_ExpandedXPath sourcePath, destPath;
const XMP_Node * sourceNode = 0;
XMP_Node * destNode = 0;
XMP_Assert ( (sourceNS != 0) && (*sourceNS != 0) );
XMP_Assert ( (sourceRoot != 0) && (*sourceRoot != 0) );
XMP_Assert ( (dest != 0) && (destNS != 0) && (destRoot != 0) );
if ( *destNS == 0 ) destNS = sourceNS;
if ( *destRoot == 0 ) destRoot = sourceRoot;
if ( XMP_LitMatch ( sourceNS, "*" ) ) fullSourceTree = true;
if ( XMP_LitMatch ( destNS, "*" ) ) fullDestTree = true;
if ( (&source == dest) && (fullSourceTree | fullDestTree) ) {
XMP_Throw ( "Can't duplicate tree onto itself", kXMPErr_BadParam );
}
if ( fullSourceTree & fullDestTree ) XMP_Throw ( "Use Clone for full tree to full tree", kXMPErr_BadParam );
if ( fullSourceTree ) {
// The destination must be an existing empty struct, copy all of the source top level as fields.
ExpandXPath ( destNS, destRoot, &destPath );
destNode = FindNode ( &dest->tree, destPath, kXMP_ExistingOnly );
if ( (destNode == 0) || (! XMP_PropIsStruct ( destNode->options )) ) {
XMP_Throw ( "Destination must be an existing struct", kXMPErr_BadXPath );
}
if ( ! destNode->children.empty() ) {
if ( options & kXMP_DeleteExisting ) {
destNode->RemoveChildren();
} else {
XMP_Throw ( "Destination must be an empty struct", kXMPErr_BadXPath );
}
}
for ( size_t schemaNum = 0, schemaLim = source.tree.children.size(); schemaNum < schemaLim; ++schemaNum ) {
const XMP_Node * currSchema = source.tree.children[schemaNum];
for ( size_t propNum = 0, propLim = currSchema->children.size(); propNum < propLim; ++propNum ) {
sourceNode = currSchema->children[propNum];
XMP_Node * copyNode = new XMP_Node ( destNode, sourceNode->name, sourceNode->value, sourceNode->options );
destNode->children.push_back ( copyNode );
CloneOffspring ( sourceNode, copyNode );
}
}
} else if ( fullDestTree ) {
// The source node must be an existing struct, copy all of the fields to the dest top level.
XMP_ExpandedXPath srcPath;
ExpandXPath ( sourceNS, sourceRoot, &srcPath );
sourceNode = FindConstNode ( &source.tree, srcPath );
if ( (sourceNode == 0) || (! XMP_PropIsStruct ( sourceNode->options )) ) {
XMP_Throw ( "Source must be an existing struct", kXMPErr_BadXPath );
}
destNode = &dest->tree;
if ( ! destNode->children.empty() ) {
if ( options & kXMP_DeleteExisting ) {
destNode->RemoveChildren();
} else {
XMP_Throw ( "Destination tree must be empty", kXMPErr_BadXPath );
}
}
std::string nsPrefix;
XMP_StringPtr nsURI;
XMP_StringLen nsLen;
for ( size_t fieldNum = 0, fieldLim = sourceNode->children.size(); fieldNum < fieldLim; ++fieldNum ) {
const XMP_Node * currField = sourceNode->children[fieldNum];
size_t colonPos = currField->name.find ( ':' );
if ( colonPos == std::string::npos ) continue;
nsPrefix.assign ( currField->name.c_str(), colonPos );
bool nsOK = XMPMeta::GetNamespaceURI ( nsPrefix.c_str(), &nsURI, &nsLen );
if ( ! nsOK ) XMP_Throw ( "Source field namespace is not global", kXMPErr_BadSchema );
XMP_Node * destSchema = FindSchemaNode ( &dest->tree, nsURI, kXMP_CreateNodes );
if ( destSchema == 0 ) XMP_Throw ( "Failed to find destination schema", kXMPErr_BadSchema );
XMP_Node * copyNode = new XMP_Node ( destSchema, currField->name, currField->value, currField->options );
destSchema->children.push_back ( copyNode );
CloneOffspring ( currField, copyNode );
}
} else {
// Find the root nodes for the source and destination subtrees.
ExpandXPath ( sourceNS, sourceRoot, &sourcePath );
ExpandXPath ( destNS, destRoot, &destPath );
sourceNode = FindConstNode ( &source.tree, sourcePath );
if ( sourceNode == 0 ) XMP_Throw ( "Can't find source subtree", kXMPErr_BadXPath );
destNode = FindNode ( &dest->tree, destPath, kXMP_ExistingOnly ); // Dest must not yet exist.
if ( destNode != 0 ) XMP_Throw ( "Destination subtree must not exist", kXMPErr_BadXPath );
destNode = FindNode ( &dest->tree, destPath, kXMP_CreateNodes ); // Now create the dest.
if ( destNode == 0 ) XMP_Throw ( "Can't create destination root node", kXMPErr_BadXPath );
// Make sure the destination is not within the source! The source can't be inside the destination
// because the source already existed and the destination was just created.
if ( &source == dest ) {
for ( XMP_Node * testNode = destNode; testNode != 0; testNode = testNode->parent ) {
if ( testNode == sourceNode ) {
// *** delete the just-created dest root node
XMP_Throw ( "Destination subtree is within the source subtree", kXMPErr_BadXPath );
}
}
}
// *** Could use a CloneTree util here and maybe elsewhere.
destNode->value = sourceNode->value; // *** Should use SetNode.
destNode->options = sourceNode->options;
CloneOffspring ( sourceNode, destNode );
}
} // DuplicateSubtree
// =================================================================================================