/// <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> /// After processing by ExpandXPath, a step can be of these forms: /// </summary> /// <remarks> /// After processing by ExpandXPath, a step can be of these forms: /// <list type="bullet"> /// <item>qualName - A top level property or struct field.</item> /// <item>[index] - An element of an array.</item> /// <item>[last()] - The last element of an array.</item> /// <item>[qualName="value"] - An element in an array of structs, chosen by a field value.</item> /// <item>[?qualName="value"] - An element in an array, chosen by a qualifier value.</item> /// <item>?qualName - A general qualifier.</item> /// </list> /// Find the appropriate child node, resolving aliases, and optionally creating nodes. /// </remarks> /// <param name="parentNode">the node to start to start from</param> /// <param name="nextStep">the xpath segment</param> /// <param name="createNodes"></param> /// <returns>returns the found or created XMPPath node</returns> /// <exception cref="XmpException"></exception> private static XmpNode FollowXPathStep(XmpNode parentNode, XmpPathSegment nextStep, bool createNodes) { XmpNode nextNode = null; var stepKind = nextStep.Kind; switch (stepKind) { case XmpPathStepType.StructFieldStep: nextNode = FindChildNode(parentNode, nextStep.Name, createNodes); break; case XmpPathStepType.QualifierStep: nextNode = FindQualifierNode(parentNode, nextStep.Name.Substring(1), createNodes); break; default: // This is an array indexing step. First get the index, then get the node. if (!parentNode.Options.IsArray) { throw new XmpException("Indexing applied to non-array", XmpErrorCode.BadXPath); } int index; switch (stepKind) { case XmpPathStepType.ArrayIndexStep: index = FindIndexedItem(parentNode, nextStep.Name, createNodes); break; case XmpPathStepType.ArrayLastStep: index = parentNode.GetChildrenLength(); break; case XmpPathStepType.FieldSelectorStep: Utils.SplitNameAndValue(nextStep.Name, out string fieldName, out string fieldValue); index = LookupFieldSelector(parentNode, fieldName, fieldValue); break; case XmpPathStepType.QualSelectorStep: Utils.SplitNameAndValue(nextStep.Name, out string qualName, out string qualValue); index = LookupQualSelector(parentNode, qualName, qualValue, nextStep.AliasForm); break; default: throw new XmpException("Unknown array indexing step in FollowXPathStep", XmpErrorCode.InternalFailure); } if (1 <= index && index <= parentNode.GetChildrenLength()) { nextNode = parentNode.GetChild(index); } break; } return(nextNode); }
/// <summary>The outermost call is special.</summary> /// <remarks> /// The outermost call is special. The names almost certainly differ. The /// qualifiers (and hence options) will differ for an alias to the x-default /// item of a langAlt array. /// </remarks> /// <param name="aliasNode">the alias node</param> /// <param name="baseNode">the base node of the alias</param> /// <param name="outerCall">marks the outer call of the recursion</param> /// <exception cref="XmpException">Forwards XMP errors</exception> private static void CompareAliasedSubtrees(XmpNode aliasNode, XmpNode baseNode, bool outerCall) { if (baseNode.Value != aliasNode.Value || aliasNode.GetChildrenLength() != baseNode.GetChildrenLength()) { throw new XmpException("Mismatch between alias and base nodes", XmpErrorCode.BadXmp); } if (!outerCall && (baseNode.Name != aliasNode.Name || !aliasNode.Options.Equals(baseNode.Options) || aliasNode.GetQualifierLength() != baseNode.GetQualifierLength())) { throw new XmpException("Mismatch between alias and base nodes", XmpErrorCode.BadXmp); } for (IIterator an = aliasNode.IterateChildren(), bn = baseNode.IterateChildren(); an.HasNext() && bn.HasNext();) { var aliasChild = (XmpNode)an.Next(); var baseChild = (XmpNode)bn.Next(); CompareAliasedSubtrees(aliasChild, baseChild, false); } for (IIterator an = aliasNode.IterateQualifier(), bn1 = baseNode.IterateQualifier(); an.HasNext() && bn1.HasNext();) { var aliasQual = (XmpNode)an.Next(); var baseQual = (XmpNode)bn1.Next(); CompareAliasedSubtrees(aliasQual, baseQual, false); } }
/// <summary> /// Searches for a field selector in a node: /// [fieldName="value] - an element in an array of structs, chosen by a field value. /// </summary> /// <remarks> /// Searches for a field selector in a node: /// [fieldName="value] - an element in an array of structs, chosen by a field value. /// No implicit nodes are created by field selectors. /// </remarks> /// <param name="arrayNode"/> /// <param name="fieldName"/> /// <param name="fieldValue"/> /// <returns>Returns the index of the field if found, otherwise -1.</returns> /// <exception cref="XmpException"></exception> private static int LookupFieldSelector(XmpNode arrayNode, string fieldName, string fieldValue) { var result = -1; for (var index = 1; index <= arrayNode.GetChildrenLength() && result < 0; index++) { var currItem = arrayNode.GetChild(index); if (!currItem.Options.IsStruct) { throw new XmpException("Field selector must be used on array of struct", XmpErrorCode.BadXPath); } for (var f = 1; f <= currItem.GetChildrenLength(); f++) { var currField = currItem.GetChild(f); if (currField.Name != fieldName || currField.Value != fieldValue) { continue; } result = index; break; } } return(result); }
/// <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); }
/// <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>Looks for the appropriate language item in a text alternative array.item</summary> /// <param name="arrayNode">an array node</param> /// <param name="language">the requested language</param> /// <returns>Returns the index if the language has been found, -1 otherwise.</returns> /// <exception cref="XmpException"/> internal static int LookupLanguageItem(XmpNode arrayNode, string language) { if (!arrayNode.Options.IsArray) { throw new XmpException("Language item must be used on array", XmpErrorCode.BadXPath); } for (var index = 1; index <= arrayNode.GetChildrenLength(); index++) { var child = arrayNode.GetChild(index); if (!child.HasQualifier || !XmpConstants.XmlLang.Equals(child.GetQualifier(1).Name)) { continue; } if (language.Equals(child.GetQualifier(1).Value)) { return(index); } } return(-1); }
/// <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); } } } }
/// <summary>Compares two nodes including its children and qualifier.</summary> /// <param name="leftNode">an <c>XMPNode</c></param> /// <param name="rightNode">an <c>XMPNode</c></param> /// <returns>Returns true if the nodes are equal, false otherwise.</returns> /// <exception cref="XmpException">Forwards exceptions to the calling method.</exception> private static bool ItemValuesMatch(XmpNode leftNode, XmpNode rightNode) { var leftForm = leftNode.Options; var rightForm = rightNode.Options; if (leftForm.Equals(rightForm)) { return(false); } if (leftForm.GetOptions() == 0) { // Simple nodes, check the values and xml:lang qualifiers. if (!leftNode.Value.Equals(rightNode.Value)) { return(false); } if (leftNode.Options.HasLanguage != rightNode.Options.HasLanguage) { return(false); } if (leftNode.Options.HasLanguage && !leftNode.GetQualifier(1).Value.Equals(rightNode.GetQualifier(1).Value)) { return(false); } } else { if (leftForm.IsStruct) { // Struct nodes, see if all fields match, ignoring order. if (leftNode.GetChildrenLength() != rightNode.GetChildrenLength()) { return(false); } for (var it = leftNode.IterateChildren(); it.HasNext();) { var leftField = (XmpNode)it.Next(); var rightField = XmpNodeUtils.FindChildNode(rightNode, leftField.Name, false); if (rightField == null || !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. Debug.Assert(leftForm.IsArray); for (var il = leftNode.IterateChildren(); il.HasNext();) { var leftItem = (XmpNode)il.Next(); var match = false; for (var ir = rightNode.IterateChildren(); ir.HasNext();) { var rightItem = (XmpNode)ir.Next(); if (ItemValuesMatch(leftItem, rightItem)) { match = true; break; } } if (!match) { return(false); } } } } return(true); }
/// <summary> /// After processing by ExpandXPath, a step can be of these forms: /// </summary> /// <remarks> /// After processing by ExpandXPath, a step can be of these forms: /// <list type="bullet"> /// <item>qualName - A top level property or struct field.</item> /// <item>[index] - An element of an array.</item> /// <item>[last()] - The last element of an array.</item> /// <item>[qualName="value"] - An element in an array of structs, chosen by a field value.</item> /// <item>[?qualName="value"] - An element in an array, chosen by a qualifier value.</item> /// <item>?qualName - A general qualifier.</item> /// </list> /// Find the appropriate child node, resolving aliases, and optionally creating nodes. /// </remarks> /// <param name="parentNode">the node to start to start from</param> /// <param name="nextStep">the xpath segment</param> /// <param name="createNodes"></param> /// <returns>returns the found or created XMPPath node</returns> /// <exception cref="XmpException"></exception> private static XmpNode FollowXPathStep(XmpNode parentNode, XmpPathSegment nextStep, bool createNodes) { XmpNode nextNode = null; var stepKind = nextStep.Kind; if (stepKind == XmpPath.StructFieldStep) { nextNode = FindChildNode(parentNode, nextStep.Name, createNodes); } else { if (stepKind == XmpPath.QualifierStep) { nextNode = FindQualifierNode(parentNode, nextStep.Name.Substring(1), createNodes); } else { // This is an array indexing step. First get the index, then get the node. if (!parentNode.Options.IsArray) { throw new XmpException("Indexing applied to non-array", XmpErrorCode.BadXPath); } var index = 0; if (stepKind == XmpPath.ArrayIndexStep) { index = FindIndexedItem(parentNode, nextStep.Name, createNodes); } else { if (stepKind == XmpPath.ArrayLastStep) { index = parentNode.GetChildrenLength(); } else { if (stepKind == XmpPath.FieldSelectorStep) { var result = Utils.SplitNameAndValue(nextStep.Name); var fieldName = result[0]; var fieldValue = result[1]; index = LookupFieldSelector(parentNode, fieldName, fieldValue); } else { if (stepKind == XmpPath.QualSelectorStep) { var result = Utils.SplitNameAndValue(nextStep.Name); var qualName = result[0]; var qualValue = result[1]; index = LookupQualSelector(parentNode, qualName, qualValue, nextStep.AliasForm); } else { throw new XmpException("Unknown array indexing step in FollowXPathStep", XmpErrorCode.InternalFailure); } } } } if (1 <= index && index <= parentNode.GetChildrenLength()) { nextNode = parentNode.GetChild(index); } } } return(nextNode); }