/// <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) { var newItem = new XmpNode(XmpConstants.ArrayItemName, itemValue, null); var langQual = new XmpNode(XmpConstants.XmlLang, itemLang, null); newItem.AddQualifier(langQual); if (!XmpConstants.XDefault.Equals(langQual.Value)) { arrayNode.AddChild(newItem); } else { arrayNode.AddChild(1, newItem); } }
/// <summary>Find or create a schema node if <c>createNodes</c> 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, <c>null</c> otherwise. /// Note: If <c>createNodes</c> is <c>true</c>, 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 var schemaNode = tree.FindChildByName(namespaceUri); if (schemaNode == null && createNodes) { var po = new PropertyOptions { IsSchemaNode = true }; schemaNode = new XmpNode(namespaceUri, po) { IsImplicit = true }; // only previously registered schema namespaces are allowed in the XMP tree. var 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", XmpErrorCode.BadSchema); } } schemaNode.Value = prefix; tree.AddChild(schemaNode); } return(schemaNode); }
/// <summary>Find or create a child node under a given parent node.</summary> /// <remarks> /// Find or create a child node under a given parent node. If the parent node is no /// Returns the found or created child node. /// </remarks> /// <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 <c>null</c>.</returns> /// <exception cref="XmpException">Thrown if</exception> internal static XmpNode FindChildNode(XmpNode parent, string childName, bool createNodes) { if (!parent.Options.IsSchemaNode && !parent.Options.IsStruct) { if (!parent.IsImplicit) { throw new XmpException("Named children only allowed for schemas and structs", XmpErrorCode.BadXPath); } if (parent.Options.IsArray) { throw new XmpException("Named children not allowed for arrays", XmpErrorCode.BadXPath); } if (createNodes) { parent.Options.IsStruct = true; } } var childNode = parent.FindChildByName(childName); if (childNode == null && createNodes) { var options = new PropertyOptions(); childNode = new XmpNode(childName, options) { IsImplicit = true }; parent.AddChild(childNode); } Debug.Assert(childNode != null || !createNodes); return(childNode); }
/// <summary>Locate or create the item node and set the value.</summary> /// <remarks> /// 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. /// </remarks> /// <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"/> private void DoSetArrayItem(XmpNode arrayNode, int itemIndex, string itemValue, PropertyOptions itemOptions, bool insert) { var itemNode = new XmpNode(XmpConstants.ArrayItemName, 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. var maxIndex = insert ? arrayNode.GetChildrenLength() + 1 : arrayNode.GetChildrenLength(); if (itemIndex == XmpConstants.ArrayLastItem) { 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", XmpErrorCode.BadIndex); } }
/// <summary>Make sure the x-default item is first.</summary> /// <remarks> /// 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. /// </remarks> /// <param name="arrayNode">an alt text array node</param> internal static void NormalizeLangArray(XmpNode arrayNode) { if (!arrayNode.Options.IsArrayAltText) { return; } // check if node with x-default qual is first place for (var i = 2; i <= arrayNode.GetChildrenLength(); i++) { var child = arrayNode.GetChild(i); if (child.HasQualifier && XmpConstants.XDefault.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. /// </summary> /// <remarks> /// 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. /// </remarks> /// <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, int aliasForm) { if (XmpConstants.XmlLang.Equals(qualName)) { qualValue = Utils.NormalizeLangValue(qualValue); var index = LookupLanguageItem(arrayNode, qualValue); if (index < 0 && (aliasForm & AliasOptions.PropArrayAltText) > 0) { var langNode = new XmpNode(XmpConstants.ArrayItemName, null); var xdefault = new XmpNode(XmpConstants.XmlLang, XmpConstants.XDefault, null); langNode.AddQualifier(xdefault); arrayNode.AddChild(1, langNode); return(1); } return(index); } for (var index = 1; index < arrayNode.GetChildrenLength(); index++) { var currItem = arrayNode.GetChild(index); for (var it = currItem.IterateQualifier(); it.HasNext();) { var qualifier = (XmpNode)it.Next(); if (qualName.Equals(qualifier.Name) && qualValue.Equals(qualifier.Value)) { return(index); } } } return(-1); }
/// <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) { var index = 0; 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", XmpErrorCode.BadXPath); } } catch (FormatException) { throw new XmpException("Array index not digits.", XmpErrorCode.BadXPath); } if (createNodes && index == arrayNode.GetChildrenLength() + 1) { // Append a new last + 1 node. var newItem = new XmpNode(XmpConstants.ArrayItemName, null) { IsImplicit = true }; arrayNode.AddChild(newItem); } return(index); }
/// <summary>Moves an alias node of array form to another schema into an array</summary> /// <param name="propertyIt">the property iterator of the old schema (used to delete the property)</param> /// <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(IIterator propertyIt, XmpNode childNode, XmpNode baseArray) { if (baseArray.Options.IsArrayAltText) { if (childNode.Options.HasLanguage) { throw new XmpException("Alias to x-default already has a language qualifier", XmpErrorCode.BadXmp); } var langQual = new XmpNode(XmpConstants.XmlLang, XmpConstants.XDefault, null); childNode.AddQualifier(langQual); } propertyIt.Remove(); childNode.Name = XmpConstants.ArrayItemName; baseArray.AddChild(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) { if (!int.TryParse(segment.Substring(1, segment.Length - 1 - 1), out int index)) { throw new XmpException("Array index not digits.", XmpErrorCode.BadXPath); } if (createNodes && index == arrayNode.GetChildrenLength() + 1) { // Append a new last + 1 node. var newItem = new XmpNode(XmpConstants.ArrayItemName, null) { IsImplicit = true }; arrayNode.AddChild(newItem); } return(index); }
/// <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 void CloneSubtree(XmpNode destination) { try { for (var it = IterateChildren(); it.HasNext();) { var child = (XmpNode)it.Next(); destination.AddChild((XmpNode)child.Clone()); } for (var it1 = IterateQualifier(); it1.HasNext();) { var qualifier = (XmpNode)it1.Next(); destination.AddQualifier((XmpNode)qualifier.Clone()); } } catch (XmpException) { // cannot happen (duplicate childs/quals do not exist in this node) Debug.Assert(false); } }
/// <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. /// </summary> /// <remarks> /// 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 <c>xml:lang</c> attribute was dropped from an /// <c>alt-text</c> item if the language was <c>x-default</c>. /// </remarks> /// <param name="dcSchema">the DC schema node</param> /// <exception cref="XmpException">Thrown if normalization fails</exception> private static void NormalizeDcArrays(XmpNode dcSchema) { for (var i = 1; i <= dcSchema.GetChildrenLength(); i++) { var currProp = dcSchema.GetChild(i); var arrayForm = (PropertyOptions)_dcArrayForms[currProp.Name]; if (arrayForm == null) { continue; } if (currProp.Options.IsSimple) { // create a new array and add the current property as child, // if it was formerly simple var newArray = new XmpNode(currProp.Name, arrayForm); currProp.Name = XmpConstants.ArrayItemName; newArray.AddChild(currProp); dcSchema.ReplaceChild(i, newArray); // fix language alternatives if (arrayForm.IsArrayAltText && !currProp.Options.HasLanguage) { var newLang = new XmpNode(XmpConstants.XmlLang, XmpConstants.XDefault, null); currProp.AddQualifier(newLang); } } else { // clear array options and add corrected array form if it has been an array before currProp.Options.SetOption(PropertyOptions.ArrayFlag | PropertyOptions.ArrayOrderedFlag | PropertyOptions.ArrayAlternateFlag | PropertyOptions.ArrayAltTextFlag, false); currProp.Options.MergeWith(arrayForm); if (arrayForm.IsArrayAltText) { // applying for "dc:description", "dc:rights", "dc:title" RepairAltText(currProp); } } } }
/// <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"/> private static void AppendSubtree(XmpMeta destXmp, XmpNode sourceNode, XmpNode destParent, bool replaceOldValues, bool deleteEmptyValues) { var destNode = XmpNodeUtils.FindChildNode(destParent, sourceNode.Name, false); var valueIsEmpty = false; if (deleteEmptyValues) { valueIsEmpty = sourceNode.Options.IsSimple ? 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. var sourceForm = sourceNode.Options; var destForm = destNode.Options; if (sourceForm != destForm) { return; } if (sourceForm.IsStruct) { // 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 (var it = sourceNode.IterateChildren(); it.HasNext();) { var sourceField = (XmpNode)it.Next(); AppendSubtree(destXmp, sourceField, destNode, replaceOldValues, deleteEmptyValues); if (deleteEmptyValues && !destNode.HasChildren) { destParent.RemoveChild(destNode); } } } else if (sourceForm.IsArrayAltText) { // 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 (var it = sourceNode.IterateChildren(); it.HasNext();) { var sourceItem = (XmpNode)it.Next(); if (!sourceItem.HasQualifier || !XmpConstants.XmlLang.Equals(sourceItem.GetQualifier(1).Name)) { continue; } var 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 (!XmpConstants.XDefault.Equals(sourceItem.GetQualifier(1).Value) || !destNode.HasChildren) { sourceItem.CloneSubtree(destNode); } else { var destItem = new XmpNode(sourceItem.Name, sourceItem.Value, sourceItem.Options); sourceItem.CloneSubtree(destItem); destNode.AddChild(1, destItem); } } } } else if (sourceForm.IsArray) { // 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 (var children = sourceNode.IterateChildren(); children.HasNext();) { var sourceItem = (XmpNode)children.Next(); var match = false; for (var id = destNode.IterateChildren(); id.HasNext();) { var destItem = (XmpNode)id.Next(); if (ItemValuesMatch(sourceItem, destItem)) { match = true; } } if (!match) { destNode = (XmpNode)sourceItem.Clone(); destParent.AddChild(destNode); } } } } } } }