public AddChild ( |
||
node | an XMPNode | |
return | void |
/// <summary> /// Appends a language item to an alt text array. /// </summary> /// <param name="arrayNode"> the language array </param> /// <param name="itemLang"> the language of the item </param> /// <param name="itemValue"> the content of the item </param> /// <exception cref="XmpException"> Thrown if a duplicate property is added </exception> internal static void AppendLangItem(XmpNode arrayNode, string itemLang, string itemValue) { XmpNode newItem = new XmpNode(ARRAY_ITEM_NAME, itemValue, null); XmpNode langQual = new XmpNode(XML_LANG, itemLang, null); newItem.AddQualifier(langQual); if (!X_DEFAULT.Equals(langQual.Value)) { arrayNode.AddChild(newItem); } else { arrayNode.AddChild(1, newItem); } }
// ------------------------------------------------------------------------------------- // private /// <summary> /// Locate or create the item node and set the value. Note the index /// parameter is one-based! The index can be in the range [1..size + 1] or /// "last()", normalize it and check the insert flags. The order of the /// normalization checks is important. If the array is empty we end up with /// an index and location to set item size + 1. /// </summary> /// <param name="arrayNode"> an array node </param> /// <param name="itemIndex"> the index where to insert the item </param> /// <param name="itemValue"> the item value </param> /// <param name="itemOptions"> the options for the new item </param> /// <param name="insert"> insert oder overwrite at index position? </param> /// <exception cref="XmpException"> </exception> private void DoSetArrayItem(XmpNode arrayNode, int itemIndex, string itemValue, PropertyOptions itemOptions, bool insert) { XmpNode itemNode = new XmpNode(ARRAY_ITEM_NAME, null); itemOptions = XmpNodeUtils.VerifySetOptions(itemOptions, itemValue); // in insert mode the index after the last is allowed, // even ARRAY_LAST_ITEM points to the index *after* the last. int maxIndex = insert ? arrayNode.ChildrenLength + 1 : arrayNode.ChildrenLength; if (itemIndex == ARRAY_LAST_ITEM) { itemIndex = maxIndex; } if (1 <= itemIndex && itemIndex <= maxIndex) { if (!insert) { arrayNode.RemoveChild(itemIndex); } arrayNode.AddChild(itemIndex, itemNode); SetNode(itemNode, itemValue, itemOptions, false); } else { throw new XmpException("Array index out of bounds", XmpError.BADINDEX); } }
/// <summary> /// Find or create a child node under a given parent node. If the parent node is no /// Returns the found or created child node. /// </summary> /// <param name="parent"> /// the parent node </param> /// <param name="childName"> /// the node name to find </param> /// <param name="createNodes"> /// flag, if new nodes shall be created. </param> /// <returns> Returns the found or created node or <code>null</code>. </returns> /// <exception cref="XmpException"> Thrown if </exception> internal static XmpNode FindChildNode(XmpNode parent, string childName, bool createNodes) { if (!parent.Options.SchemaNode && !parent.Options.Struct) { if (!parent.Implicit) { throw new XmpException("Named children only allowed for schemas and structs", XmpError.BADXPATH); } if (parent.Options.Array) { throw new XmpException("Named children not allowed for arrays", XmpError.BADXPATH); } if (createNodes) { parent.Options.Struct = true; } } XmpNode childNode = parent.FindChildByName(childName); if (childNode == null && createNodes) { PropertyOptions options = new PropertyOptions(); childNode = new XmpNode(childName, options); childNode.Implicit = true; parent.AddChild(childNode); } Debug.Assert(childNode != null || !createNodes); return(childNode); }
/// <param name="arrayNode"> an array node </param> /// <param name="segment"> the segment containing the array index </param> /// <param name="createNodes"> flag if new nodes are allowed to be created. </param> /// <returns> Returns the index or index = -1 if not found </returns> /// <exception cref="XmpException"> Throws Exceptions </exception> private static int FindIndexedItem(XmpNode arrayNode, string segment, bool createNodes) { int index; try { segment = segment.Substring(1, segment.Length - 1 - 1); index = Convert.ToInt32(segment); if (index < 1) { throw new XmpException("Array index must be larger than zero", XmpError.BADXPATH); } } catch (FormatException) { throw new XmpException("Array index not digits.", XmpError.BADXPATH); } if (createNodes && index == arrayNode.ChildrenLength + 1) { // Append a new last + 1 node. XmpNode newItem = new XmpNode(ARRAY_ITEM_NAME, null); newItem.Implicit = true; arrayNode.AddChild(newItem); } return(index); }
/// <summary> /// Find or create a schema node if <code>createNodes</code> is true. /// </summary> /// <param name="tree"> the root of the xmp tree. </param> /// <param name="namespaceUri"> a namespace </param> /// <param name="suggestedPrefix"> If a prefix is suggested, the namespace is allowed to be registered. </param> /// <param name="createNodes"> a flag indicating if the node shall be created if not found. /// <em>Note:</em> The namespace must be registered prior to this call. /// </param> /// <returns> Returns the schema node if found, <code>null</code> otherwise. /// Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b> /// returned a valid node. </returns> /// <exception cref="XmpException"> An exception is only thrown if an error occurred, not if a /// node was not found. </exception> internal static XmpNode FindSchemaNode(XmpNode tree, string namespaceUri, string suggestedPrefix, bool createNodes) { Debug.Assert(tree.Parent == null); // make sure that its the root XmpNode schemaNode = tree.FindChildByName(namespaceUri); if (schemaNode == null && createNodes) { PropertyOptions propertyOptions = new PropertyOptions(); propertyOptions.SchemaNode = true; schemaNode = new XmpNode(namespaceUri, propertyOptions); schemaNode.Implicit = true; // only previously registered schema namespaces are allowed in the XMP tree. string prefix = XMPMetaFactory.SchemaRegistry.GetNamespacePrefix(namespaceUri); if (prefix == null) { if (!String.IsNullOrEmpty(suggestedPrefix)) { prefix = XMPMetaFactory.SchemaRegistry.RegisterNamespace(namespaceUri, suggestedPrefix); } else { throw new XmpException("Unregistered schema namespace URI", XmpError.BADSCHEMA); } } schemaNode.Value = prefix; tree.AddChild(schemaNode); } return(schemaNode); }
/// <summary> /// Make sure the x-default item is first. Touch up "single value" /// arrays that have a default plus one real language. This case should have /// the same value for both items. Older Adobe apps were hardwired to only /// use the "x-default" item, so we copy that value to the other /// item. /// </summary> /// <param name="arrayNode"> /// an alt text array node </param> internal static void NormalizeLangArray(XmpNode arrayNode) { if (!arrayNode.Options.ArrayAltText) { return; } // check if node with x-default qual is first place for (int i = 2; i <= arrayNode.ChildrenLength; i++) { XmpNode child = arrayNode.GetChild(i); if (child.HasQualifier() && X_DEFAULT.Equals(child.GetQualifier(1).Value)) { // move node to first place try { arrayNode.RemoveChild(i); arrayNode.AddChild(1, child); } catch (XmpException) { // cannot occur, because same child is removed before Debug.Assert(false); } if (i == 2) { arrayNode.GetChild(2).Value = child.Value; } break; } } }
/// <summary> /// Searches for a qualifier selector in a node: /// [?qualName="value"] - an element in an array, chosen by a qualifier value. /// No implicit nodes are created for qualifier selectors, /// except for an alias to an x-default item. /// </summary> /// <param name="arrayNode"> an array node </param> /// <param name="qualName"> the qualifier name </param> /// <param name="qualValue"> the qualifier value </param> /// <param name="aliasForm"> in case the qual selector results from an alias, /// an x-default node is created if there has not been one. </param> /// <returns> Returns the index of th </returns> /// <exception cref="XmpException"> </exception> private static int LookupQualSelector(XmpNode arrayNode, string qualName, string qualValue, uint aliasForm) { if (XML_LANG.Equals(qualName)) { qualValue = Utils.NormalizeLangValue(qualValue); int index = LookupLanguageItem(arrayNode, qualValue); if (index < 0 && (aliasForm & AliasOptions.PROP_ARRAY_ALT_TEXT) > 0) { XmpNode langNode = new XmpNode(ARRAY_ITEM_NAME, null); XmpNode xdefault = new XmpNode(XML_LANG, X_DEFAULT, null); langNode.AddQualifier(xdefault); arrayNode.AddChild(1, langNode); return(1); } return(index); } for (int index = 1; index < arrayNode.ChildrenLength; index++) { XmpNode currItem = arrayNode.GetChild(index); for (IEnumerator it = currItem.IterateQualifier(); it.MoveNext();) { XmpNode qualifier = (XmpNode)it.Current; if (qualifier != null && qualName.Equals(qualifier.Name) && qualValue.Equals(qualifier.Value)) { return(index); } } } return(-1); }
/// <summary> /// Performs a <b>deep clone</b> of the complete subtree (children and /// qualifier )into and add it to the destination node. /// </summary> /// <param name="destination"> the node to add the cloned subtree </param> public virtual void CloneSubtree(XmpNode destination) { try { foreach (XmpNode node in Children) { destination.AddChild((XmpNode)node.Clone()); } foreach (XmpNode node in Qualifier) { destination.AddQualifier((XmpNode)node.Clone()); } } catch (XmpException) { // cannot happen (duplicate childs/quals do not exist in this node) Debug.Assert(false); } }
/// <summary> /// Moves an alias node of array form to another schema into an array </summary> /// <param name="childNode"> the node to be moved </param> /// <param name="baseArray"> the base array for the array item </param> /// <exception cref="XmpException"> Forwards XMP errors </exception> private static void TransplantArrayItemAlias(XmpNode childNode, XmpNode baseArray) { if (baseArray.Options.ArrayAltText) { if (childNode.Options.HasLanguage) { throw new XmpException("Alias to x-default already has a language qualifier", XmpError.BADXMP); } XmpNode langQual = new XmpNode(XmpConst.XML_LANG, XmpConst.X_DEFAULT, null); childNode.AddQualifier(langQual); } childNode.Name = XmpConst.ARRAY_ITEM_NAME; baseArray.AddChild(childNode); }
/// <summary> /// Undo the denormalization performed by the XMP used in Acrobat 5.<br> /// If a Dublin Core array had only one item, it was serialized as a simple /// property. <br> /// The <code>xml:lang</code> attribute was dropped from an /// <code>alt-text</code> item if the language was <code>x-default</code>. /// </summary> /// <param name="dcSchema"> the DC schema node </param> /// <exception cref="XmpException"> Thrown if normalization fails </exception> private static void NormalizeDcArrays(XmpNode dcSchema) { for (int i = 1; i <= dcSchema.ChildrenLength; i++) { XmpNode currProp = dcSchema.GetChild(i); PropertyOptions arrayForm = (PropertyOptions)_dcArrayForms[currProp.Name]; if (arrayForm == null) { continue; } if (currProp.Options.Simple) { // create a new array and add the current property as child, // if it was formerly simple XmpNode newArray = new XmpNode(currProp.Name, arrayForm); currProp.Name = XmpConst.ARRAY_ITEM_NAME; newArray.AddChild(currProp); dcSchema.ReplaceChild(i, newArray); // fix language alternatives if (arrayForm.ArrayAltText && !currProp.Options.HasLanguage) { XmpNode newLang = new XmpNode(XmpConst.XML_LANG, XmpConst.X_DEFAULT, null); currProp.AddQualifier(newLang); } } else { // clear array options and add corrected array form if it has been an array before currProp.Options.SetOption( PropertyOptions.ARRAY | PropertyOptions.ARRAY_ORDERED | PropertyOptions.ARRAY_ALTERNATE | PropertyOptions.ARRAY_ALT_TEXT, false); currProp.Options.MergeWith(arrayForm); if (arrayForm.ArrayAltText) { // applying for "dc:description", "dc:rights", "dc:title" RepairAltText(currProp); } } } }
/// <summary> /// Performs a <b>deep clone</b> of the complete subtree (children and /// qualifier )into and add it to the destination node. /// </summary> /// <param name="destination"> the node to add the cloned subtree </param> public virtual void CloneSubtree(XmpNode destination) { try { foreach (XmpNode node in Children) { destination.AddChild((XmpNode) node.Clone()); } foreach (XmpNode node in Qualifier) { destination.AddQualifier((XmpNode) node.Clone()); } } catch (XmpException) { // cannot happen (duplicate childs/quals do not exist in this node) Debug.Assert(false); } }
/// <summary> /// The parent is an RDF pseudo-struct containing an rdf:value field. Fix the /// XMP data model. The rdf:value node must be the first child, the other /// children are qualifiers. The form, value, and children of the rdf:value /// node are the real ones. The rdf:value node's qualifiers must be added to /// the others. /// </summary> /// <param name="xmpParent"> the parent xmp node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void FixupQualifiedNode(XmpNode xmpParent) { Debug.Assert(xmpParent.Options.Struct && xmpParent.HasChildren()); XmpNode valueNode = xmpParent.GetChild(1); Debug.Assert("rdf:value".Equals(valueNode.Name)); // Move the qualifiers on the value node to the parent. // Make sure an xml:lang qualifier stays at the front. // Check for duplicate names between the value node's qualifiers and the parent's children. // The parent's children are about to become qualifiers. Check here, between the groups. // Intra-group duplicates are caught by XMPNode#addChild(...). if (valueNode.Options.HasLanguage) { if (xmpParent.Options.HasLanguage) { throw new XmpException("Redundant xml:lang for rdf:value element", XmpError.BADXMP); } XmpNode langQual = valueNode.GetQualifier(1); valueNode.RemoveQualifier(langQual); xmpParent.AddQualifier(langQual); } // Start the remaining copy after the xml:lang qualifier. for (int i = 1; i <= valueNode.QualifierLength; i++) { XmpNode qualifier = valueNode.GetQualifier(i); xmpParent.AddQualifier(qualifier); } // Change the parent's other children into qualifiers. // This loop starts at 1, child 0 is the rdf:value node. for (int i = 2; i <= xmpParent.ChildrenLength; i++) { XmpNode qualifier = xmpParent.GetChild(i); xmpParent.AddQualifier(qualifier); } // Move the options and value last, other checks need the parent's original options. // Move the value node's children to be the parent's children. Debug.Assert(xmpParent.Options.Struct || xmpParent.HasValueChild); xmpParent.HasValueChild = false; xmpParent.Options.Struct = false; xmpParent.Options.MergeWith(valueNode.Options); xmpParent.Value = valueNode.Value; xmpParent.RemoveChildren(); for (IEnumerator it = valueNode.IterateChildren(); it.MoveNext();) { XmpNode child = (XmpNode) it.Current; xmpParent.AddChild(child); } }
/// <summary> /// Appends a language item to an alt text array. /// </summary> /// <param name="arrayNode"> the language array </param> /// <param name="itemLang"> the language of the item </param> /// <param name="itemValue"> the content of the item </param> /// <exception cref="XmpException"> Thrown if a duplicate property is added </exception> internal static void AppendLangItem(XmpNode arrayNode, string itemLang, string itemValue) { XmpNode newItem = new XmpNode(ARRAY_ITEM_NAME, itemValue, null); XmpNode langQual = new XmpNode(XML_LANG, itemLang, null); newItem.AddQualifier(langQual); if (!X_DEFAULT.Equals(langQual.Value)) { arrayNode.AddChild(newItem); } else { arrayNode.AddChild(1, newItem); } }
/// <seealso cref= XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean) </seealso> /// <param name="destXmp"> The destination XMP object. </param> /// <param name="sourceNode"> the source node </param> /// <param name="destParent"> the parent of the destination node </param> /// <param name="replaceOldValues"> Replace the values of existing properties. </param> /// <param name="deleteEmptyValues"> flag if properties with empty values should be deleted /// in the destination object. </param> /// <exception cref="XmpException"> </exception> private static void AppendSubtree(XmpMetaImpl destXmp, XmpNode sourceNode, XmpNode destParent, bool replaceOldValues, bool deleteEmptyValues) { XmpNode destNode = XmpNodeUtils.FindChildNode(destParent, sourceNode.Name, false); bool valueIsEmpty = false; if (deleteEmptyValues) { valueIsEmpty = sourceNode.Options.Simple ? string.IsNullOrEmpty(sourceNode.Value) : !sourceNode.HasChildren(); } if (deleteEmptyValues && valueIsEmpty) { if (destNode != null) { destParent.RemoveChild(destNode); } } else if (destNode == null) { // The one easy case, the destination does not exist. destParent.AddChild((XmpNode) sourceNode.Clone()); } else if (replaceOldValues) { // The destination exists and should be replaced. destXmp.SetNode(destNode, sourceNode.Value, sourceNode.Options, true); destParent.RemoveChild(destNode); destNode = (XmpNode) sourceNode.Clone(); destParent.AddChild(destNode); } else { // The destination exists and is not totally replaced. Structs and // arrays are merged. PropertyOptions sourceForm = sourceNode.Options; PropertyOptions destForm = destNode.Options; if (sourceForm != destForm) { return; } if (sourceForm.Struct) { // 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 (IEnumerator it = sourceNode.IterateChildren(); it.MoveNext();) { XmpNode sourceField = (XmpNode) it.Current; if (sourceField == null) continue; AppendSubtree(destXmp, sourceField, destNode, replaceOldValues, deleteEmptyValues); if (deleteEmptyValues && !destNode.HasChildren()) { destParent.RemoveChild(destNode); } } } else if (sourceForm.ArrayAltText) { // 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. for (IEnumerator it = sourceNode.IterateChildren(); it.MoveNext();) { XmpNode sourceItem = (XmpNode) it.Current; if (sourceItem == null) continue; if (!sourceItem.HasQualifier() || !XML_LANG.Equals(sourceItem.GetQualifier(1).Name)) { continue; } int destIndex = XmpNodeUtils.LookupLanguageItem(destNode, sourceItem.GetQualifier(1).Value); if (deleteEmptyValues && (string.IsNullOrEmpty(sourceItem.Value))) { if (destIndex != -1) { destNode.RemoveChild(destIndex); if (!destNode.HasChildren()) { destParent.RemoveChild(destNode); } } } else if (destIndex == -1) { // Not replacing, keep the existing item. if (!X_DEFAULT.Equals(sourceItem.GetQualifier(1).Value) || !destNode.HasChildren()) { sourceItem.CloneSubtree(destNode); } else { XmpNode destItem = new XmpNode(sourceItem.Name, sourceItem.Value, sourceItem.Options); sourceItem.CloneSubtree(destItem); destNode.AddChild(1, destItem); } } } } else if (sourceForm.Array) { // 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 (IEnumerator @is = sourceNode.IterateChildren(); @is.MoveNext();) { XmpNode sourceItem = (XmpNode) @is.Current; if (sourceItem == null) continue; bool match = false; for (IEnumerator id = destNode.IterateChildren(); id.MoveNext();) { XmpNode destItem = (XmpNode) id.Current; if (destItem == null) continue; if (ItemValuesMatch(sourceItem, destItem)) { match = true; } } if (!match) { destNode = (XmpNode) sourceItem.Clone(); destParent.AddChild(destNode); } } } } }
/// <summary> /// Searches for a qualifier selector in a node: /// [?qualName="value"] - an element in an array, chosen by a qualifier value. /// No implicit nodes are created for qualifier selectors, /// except for an alias to an x-default item. /// </summary> /// <param name="arrayNode"> an array node </param> /// <param name="qualName"> the qualifier name </param> /// <param name="qualValue"> the qualifier value </param> /// <param name="aliasForm"> in case the qual selector results from an alias, /// an x-default node is created if there has not been one. </param> /// <returns> Returns the index of th </returns> /// <exception cref="XmpException"> </exception> private static int LookupQualSelector(XmpNode arrayNode, string qualName, string qualValue, uint aliasForm) { if (XML_LANG.Equals(qualName)) { qualValue = Utils.NormalizeLangValue(qualValue); int index = LookupLanguageItem(arrayNode, qualValue); if (index < 0 && (aliasForm & AliasOptions.PROP_ARRAY_ALT_TEXT) > 0) { XmpNode langNode = new XmpNode(ARRAY_ITEM_NAME, null); XmpNode xdefault = new XmpNode(XML_LANG, X_DEFAULT, null); langNode.AddQualifier(xdefault); arrayNode.AddChild(1, langNode); return 1; } return index; } for (int index = 1; index < arrayNode.ChildrenLength; index++) { XmpNode currItem = arrayNode.GetChild(index); for (IEnumerator it = currItem.IterateQualifier(); it.MoveNext();) { XmpNode qualifier = (XmpNode) it.Current; if (qualifier != null && qualName.Equals(qualifier.Name) && qualValue.Equals(qualifier.Value)) { return index; } } } return -1; }
/// <summary> /// Make sure the x-default item is first. Touch up "single value" /// arrays that have a default plus one real language. This case should have /// the same value for both items. Older Adobe apps were hardwired to only /// use the "x-default" item, so we copy that value to the other /// item. /// </summary> /// <param name="arrayNode"> /// an alt text array node </param> internal static void NormalizeLangArray(XmpNode arrayNode) { if (!arrayNode.Options.ArrayAltText) { return; } // check if node with x-default qual is first place for (int i = 2; i <= arrayNode.ChildrenLength; i++) { XmpNode child = arrayNode.GetChild(i); if (child.HasQualifier() && X_DEFAULT.Equals(child.GetQualifier(1).Value)) { // move node to first place try { arrayNode.RemoveChild(i); arrayNode.AddChild(1, child); } catch (XmpException) { // cannot occur, because same child is removed before Debug.Assert(false); } if (i == 2) { arrayNode.GetChild(2).Value = child.Value; } break; } } }
/// <summary> /// Find or create a child node under a given parent node. If the parent node is no /// Returns the found or created child node. /// </summary> /// <param name="parent"> /// the parent node </param> /// <param name="childName"> /// the node name to find </param> /// <param name="createNodes"> /// flag, if new nodes shall be created. </param> /// <returns> Returns the found or created node or <code>null</code>. </returns> /// <exception cref="XmpException"> Thrown if </exception> internal static XmpNode FindChildNode(XmpNode parent, string childName, bool createNodes) { if (!parent.Options.SchemaNode && !parent.Options.Struct) { if (!parent.Implicit) { throw new XmpException("Named children only allowed for schemas and structs", XmpError.BADXPATH); } if (parent.Options.Array) { throw new XmpException("Named children not allowed for arrays", XmpError.BADXPATH); } if (createNodes) { parent.Options.Struct = true; } } XmpNode childNode = parent.FindChildByName(childName); if (childNode == null && createNodes) { PropertyOptions options = new PropertyOptions(); childNode = new XmpNode(childName, options); childNode.Implicit = true; parent.AddChild(childNode); } Debug.Assert(childNode != null || !createNodes); return childNode; }
/// <param name="arrayNode"> an array node </param> /// <param name="segment"> the segment containing the array index </param> /// <param name="createNodes"> flag if new nodes are allowed to be created. </param> /// <returns> Returns the index or index = -1 if not found </returns> /// <exception cref="XmpException"> Throws Exceptions </exception> private static int FindIndexedItem(XmpNode arrayNode, string segment, bool createNodes) { int index; try { segment = segment.Substring(1, segment.Length - 1 - 1); index = Convert.ToInt32(segment); if (index < 1) { throw new XmpException("Array index must be larger than zero", XmpError.BADXPATH); } } catch (FormatException) { throw new XmpException("Array index not digits.", XmpError.BADXPATH); } if (createNodes && index == arrayNode.ChildrenLength + 1) { // Append a new last + 1 node. XmpNode newItem = new XmpNode(ARRAY_ITEM_NAME, null); newItem.Implicit = true; arrayNode.AddChild(newItem); } return index; }
/// <summary> /// Find or create a schema node if <code>createNodes</code> is true. /// </summary> /// <param name="tree"> the root of the xmp tree. </param> /// <param name="namespaceUri"> a namespace </param> /// <param name="suggestedPrefix"> If a prefix is suggested, the namespace is allowed to be registered. </param> /// <param name="createNodes"> a flag indicating if the node shall be created if not found. /// <em>Note:</em> The namespace must be registered prior to this call. /// </param> /// <returns> Returns the schema node if found, <code>null</code> otherwise. /// Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b> /// returned a valid node. </returns> /// <exception cref="XmpException"> An exception is only thrown if an error occurred, not if a /// node was not found. </exception> internal static XmpNode FindSchemaNode(XmpNode tree, string namespaceUri, string suggestedPrefix, bool createNodes) { Debug.Assert(tree.Parent == null); // make sure that its the root XmpNode schemaNode = tree.FindChildByName(namespaceUri); if (schemaNode == null && createNodes) { PropertyOptions propertyOptions = new PropertyOptions(); propertyOptions.SchemaNode = true; schemaNode = new XmpNode(namespaceUri, propertyOptions); schemaNode.Implicit = true; // only previously registered schema namespaces are allowed in the XMP tree. string prefix = XMPMetaFactory.SchemaRegistry.GetNamespacePrefix(namespaceUri); if (prefix == null) { if (!String.IsNullOrEmpty(suggestedPrefix)) { prefix = XMPMetaFactory.SchemaRegistry.RegisterNamespace(namespaceUri, suggestedPrefix); } else { throw new XmpException("Unregistered schema namespace URI", XmpError.BADSCHEMA); } } schemaNode.Value = prefix; tree.AddChild(schemaNode); } return schemaNode; }
/// <summary> /// Visit all of the top level nodes looking for aliases. If there is /// no base, transplant the alias subtree. If there is a base and strict /// aliasing is on, make sure the alias and base subtrees match. /// </summary> /// <param name="tree"> the root of the metadata tree </param> /// <param name="options"> th parsing options </param> /// <exception cref="XmpException"> Forwards XMP errors </exception> private static void MoveExplicitAliases(XmpNode tree, ParseOptions options) { if (!tree.HasAliases) { return; } tree.HasAliases = false; bool strictAliasing = options.StrictAliasing; IEnumerator schemaIt = tree.UnmodifiableChildren.GetEnumerator(); while (schemaIt.MoveNext()) { XmpNode currSchema = (XmpNode)schemaIt.Current; if (currSchema == null) { continue; } if (!currSchema.HasAliases) { continue; } ArrayList currPropsToRemove = new ArrayList(); IEnumerator propertyIt = currSchema.IterateChildren(); while (propertyIt.MoveNext()) { XmpNode currProp = (XmpNode)propertyIt.Current; if (currProp == null) { continue; } if (!currProp.Alias) { continue; } currProp.Alias = false; // Find the base path, look for the base schema and root node. XMPAliasInfo info = XMPMetaFactory.SchemaRegistry.FindAlias(currProp.Name); if (info != null) { // find or create schema XmpNode baseSchema = XmpNodeUtils.FindSchemaNode(tree, info.Namespace, null, true); baseSchema.Implicit = false; XmpNode baseNode = XmpNodeUtils.FindChildNode(baseSchema, info.Prefix + info.PropName, false); if (baseNode == null) { if (info.AliasForm.Simple) { // A top-to-top alias, transplant the property. // change the alias property name to the base name string qname = info.Prefix + info.PropName; currProp.Name = qname; baseSchema.AddChild(currProp); } else { // An alias to an array item, // create the array and transplant the property. baseNode = new XmpNode(info.Prefix + info.PropName, info.AliasForm.ToPropertyOptions()); baseSchema.AddChild(baseNode); TransplantArrayItemAlias(currProp, baseNode); } currPropsToRemove.Add(currProp); } else if (info.AliasForm.Simple) { // The base node does exist and this is a top-to-top alias. // Check for conflicts if strict aliasing is on. // Remove and delete the alias subtree. if (strictAliasing) { CompareAliasedSubtrees(currProp, baseNode, true); } currPropsToRemove.Add(currProp); } else { // This is an alias to an array item and the array exists. // Look for the aliased item. // Then transplant or check & delete as appropriate. XmpNode itemNode = null; if (info.AliasForm.ArrayAltText) { int xdIndex = XmpNodeUtils.LookupLanguageItem(baseNode, XmpConst.X_DEFAULT); if (xdIndex != -1) { itemNode = baseNode.GetChild(xdIndex); } } else if (baseNode.HasChildren()) { itemNode = baseNode.GetChild(1); } if (itemNode == null) { TransplantArrayItemAlias(currProp, baseNode); } else { if (strictAliasing) { CompareAliasedSubtrees(currProp, itemNode, true); } } currPropsToRemove.Add(currProp); } } } foreach (object o in currPropsToRemove) { currSchema.Children.Remove(o); } currPropsToRemove.Clear(); currSchema.HasAliases = false; } }
/// <summary> /// see {@link XMPUtils#separateArrayItems(XMPMeta, String, String, String, /// PropertyOptions, boolean)} /// </summary> /// <param name="xmp"> /// The XMP object containing the array to be updated. </param> /// <param name="schemaNs"> /// The schema namespace URI for the array. Must not be null or /// the empty string. </param> /// <param name="arrayName"> /// The name of the array. May be a general path expression, must /// not be null or the empty string. Each item in the array must /// be a simple string value. </param> /// <param name="catedStr"> /// The string to be separated into the array items. </param> /// <param name="arrayOptions"> /// Option flags to control the separation. </param> /// <param name="preserveCommas"> /// Flag if commas shall be preserved /// </param> /// <exception cref="XmpException"> /// Forwards the Exceptions from the metadata processing </exception> public static void SeparateArrayItems(IXmpMeta xmp, string schemaNs, string arrayName, string catedStr, PropertyOptions arrayOptions, bool preserveCommas) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(arrayName); if (catedStr == null) { throw new XmpException("Parameter must not be null", XmpError.BADPARAM); } ParameterAsserts.AssertImplementation(xmp); XmpMetaImpl xmpImpl = (XmpMetaImpl)xmp; // Keep a zero value, has special meaning below. XmpNode arrayNode = SeparateFindCreateArray(schemaNs, arrayName, arrayOptions, xmpImpl); // Extract the item values one at a time, until the whole input string is done. int charKind = UCK_NORMAL; char ch = (char)0; int itemEnd = 0; int endPos = catedStr.Length; while (itemEnd < endPos) { string itemValue; int itemStart; // 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++) { ch = catedStr[itemStart]; charKind = ClassifyCharacter(ch); if (charKind == UCK_NORMAL || charKind == UCK_QUOTE) { break; } } if (itemStart >= endPos) { break; } int nextKind; 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++) { ch = catedStr[itemEnd]; charKind = ClassifyCharacter(ch); if (charKind == UCK_NORMAL || charKind == UCK_QUOTE || (charKind == UCK_COMMA && preserveCommas)) { continue; } if (charKind != UCK_SPACE) { break; } if ((itemEnd + 1) < endPos) { ch = catedStr[itemEnd + 1]; nextKind = ClassifyCharacter(ch); if (nextKind == UCK_NORMAL || nextKind == UCK_QUOTE || (nextKind == UCK_COMMA && preserveCommas)) { continue; } } // Anything left? break; // Have multiple spaces, or a space followed by a // separator. } itemValue = catedStr.Substring(itemStart, itemEnd - itemStart); } else { // Accumulate quoted values into a local string, undoubling // internal quotes that // match the surrounding quotes. Do not undouble "unmatching" // quotes. char openQuote = ch; char closeQuote = GetClosingQuote(openQuote); itemStart++; // Skip the opening quote; itemValue = ""; for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) { ch = catedStr[itemEnd]; charKind = ClassifyCharacter(ch); if (charKind != UCK_QUOTE || !IsSurroundingQuote(ch, openQuote, closeQuote)) { // This is not a matching quote, just append it to the // item value. itemValue += ch; } 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. char nextChar; if ((itemEnd + 1) < endPos) { nextChar = catedStr[itemEnd + 1]; nextKind = ClassifyCharacter(nextChar); } else { nextKind = UCK_SEMICOLON; nextChar = (char)0x3B; } if (ch == nextChar) { // This is doubled, copy it and skip the double. itemValue += ch; // Loop will add in charSize. itemEnd++; } else if (!IsClosingingQuote(ch, openQuote, closeQuote)) { // This is an undoubled, non-closing quote, copy it. itemValue += ch; } else { // This is an undoubled closing quote, skip it and // exit the loop. itemEnd++; break; } } } } // Add the separated item to the array. // Keep a matching old value in case it had separators. int foundIndex = -1; for (int oldChild = 1; oldChild <= arrayNode.ChildrenLength; oldChild++) { if (itemValue.Equals(arrayNode.GetChild(oldChild).Value)) { foundIndex = oldChild; break; } } if (foundIndex < 0) { XmpNode newItem = new XmpNode(ARRAY_ITEM_NAME, itemValue, null); arrayNode.AddChild(newItem); } } }
/// <seealso cref= XMPMeta#setLocalizedText(String, String, String, String, String, /// PropertyOptions) </seealso> public virtual void SetLocalizedText(string schemaNs, string altTextName, string genericLang, string specificLang, string itemValue, PropertyOptions options) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(altTextName); ParameterAsserts.AssertSpecificLang(specificLang); genericLang = genericLang != null?Utils.NormalizeLangValue(genericLang) : null; specificLang = Utils.NormalizeLangValue(specificLang); XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, altTextName); // Find the array node and set the options if it was just created. XmpNode arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, true, new PropertyOptions(PropertyOptions.ARRAY | PropertyOptions.ARRAY_ORDERED | PropertyOptions.ARRAY_ALTERNATE | PropertyOptions.ARRAY_ALT_TEXT)); if (arrayNode == null) { throw new XmpException("Failed to find or create array node", XmpError.BADXPATH); } if (!arrayNode.Options.ArrayAltText) { if (!arrayNode.HasChildren() && arrayNode.Options.ArrayAlternate) { arrayNode.Options.ArrayAltText = true; } else { throw new XmpException("Specified property is no alt-text array", XmpError.BADXPATH); } } // Make sure the x-default item, if any, is first. bool haveXDefault = false; XmpNode xdItem = null; foreach (XmpNode currItem in arrayNode.Children) { if (!currItem.HasQualifier() || !XML_LANG.Equals(currItem.GetQualifier(1).Name)) { throw new XmpException("Language qualifier must be first", XmpError.BADXPATH); } if (X_DEFAULT.Equals(currItem.GetQualifier(1).Value)) { xdItem = currItem; haveXDefault = true; break; } } // Moves x-default to the beginning of the array if (xdItem != null && arrayNode.ChildrenLength > 1) { arrayNode.RemoveChild(xdItem); arrayNode.AddChild(1, xdItem); } // Find the appropriate item. // chooseLocalizedText will make sure the array is a language // alternative. object[] result = XmpNodeUtils.ChooseLocalizedText(arrayNode, genericLang, specificLang); int match = (int)((int?)result[0]); XmpNode itemNode = (XmpNode)result[1]; bool specificXDefault = X_DEFAULT.Equals(specificLang); switch (match) { case XmpNodeUtils.CLT_NO_VALUES: // Create the array items for the specificLang and x-default, with // x-default first. XmpNodeUtils.AppendLangItem(arrayNode, X_DEFAULT, itemValue); haveXDefault = true; if (!specificXDefault) { XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); } break; case XmpNodeUtils.CLT_SPECIFIC_MATCH: if (!specificXDefault) { // Update the specific item, update x-default if it matches the // old value. if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.Value.Equals(itemNode.Value)) { xdItem.Value = itemValue; } // ! Do this after the x-default check! itemNode.Value = itemValue; } else { // Update all items whose values match the old x-default value. Debug.Assert(haveXDefault && xdItem == itemNode); foreach (XmpNode currItem in arrayNode.Children) { if (currItem == xdItem || !currItem.Value.Equals(xdItem != null ? xdItem.Value : null)) { continue; } currItem.Value = itemValue; } // And finally do the x-default item. if (xdItem != null) { xdItem.Value = itemValue; } } break; case XmpNodeUtils.CLT_SINGLE_GENERIC: // Update the generic item, update x-default if it matches the old // value. if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.Value.Equals(itemNode.Value)) { xdItem.Value = itemValue; } itemNode.Value = itemValue; // ! Do this after // the x-default // check! break; case XmpNodeUtils.CLT_MULTIPLE_GENERIC: // Create the specific language, ignore x-default. XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); if (specificXDefault) { haveXDefault = true; } break; case XmpNodeUtils.CLT_XDEFAULT: // Create the specific language, update x-default if it was the only // item. if (xdItem != null && arrayNode.ChildrenLength == 1) { xdItem.Value = itemValue; } XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); break; case XmpNodeUtils.CLT_FIRST_ITEM: // Create the specific language, don't add an x-default item. XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); if (specificXDefault) { haveXDefault = true; } break; default: // does not happen under normal circumstances throw new XmpException("Unexpected result from ChooseLocalizedText", XmpError.INTERNALFAILURE); } // Add an x-default at the front if needed. if (!haveXDefault && arrayNode.ChildrenLength == 1) { XmpNodeUtils.AppendLangItem(arrayNode, X_DEFAULT, itemValue); } }
/// <seealso cref= XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean) </seealso> /// <param name="destXmp"> The destination XMP object. </param> /// <param name="sourceNode"> the source node </param> /// <param name="destParent"> the parent of the destination node </param> /// <param name="replaceOldValues"> Replace the values of existing properties. </param> /// <param name="deleteEmptyValues"> flag if properties with empty values should be deleted /// in the destination object. </param> /// <exception cref="XmpException"> </exception> private static void AppendSubtree(XmpMetaImpl destXmp, XmpNode sourceNode, XmpNode destParent, bool replaceOldValues, bool deleteEmptyValues) { XmpNode destNode = XmpNodeUtils.FindChildNode(destParent, sourceNode.Name, false); bool valueIsEmpty = false; if (deleteEmptyValues) { valueIsEmpty = sourceNode.Options.Simple ? string.IsNullOrEmpty(sourceNode.Value) : !sourceNode.HasChildren(); } if (deleteEmptyValues && valueIsEmpty) { if (destNode != null) { destParent.RemoveChild(destNode); } } else if (destNode == null) { // The one easy case, the destination does not exist. destParent.AddChild((XmpNode)sourceNode.Clone()); } else if (replaceOldValues) { // The destination exists and should be replaced. destXmp.SetNode(destNode, sourceNode.Value, sourceNode.Options, true); destParent.RemoveChild(destNode); destNode = (XmpNode)sourceNode.Clone(); destParent.AddChild(destNode); } else { // The destination exists and is not totally replaced. Structs and // arrays are merged. PropertyOptions sourceForm = sourceNode.Options; PropertyOptions destForm = destNode.Options; if (sourceForm != destForm) { return; } if (sourceForm.Struct) { // 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 (IEnumerator it = sourceNode.IterateChildren(); it.MoveNext();) { XmpNode sourceField = (XmpNode)it.Current; if (sourceField == null) { continue; } AppendSubtree(destXmp, sourceField, destNode, replaceOldValues, deleteEmptyValues); if (deleteEmptyValues && !destNode.HasChildren()) { destParent.RemoveChild(destNode); } } } else if (sourceForm.ArrayAltText) { // 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. for (IEnumerator it = sourceNode.IterateChildren(); it.MoveNext();) { XmpNode sourceItem = (XmpNode)it.Current; if (sourceItem == null) { continue; } if (!sourceItem.HasQualifier() || !XML_LANG.Equals(sourceItem.GetQualifier(1).Name)) { continue; } int destIndex = XmpNodeUtils.LookupLanguageItem(destNode, sourceItem.GetQualifier(1).Value); if (deleteEmptyValues && (string.IsNullOrEmpty(sourceItem.Value))) { if (destIndex != -1) { destNode.RemoveChild(destIndex); if (!destNode.HasChildren()) { destParent.RemoveChild(destNode); } } } else if (destIndex == -1) { // Not replacing, keep the existing item. if (!X_DEFAULT.Equals(sourceItem.GetQualifier(1).Value) || !destNode.HasChildren()) { sourceItem.CloneSubtree(destNode); } else { XmpNode destItem = new XmpNode(sourceItem.Name, sourceItem.Value, sourceItem.Options); sourceItem.CloneSubtree(destItem); destNode.AddChild(1, destItem); } } } } else if (sourceForm.Array) { // 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 (IEnumerator @is = sourceNode.IterateChildren(); @is.MoveNext();) { XmpNode sourceItem = (XmpNode)@is.Current; if (sourceItem == null) { continue; } bool match = false; for (IEnumerator id = destNode.IterateChildren(); id.MoveNext();) { XmpNode destItem = (XmpNode)id.Current; if (destItem == null) { continue; } if (ItemValuesMatch(sourceItem, destItem)) { match = true; } } if (!match) { destNode = (XmpNode)sourceItem.Clone(); destParent.AddChild(destNode); } } } } }
/// <summary> /// Adds a child node. /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="value"> Node value </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <returns> Returns the newly created child node. </returns> /// <exception cref="XmpException"> thown on parsing errors </exception> private static XmpNode AddChildNode(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, string value, bool isTopLevel) { IXmpSchemaRegistry registry = XmpMetaFactory.SchemaRegistry; string @namespace = xmlNode.NamespaceURI; string childName; if (@namespace != null) { if (NS_DC_DEPRECATED.Equals(@namespace)) { // Fix a legacy DC namespace @namespace = NS_DC; } string prefix = registry.GetNamespacePrefix(@namespace); if (prefix == null) { prefix = xmlNode.Prefix ?? DEFAULT_PREFIX; prefix = registry.RegisterNamespace(@namespace, prefix); } childName = prefix + xmlNode.LocalName; } else { throw new XmpException("XML namespace required for all elements and attributes", XmpError.BADRDF); } // create schema node if not already there PropertyOptions childOptions = new PropertyOptions(); bool isAlias = false; if (isTopLevel) { // Lookup the schema node, adjust the XMP parent pointer. // Incoming parent must be the tree root. XmpNode schemaNode = XmpNodeUtils.FindSchemaNode(xmp.Root, @namespace, DEFAULT_PREFIX, true); schemaNode.Implicit = false; // Clear the implicit node bit. // need runtime check for proper 32 bit code. xmpParent = schemaNode; // If this is an alias set the alias flag in the node // and the hasAliases flag in the tree. if (registry.FindAlias(childName) != null) { isAlias = true; xmp.Root.HasAliases = true; schemaNode.HasAliases = true; } } // Make sure that this is not a duplicate of a named node. bool isArrayItem = "rdf:li".Equals(childName); bool isValueNode = "rdf:value".Equals(childName); // Create XMP node and so some checks XmpNode newChild = new XmpNode(childName, value, childOptions); newChild.Alias = isAlias; // Add the new child to the XMP parent node, a value node first. if (!isValueNode) { xmpParent.AddChild(newChild); } else { xmpParent.AddChild(1, newChild); } if (isValueNode) { if (isTopLevel || !xmpParent.Options.Struct) { throw new XmpException("Misplaced rdf:value element", XmpError.BADRDF); } xmpParent.HasValueChild = true; } if (isArrayItem) { if (!xmpParent.Options.Array) { throw new XmpException("Misplaced rdf:li element", XmpError.BADRDF); } newChild.Name = ARRAY_ITEM_NAME; } return newChild; }
/// <summary> /// Undo the denormalization performed by the XMP used in Acrobat 5.<br> /// If a Dublin Core array had only one item, it was serialized as a simple /// property. <br> /// The <code>xml:lang</code> attribute was dropped from an /// <code>alt-text</code> item if the language was <code>x-default</code>. /// </summary> /// <param name="dcSchema"> the DC schema node </param> /// <exception cref="XmpException"> Thrown if normalization fails </exception> private static void NormalizeDcArrays(XmpNode dcSchema) { for (int i = 1; i <= dcSchema.ChildrenLength; i++) { XmpNode currProp = dcSchema.GetChild(i); PropertyOptions arrayForm = (PropertyOptions) _dcArrayForms[currProp.Name]; if (arrayForm == null) { continue; } if (currProp.Options.Simple) { // create a new array and add the current property as child, // if it was formerly simple XmpNode newArray = new XmpNode(currProp.Name, arrayForm); currProp.Name = XmpConst.ARRAY_ITEM_NAME; newArray.AddChild(currProp); dcSchema.ReplaceChild(i, newArray); // fix language alternatives if (arrayForm.ArrayAltText && !currProp.Options.HasLanguage) { XmpNode newLang = new XmpNode(XmpConst.XML_LANG, XmpConst.X_DEFAULT, null); currProp.AddQualifier(newLang); } } else { // clear array options and add corrected array form if it has been an array before currProp.Options.SetOption( PropertyOptions.ARRAY | PropertyOptions.ARRAY_ORDERED | PropertyOptions.ARRAY_ALTERNATE | PropertyOptions.ARRAY_ALT_TEXT, false); currProp.Options.MergeWith(arrayForm); if (arrayForm.ArrayAltText) { // applying for "dc:description", "dc:rights", "dc:title" RepairAltText(currProp); } } } }
// ------------------------------------------------------------------------------------- // private /// <summary> /// Locate or create the item node and set the value. Note the index /// parameter is one-based! The index can be in the range [1..size + 1] or /// "last()", normalize it and check the insert flags. The order of the /// normalization checks is important. If the array is empty we end up with /// an index and location to set item size + 1. /// </summary> /// <param name="arrayNode"> an array node </param> /// <param name="itemIndex"> the index where to insert the item </param> /// <param name="itemValue"> the item value </param> /// <param name="itemOptions"> the options for the new item </param> /// <param name="insert"> insert oder overwrite at index position? </param> /// <exception cref="XmpException"> </exception> private void DoSetArrayItem(XmpNode arrayNode, int itemIndex, string itemValue, PropertyOptions itemOptions, bool insert) { XmpNode itemNode = new XmpNode(ARRAY_ITEM_NAME, null); itemOptions = XmpNodeUtils.VerifySetOptions(itemOptions, itemValue); // in insert mode the index after the last is allowed, // even ARRAY_LAST_ITEM points to the index *after* the last. int maxIndex = insert ? arrayNode.ChildrenLength + 1 : arrayNode.ChildrenLength; if (itemIndex == ARRAY_LAST_ITEM) { itemIndex = maxIndex; } if (1 <= itemIndex && itemIndex <= maxIndex) { if (!insert) { arrayNode.RemoveChild(itemIndex); } arrayNode.AddChild(itemIndex, itemNode); SetNode(itemNode, itemValue, itemOptions, false); } else { throw new XmpException("Array index out of bounds", XmpError.BADINDEX); } }
/// <summary> /// Moves an alias node of array form to another schema into an array </summary> /// <param name="childNode"> the node to be moved </param> /// <param name="baseArray"> the base array for the array item </param> /// <exception cref="XmpException"> Forwards XMP errors </exception> private static void TransplantArrayItemAlias(XmpNode childNode, XmpNode baseArray) { if (baseArray.Options.ArrayAltText) { if (childNode.Options.HasLanguage) { throw new XmpException("Alias to x-default already has a language qualifier", XmpError.BADXMP); } XmpNode langQual = new XmpNode(XmpConst.XML_LANG, XmpConst.X_DEFAULT, null); childNode.AddQualifier(langQual); } childNode.Name = XmpConst.ARRAY_ITEM_NAME; baseArray.AddChild(childNode); }