/// <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> /// Tweak old XMP: Move an instance ID from rdf:about to the /// <em>xmpMM:InstanceID</em> property. /// </summary> /// <remarks> /// Tweak old XMP: Move an instance ID from rdf:about to the /// <em>xmpMM:InstanceID</em> property. An old instance ID usually looks /// like "uuid:bac965c4-9d87-11d9-9a30-000d936b79c4", plus InDesign /// 3.0 wrote them like "bac965c4-9d87-11d9-9a30-000d936b79c4". If /// the name looks like a UUID simply move it to <em>xmpMM:InstanceID</em>, /// don't worry about any existing <em>xmpMM:InstanceID</em>. Both will /// only be present when a newer file with the <em>xmpMM:InstanceID</em> /// property is updated by an old app that uses <em>rdf:about</em>. /// </remarks> /// <param name="tree">the root of the metadata tree</param> /// <exception cref="XmpException">Thrown if tweaking fails.</exception> private static void TweakOldXmp(XmpNode tree) { if (tree.Name != null && tree.Name.Length >= Utils.UuidLength) { var nameStr = tree.Name.ToLower(); if (nameStr.StartsWith("uuid:")) { nameStr = nameStr.Substring(5); } if (Utils.CheckUuidFormat(nameStr)) { // move UUID to xmpMM:InstanceID and remove it from the root node var path = XmpPathParser.ExpandXPath(XmpConstants.NsXmpMm, "InstanceID"); var idNode = XmpNodeUtils.FindNode(tree, path, true, null); if (idNode != null) { idNode.Options = null; // Clobber any existing xmpMM:InstanceID. idNode.Value = "uuid:" + nameStr; idNode.RemoveChildren(); idNode.RemoveQualifiers(); tree.Name = null; } else { throw new XmpException("Failure creating xmpMM:InstanceID", XmpErrorCode.InternalFailure); } } } }
/// <summary>Fixes the GPS Timestamp in EXIF.</summary> /// <param name="exifSchema">the EXIF schema node</param> /// <exception cref="XmpException">Thrown if the date conversion fails.</exception> private static void FixGpsTimeStamp(XmpNode exifSchema) { // Note: if dates are not found the convert-methods throws an exceptions, // and this methods returns. var gpsDateTime = XmpNodeUtils.FindChildNode(exifSchema, "exif:GPSTimeStamp", false); if (gpsDateTime == null) { return; } try { var binGpsStamp = XmpCore.XmpUtils.ConvertToDate(gpsDateTime.Value); if (binGpsStamp.Year != 0 || binGpsStamp.Month != 0 || binGpsStamp.Day != 0) { return; } var otherDate = XmpNodeUtils.FindChildNode(exifSchema, "exif:DateTimeOriginal", false) ?? XmpNodeUtils.FindChildNode(exifSchema, "exif:DateTimeDigitized", false); var binOtherDate = XmpCore.XmpUtils.ConvertToDate(otherDate.Value); var cal = binGpsStamp.Calendar; cal.Set(CalendarEnum.Year, binOtherDate.Year); cal.Set(CalendarEnum.Month, binOtherDate.Month); cal.Set(CalendarEnum.DayOfMonth, binOtherDate.Day); binGpsStamp = new XmpDateTime(cal); gpsDateTime.Value = XmpCore.XmpUtils.ConvertFromDate(binGpsStamp); } catch (XmpException) { // Don't let a missing or bad date stop other things. } }
/// <exception cref="XmpException"/> public IXmpProperty GetLocalizedText(string schemaNs, string altTextName, string genericLang, string specificLang) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(altTextName); ParameterAsserts.AssertSpecificLang(specificLang); genericLang = genericLang != null?Utils.NormalizeLangValue(genericLang) : null; specificLang = Utils.NormalizeLangValue(specificLang); var arrayPath = XmpPathParser.ExpandXPath(schemaNs, altTextName); var arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, false, null); if (arrayNode == null) { return(null); } var result = XmpNodeUtils.ChooseLocalizedText(arrayNode, genericLang, specificLang); var match = (int)result[0]; var itemNode = (XmpNode)result[1]; if (match != XmpNodeUtils.CltNoValues) { return(new XmpProperty407(itemNode)); } return(null); }
/// <summary> /// The internals for setProperty() and related calls, used after the node is /// found or created. /// </summary> /// <param name="node">the newly created node</param> /// <param name="value">the node value, can be <c>null</c></param> /// <param name="newOptions">options for the new node, must not be <c>null</c>.</param> /// <param name="deleteExisting">flag if the existing value is to be overwritten</param> /// <exception cref="XmpException">thrown if options and value do not correspond</exception> internal static void SetNode(XmpNode node, object value, PropertyOptions newOptions, bool deleteExisting) { if (deleteExisting) { node.Clear(); } // its checked by setOptions(), if the merged result is a valid options set node.Options.MergeWith(newOptions); if (!node.Options.IsCompositeProperty) { // This is setting the value of a leaf node. XmpNodeUtils.SetNodeValue(node, value); } else { if (value != null && value.ToString().Length > 0) { throw new XmpException("Composite nodes can't have values", XmpErrorCode.BadXPath); } node.RemoveChildren(); } }
/// <summary>Visit all schemas to do general fixes and handle special cases.</summary> /// <param name="xmp">the metadata object implementation</param> /// <exception cref="XmpException">Thrown if the normalisation fails.</exception> private static void TouchUpDataModel(XmpMeta xmp) { // make sure the DC schema is existing, because it might be needed within the normalization // if not touched it will be removed by removeEmptySchemas XmpNodeUtils.FindSchemaNode(xmp.GetRoot(), XmpConstants.NsDC, true); // Do the special case fixes within each schema. for (var it = xmp.GetRoot().IterateChildren(); it.HasNext();) { var currSchema = (XmpNode)it.Next(); switch (currSchema.Name) { case XmpConstants.NsDC: { NormalizeDcArrays(currSchema); break; } case XmpConstants.NsExif: { // Do a special case fix for exif:GPSTimeStamp. FixGpsTimeStamp(currSchema); var arrayNode = XmpNodeUtils.FindChildNode(currSchema, "exif:UserComment", false); if (arrayNode != null) { RepairAltText(arrayNode); } break; } case XmpConstants.NsDm: { // Do a special case migration of xmpDM:copyright to // dc:rights['x-default']. var dmCopyright = XmpNodeUtils.FindChildNode(currSchema, "xmpDM:copyright", false); if (dmCopyright != null) { MigrateAudioCopyright(xmp, dmCopyright); } break; } case XmpConstants.NsXmpRights: { var arrayNode = XmpNodeUtils.FindChildNode(currSchema, "xmpRights:UsageTerms", false); if (arrayNode != null) { RepairAltText(arrayNode); } break; } } } }
/// <summary> /// The initial support for WAV files mapped a legacy ID3 audio copyright /// into a new xmpDM:copyright property. /// </summary> /// <remarks> /// The initial support for WAV files mapped a legacy ID3 audio copyright /// into a new xmpDM:copyright property. This is special case code to migrate /// that into dc:rights['x-default']. The rules: /// <pre> /// 1. If there is no dc:rights array, or an empty array - /// Create one with dc:rights['x-default'] set from double linefeed and xmpDM:copyright. /// 2. If there is a dc:rights array but it has no x-default item - /// Create an x-default item as a copy of the first item then apply rule #3. /// 3. If there is a dc:rights array with an x-default item, /// Look for a double linefeed in the value. /// A. If no double linefeed, compare the x-default value to the xmpDM:copyright value. /// A1. If they match then leave the x-default value alone. /// A2. Otherwise, append a double linefeed and /// the xmpDM:copyright value to the x-default value. /// B. If there is a double linefeed, compare the trailing text to the xmpDM:copyright value. /// B1. If they match then leave the x-default value alone. /// B2. Otherwise, replace the trailing x-default text with the xmpDM:copyright value. /// 4. In all cases, delete the xmpDM:copyright property. /// </pre> /// </remarks> /// <param name="xmp">the metadata object</param> /// <param name="dmCopyright">the "dm:copyright"-property</param> private static void MigrateAudioCopyright(IXmpMeta xmp, XmpNode dmCopyright) { try { var dcSchema = XmpNodeUtils.FindSchemaNode(((XmpMeta)xmp).GetRoot(), XmpConstants.NsDC, true); var dmValue = dmCopyright.Value; var doubleLf = "\n\n"; var dcRightsArray = XmpNodeUtils.FindChildNode(dcSchema, "dc:rights", false); if (dcRightsArray == null || !dcRightsArray.HasChildren) { // 1. No dc:rights array, create from double linefeed and xmpDM:copyright. dmValue = doubleLf + dmValue; xmp.SetLocalizedText(XmpConstants.NsDC, "rights", string.Empty, XmpConstants.XDefault, dmValue, null); } else { var xdIndex = XmpNodeUtils.LookupLanguageItem(dcRightsArray, XmpConstants.XDefault); if (xdIndex < 0) { // 2. No x-default item, create from the first item. var firstValue = dcRightsArray.GetChild(1).Value; xmp.SetLocalizedText(XmpConstants.NsDC, "rights", string.Empty, XmpConstants.XDefault, firstValue, null); xdIndex = XmpNodeUtils.LookupLanguageItem(dcRightsArray, XmpConstants.XDefault); } // 3. Look for a double linefeed in the x-default value. var defaultNode = dcRightsArray.GetChild(xdIndex); var defaultValue = defaultNode.Value; var lfPos = defaultValue.IndexOf(doubleLf); if (lfPos < 0) { // 3A. No double LF, compare whole values. if (!dmValue.Equals(defaultValue)) { // 3A2. Append the xmpDM:copyright to the x-default // item. defaultNode.Value = defaultValue + doubleLf + dmValue; } } else { // 3B. Has double LF, compare the tail. if (!defaultValue.Substring(lfPos + 2).Equals(dmValue)) { // 3B2. Replace the x-default tail. defaultNode.Value = defaultValue.Substring(0, lfPos + 2 - 0) + dmValue; } } } // 4. Get rid of the xmpDM:copyright. dmCopyright.Parent.RemoveChild(dmCopyright); } catch (XmpException) { } }
/// <summary>Constructor with optional initial values.</summary> /// <remarks>If <c>propName</c> is provided, <c>schemaNS</c> has also be provided.</remarks> /// <param name="xmp">the iterated metadata object.</param> /// <param name="schemaNs">the iteration is reduced to this schema (optional)</param> /// <param name="propPath">the iteration is reduced to this property within the <c>schemaNS</c></param> /// <param name="options">advanced iteration options, see <see cref="IteratorOptions"/></param> /// <exception cref="XmpException">If the node defined by the parameters is not existing.</exception> public XmpIterator(XmpMeta xmp, string schemaNs, string propPath, IteratorOptions options) { // make sure that options is defined at least with defaults Options = options ?? new IteratorOptions(); // the start node of the iteration depending on the schema and property filter XmpNode startNode = null; string initialPath = null; var baseSchema = !string.IsNullOrEmpty(schemaNs); var baseProperty = !string.IsNullOrEmpty(propPath); if (!baseSchema && !baseProperty) { // complete tree will be iterated startNode = xmp.GetRoot(); } else { if (baseSchema && baseProperty) { // Schema and property node provided var path = XmpPathParser.ExpandXPath(schemaNs, propPath); // base path is the prop path without the property leaf var basePath = new XmpPath(); for (var i = 0; i < path.Size() - 1; i++) { basePath.Add(path.GetSegment(i)); } startNode = XmpNodeUtils.FindNode(xmp.GetRoot(), path, false, null); BaseNamespace = schemaNs; initialPath = basePath.ToString(); } else { if (baseSchema && !baseProperty) { // Only Schema provided startNode = XmpNodeUtils.FindSchemaNode(xmp.GetRoot(), schemaNs, false); } else { // !baseSchema && baseProperty // No schema but property provided -> error throw new XmpException("Schema namespace URI is required", XmpErrorCode.BadSchema); } } } // create iterator _nodeIterator = startNode != null ? (IIterator)(!Options.IsJustChildren ? new NodeIterator(this, startNode, initialPath, 1) : new NodeIteratorChildren(this, startNode, initialPath)) : Enumerable.Empty <object>().Iterator(); }
/// <exception cref="XmpException"/> public void AppendArrayItem(string schemaNs, string arrayName, PropertyOptions arrayOptions, string itemValue, PropertyOptions itemOptions) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(arrayName); if (arrayOptions == null) { arrayOptions = new PropertyOptions(); } if (!arrayOptions.IsOnlyArrayOptions) { throw new XmpException("Only array form flags allowed for arrayOptions", XmpErrorCode.BadOptions); } // Check if array options are set correctly. arrayOptions = XmpNodeUtils.VerifySetOptions(arrayOptions, null); // Locate or create the array. If it already exists, make sure the array // form from the options // parameter is compatible with the current state. var arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName); // Just lookup, don't try to create. var arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, false, null); if (arrayNode != null) { // The array exists, make sure the form is compatible. Zero // arrayForm means take what exists. if (!arrayNode.Options.IsArray) { throw new XmpException("The named property is not an array", XmpErrorCode.BadXPath); } } else { // if (arrayOptions != null && !arrayOptions.equalArrayTypes(arrayNode.getOptions())) // { // throw new XMPException("Mismatch of existing and specified array form", BADOPTIONS); // } // The array does not exist, try to create it. if (arrayOptions.IsArray) { arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, true, arrayOptions); if (arrayNode == null) { throw new XmpException("Failure creating array node", XmpErrorCode.BadXPath); } } else { // array options missing throw new XmpException("Explicit arrayOptions required to create new array", XmpErrorCode.BadOptions); } } DoSetArrayItem(arrayNode, XmpConstants.ArrayLastItem, itemValue, itemOptions, true); }
/// <summary>Serializes an array property.</summary> /// <param name="node">an XMPNode</param> /// <param name="indent">the current indent level</param> /// <exception cref="System.IO.IOException">Forwards the writer exceptions.</exception> /// <exception cref="XmpException">If qualifier and element fields are mixed.</exception> private void SerializeCompactRdfArrayProp(XmpNode node, int indent) { // This is an array. Write('>'); WriteNewline(); EmitRdfArrayTag(node, true, indent + 1); if (node.Options.IsArrayAltText) { XmpNodeUtils.NormalizeLangArray(node); } SerializeCompactRdfElementProps(node, indent + 2); EmitRdfArrayTag(node, false, indent + 1); }
public bool DoesPropertyExist(string schemaNs, string propName) { try { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertPropName(propName); var expPath = XmpPathParser.ExpandXPath(schemaNs, propName); var propNode = XmpNodeUtils.FindNode(_tree, expPath, false, null); return(propNode != null); } catch (XmpException) { return(false); } }
/// <summary>Associates an alias name with an actual name.</summary> /// <remarks> /// Associates an alias name with an actual name. /// <para /> /// Define a alias mapping from one namespace/property to another. Both /// property names must be simple names. An alias can be a direct mapping, /// where the alias and actual have the same data type. It is also possible /// to map a simple alias to an item in an array. This can either be to the /// first item in the array, or to the 'x-default' item in an alt-text array. /// Multiple alias names may map to the same actual, as long as the forms /// match. It is a no-op to reregister an alias in an identical fashion. /// Note: This method is not locking because only called by registerStandardAliases /// which is only called by the constructor. /// Note2: The method is only package-private so that it can be tested with unittests /// </remarks> /// <param name="aliasNs">The namespace URI for the alias. Must not be null or the empty string.</param> /// <param name="aliasProp">The name of the alias. Must be a simple name, not null or the empty string and not a general path expression.</param> /// <param name="actualNs">The namespace URI for the actual. Must not be null or the empty string.</param> /// <param name="actualProp">The name of the actual. Must be a simple name, not null or the empty string and not a general path expression.</param> /// <param name="aliasForm">Provides options for aliases for simple aliases to array items. This is needed to know what kind of array to create if /// set for the first time via the simple alias. Pass <c>XMP_NoOptions</c>, the default value, for all direct aliases regardless of whether the actual /// data type is an array or not (see <see cref="AliasOptions"/>).</param> /// <exception cref="XmpException">for inconsistant aliases.</exception> private void RegisterAlias(string aliasNs, string aliasProp, string actualNs, string actualProp, AliasOptions aliasForm) { lock (_lock) { ParameterAsserts.AssertSchemaNs(aliasNs); ParameterAsserts.AssertPropName(aliasProp); ParameterAsserts.AssertSchemaNs(actualNs); ParameterAsserts.AssertPropName(actualProp); // FfF: if we need the decoration with [1] or // FfF: [?xml:lang="x-default"] for array forms // Fix the alias options var aliasOpts = aliasForm != null ? new AliasOptions(XmpNodeUtils.VerifySetOptions(aliasForm.ToPropertyOptions(), null).GetOptions()) : new AliasOptions(); if (_p.IsMatch(aliasProp) || _p.IsMatch(actualProp)) { throw new XmpException("Alias and actual property names must be simple", XmpErrorCode.BadXPath); } // check if both namespaces are registered var aliasPrefix = GetNamespacePrefix(aliasNs); var actualPrefix = GetNamespacePrefix(actualNs); if (aliasPrefix == null) { throw new XmpException("Alias namespace is not registered", XmpErrorCode.BadSchema); } if (actualPrefix == null) { throw new XmpException("Actual namespace is not registered", XmpErrorCode.BadSchema); } var key = aliasPrefix + aliasProp; // check if alias is already existing if (_aliasMap.ContainsKey(key)) { throw new XmpException("Alias is already existing", XmpErrorCode.BadParam); } if (_aliasMap.ContainsKey(actualPrefix + actualProp)) { throw new XmpException("Actual property is already an alias, use the base property", XmpErrorCode.BadParam); } _aliasMap[key] = new XmpAliasInfo(actualNs, actualPrefix, actualProp, aliasOpts); } }
/// <summary>Returns a property, but the result value can be requested.</summary> /// <param name="schemaNs">a schema namespace</param> /// <param name="propName">a property name or path</param> /// <param name="valueType">the type of the value, see VALUE_...</param> /// <returns> /// Returns the node value as an object according to the /// <c>valueType</c>. /// </returns> /// <exception cref="XmpException">Collects any exception that occurs.</exception> private object GetPropertyObject(string schemaNs, string propName, ValueType valueType) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertPropName(propName); var expPath = XmpPathParser.ExpandXPath(schemaNs, propName); var propNode = XmpNodeUtils.FindNode(_tree, expPath, false, null); if (propNode != null) { if (valueType != ValueType.String && propNode.Options.IsCompositeProperty) { throw new XmpException("Property must be simple when a value type is requested", XmpErrorCode.BadXPath); } return(EvaluateNodeValue(valueType, propNode)); } return(null); }
public void DeleteProperty(string schemaNs, string propName) { try { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertPropName(propName); var expPath = XmpPathParser.ExpandXPath(schemaNs, propName); var propNode = XmpNodeUtils.FindNode(_tree, expPath, false, null); if (propNode != null) { XmpNodeUtils.DeleteNode(propNode); } } catch (XmpException) { } }
/// <exception cref="XmpException"/> public int CountArrayItems(string schemaNs, string arrayName) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(arrayName); var arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName); var arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, false, null); if (arrayNode == null) { return(0); } if (arrayNode.Options.IsArray) { return(arrayNode.GetChildrenLength()); } throw new XmpException("The named property is not an array", XmpErrorCode.BadXPath); }
/// <exception cref="XmpException"/> public void SetProperty(string schemaNs, string propName, object propValue, PropertyOptions options) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertPropName(propName); options = XmpNodeUtils.VerifySetOptions(options, propValue); var expPath = XmpPathParser.ExpandXPath(schemaNs, propName); var propNode = XmpNodeUtils.FindNode(_tree, expPath, true, options); if (propNode != null) { SetNode(propNode, propValue, options, false); } else { throw new XmpException("Specified property does not exist", XmpErrorCode.BadXPath); } }
/// <exception cref="XmpException"/> public void InsertArrayItem(string schemaNs, string arrayName, int itemIndex, string itemValue, PropertyOptions options) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(arrayName); // Just lookup, don't try to create. var arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName); var arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, false, null); if (arrayNode != null) { DoSetArrayItem(arrayNode, itemIndex, itemValue, options, true); } else { throw new XmpException("Specified array does not exist", XmpErrorCode.BadXPath); } }
/// <summary>Utility to find or create the array used by <c>separateArrayItems()</c>.</summary> /// <param name="schemaNs">a the namespace fo the array</param> /// <param name="arrayName">the name of the array</param> /// <param name="arrayOptions">the options for the array if newly created</param> /// <param name="xmp">the xmp object</param> /// <returns>Returns the array node.</returns> /// <exception cref="XmpException">Forwards exceptions</exception> private static XmpNode SeparateFindCreateArray(string schemaNs, string arrayName, PropertyOptions arrayOptions, XmpMeta xmp) { arrayOptions = XmpNodeUtils.VerifySetOptions(arrayOptions, null); if (!arrayOptions.IsOnlyArrayOptions) { throw new XmpException("Options can only provide array form", XmpErrorCode.BadOptions); } // Find the array node, make sure it is OK. Move the current children // aside, to be readded later if kept. var arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName); var arrayNode = XmpNodeUtils.FindNode(xmp.GetRoot(), arrayPath, false, null); if (arrayNode != null) { // The array exists, make sure the form is compatible. Zero // arrayForm means take what exists. var arrayForm = arrayNode.Options; if (!arrayForm.IsArray || arrayForm.IsArrayAlternate) { throw new XmpException("Named property must be non-alternate array", XmpErrorCode.BadXPath); } if (arrayOptions.EqualArrayTypes(arrayForm)) { throw new XmpException("Mismatch of specified and existing array form", XmpErrorCode.BadXPath); } } else { // *** Right error? // The array does not exist, try to create it. // don't modify the options handed into the method arrayOptions.IsArray = true; arrayNode = XmpNodeUtils.FindNode(xmp.GetRoot(), arrayPath, true, arrayOptions); if (arrayNode == null) { throw new XmpException("Failed to create named array", XmpErrorCode.BadXPath); } } return(arrayNode); }
/// <param name="source">The source XMP object.</param> /// <param name="destination">The destination XMP object.</param> /// <param name="doAllProperties">Do internal properties in addition to external properties.</param> /// <param name="replaceOldValues">Replace the values of existing properties.</param> /// <param name="deleteEmptyValues">Delete destination values if source property is empty.</param> /// <exception cref="XmpException">Forwards the Exceptions from the metadata processing</exception> public static void AppendProperties(IXmpMeta source, IXmpMeta destination, bool doAllProperties, bool replaceOldValues, bool deleteEmptyValues) { ParameterAsserts.AssertImplementation(source); ParameterAsserts.AssertImplementation(destination); var src = (XmpMeta)source; var dest = (XmpMeta)destination; for (var it = src.GetRoot().IterateChildren(); it.HasNext();) { var sourceSchema = (XmpNode)it.Next(); // Make sure we have a destination schema node var destSchema = XmpNodeUtils.FindSchemaNode(dest.GetRoot(), sourceSchema.Name, false); var createdSchema = false; if (destSchema == null) { destSchema = new XmpNode(sourceSchema.Name, sourceSchema.Value, new PropertyOptions { IsSchemaNode = true }); dest.GetRoot().AddChild(destSchema); createdSchema = true; } // Process the source schema's children. for (var ic = sourceSchema.IterateChildren(); ic.HasNext();) { var sourceProp = (XmpNode)ic.Next(); if (doAllProperties || !Utils.IsInternalProperty(sourceSchema.Name, sourceProp.Name)) { AppendSubtree(dest, sourceProp, destSchema, replaceOldValues, deleteEmptyValues); } } if (!destSchema.HasChildren && (createdSchema || deleteEmptyValues)) { // Don't create an empty schema / remove empty schema. dest.GetRoot().RemoveChild(destSchema); } } }
/// <exception cref="XmpException"/> public 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); var arrayPath = XmpPathParser.ExpandXPath(schemaNs, altTextName); // Find the array node and set the options if it was just created. var arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, true, new PropertyOptions(PropertyOptions.ArrayFlag | PropertyOptions.ArrayOrderedFlag | PropertyOptions.ArrayAlternateFlag | PropertyOptions.ArrayAltTextFlag)); if (arrayNode == null) { throw new XmpException("Failed to find or create array node", XmpErrorCode.BadXPath); } if (!arrayNode.Options.IsArrayAltText) { if (!arrayNode.HasChildren && arrayNode.Options.IsArrayAlternate) { arrayNode.Options.IsArrayAltText = true; } else { throw new XmpException("Specified property is no alt-text array", XmpErrorCode.BadXPath); } } // Make sure the x-default item, if any, is first. var haveXDefault = false; XmpNode xdItem = null; for (var it = arrayNode.IterateChildren(); it.HasNext();) { var currItem = (XmpNode)it.Next(); if (!currItem.HasQualifier || !XmpConstants.XmlLang.Equals(currItem.GetQualifier(1).Name)) { throw new XmpException("Language qualifier must be first", XmpErrorCode.BadXPath); } if (XmpConstants.XDefault.Equals(currItem.GetQualifier(1).Value)) { xdItem = currItem; haveXDefault = true; break; } } // Moves x-default to the beginning of the array if (xdItem != null && arrayNode.GetChildrenLength() > 1) { arrayNode.RemoveChild(xdItem); arrayNode.AddChild(1, xdItem); } // Find the appropriate item. // chooseLocalizedText will make sure the array is a language // alternative. var result = XmpNodeUtils.ChooseLocalizedText(arrayNode, genericLang, specificLang); var match = (int)result[0]; var itemNode = (XmpNode)result[1]; var specificXDefault = XmpConstants.XDefault.Equals(specificLang); switch (match) { case XmpNodeUtils.CltNoValues: { // Create the array items for the specificLang and x-default, with // x-default first. XmpNodeUtils.AppendLangItem(arrayNode, XmpConstants.XDefault, itemValue); haveXDefault = true; if (!specificXDefault) { XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); } break; } case XmpNodeUtils.CltSpecificMatch: { 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); for (var it1 = arrayNode.IterateChildren(); it1.HasNext();) { var currItem = (XmpNode)it1.Next(); 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.CltSingleGeneric: { // 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.CltMultipleGeneric: { // Create the specific language, ignore x-default. XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); if (specificXDefault) { haveXDefault = true; } break; } case XmpNodeUtils.CltXdefault: { // Create the specific language, update x-default if it was the only // item. if (xdItem != null && arrayNode.GetChildrenLength() == 1) { xdItem.Value = itemValue; } XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); break; } case XmpNodeUtils.CltFirstItem: { // 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", XmpErrorCode.InternalFailure); } } // Add an x-default at the front if needed. if (!haveXDefault && arrayNode.GetChildrenLength() == 1) { XmpNodeUtils.AppendLangItem(arrayNode, XmpConstants.XDefault, itemValue); } }
/// <param name="xmp">The XMP object containing the properties to be removed.</param> /// <param name="schemaNs"> /// Optional schema namespace URI for the properties to be /// removed. /// </param> /// <param name="propName">Optional path expression for the property to be removed.</param> /// <param name="doAllProperties"> /// Option flag to control the deletion: do internal properties in /// addition to external properties. /// </param> /// <param name="includeAliases"> /// Option flag to control the deletion: Include aliases in the /// "named schema" case above. /// </param> /// <exception cref="XmpException">If metadata processing fails</exception> public static void RemoveProperties(IXmpMeta xmp, string schemaNs, string propName, bool doAllProperties, bool includeAliases) { ParameterAsserts.AssertImplementation(xmp); var xmpImpl = (XmpMeta)xmp; if (!string.IsNullOrEmpty(propName)) { // 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 (string.IsNullOrEmpty(schemaNs)) { throw new XmpException("Property name requires schema namespace", XmpErrorCode.BadParam); } var expPath = XmpPathParser.ExpandXPath(schemaNs, propName); var propNode = XmpNodeUtils.FindNode(xmpImpl.GetRoot(), expPath, false, null); if (propNode != null) { if (doAllProperties || !Utils.IsInternalProperty(expPath.GetSegment(XmpPath.StepSchema).Name, expPath.GetSegment(XmpPath.StepRootProp).Name)) { var parent = propNode.Parent; parent.RemoveChild(propNode); if (parent.Options.IsSchemaNode && !parent.HasChildren) { // remove empty schema node parent.Parent.RemoveChild(parent); } } } } else { if (!string.IsNullOrEmpty(schemaNs)) { // Remove all properties from the named schema. Optionally include // aliases, in which case // there might not be an actual schema node. // XMP_NodePtrPos schemaPos; var schemaNode = XmpNodeUtils.FindSchemaNode(xmpImpl.GetRoot(), schemaNs, false); if (schemaNode != null && RemoveSchemaChildren(schemaNode, doAllProperties)) { xmpImpl.GetRoot().RemoveChild(schemaNode); } if (includeAliases) { // We're removing the aliases also. Look them up by their namespace prefix. // But that takes more code and the extra speed isn't worth it. // Lookup the XMP node from the alias, to make sure the actual exists. foreach (var info in XmpMetaFactory.SchemaRegistry.FindAliases(schemaNs)) { var path = XmpPathParser.ExpandXPath(info.Namespace, info.PropName); var actualProp = XmpNodeUtils.FindNode(xmpImpl.GetRoot(), path, false, null); if (actualProp != null) { actualProp.Parent.RemoveChild(actualProp); } } } } 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. for (var it = xmpImpl.GetRoot().IterateChildren(); it.HasNext();) { var schema = (XmpNode)it.Next(); if (RemoveSchemaChildren(schema, doAllProperties)) { it.Remove(); } } } } }
/// <summary>Recursively handles the "value" for a node.</summary> /// <remarks> /// Recursively handles the "value" for a node. It does not matter if it is a /// top level property, a field of a struct, or an item of an array. The /// indent is that for the property element. An xml:lang qualifier is written /// as an attribute of the property start tag, not by itself forcing the /// qualified property form. The patterns below mostly ignore attribute /// qualifiers like xml:lang. Except for the one struct case, attribute /// qualifiers don't affect the output form. /// <code> /// <ns:UnqualifiedSimpleProperty>value</ns:UnqualifiedSimpleProperty> /// <ns:UnqualifiedStructProperty> (If no rdf:resource qualifier) /// <rdf:Description> /// ... Fields, same forms as top level properties /// </rdf:Description> /// </ns:UnqualifiedStructProperty> /// <ns:ResourceStructProperty rdf:resource="URI" /// ... Fields as attributes /// > /// <ns:UnqualifiedArrayProperty> /// <rdf:Bag> or Seq or Alt /// ... Array items as rdf:li elements, same forms as top level properties /// </rdf:Bag> /// </ns:UnqualifiedArrayProperty> /// <ns:QualifiedProperty> /// <rdf:Description> /// <rdf:value> ... Property "value" following the unqualified /// forms ... </rdf:value> /// ... Qualifiers looking like named struct fields /// </rdf:Description> /// </ns:QualifiedProperty> /// </code> /// </remarks> /// <param name="node">the property node</param> /// <param name="emitAsRdfValue">property shall be rendered as attribute rather than tag</param> /// <param name="useCanonicalRdf"> /// use canonical form with inner description tag or /// the compact form with rdf:ParseType="resource" attribute. /// </param> /// <param name="indent">the current indent level</param> /// <exception cref="System.IO.IOException">Forwards all writer exceptions.</exception> /// <exception cref="XmpException">If "rdf:resource" and general qualifiers are mixed.</exception> private void SerializeCanonicalRdfProperty(XmpNode node, bool useCanonicalRdf, bool emitAsRdfValue, int indent) { var emitEndTag = true; var indentEndTag = true; // Determine the XML element name. Open the start tag with the name and // attribute qualifiers. var elemName = node.Name; if (emitAsRdfValue) { elemName = "rdf:value"; } else if (elemName == XmpConstants.ArrayItemName) { elemName = XmpConstants.RdfLi; } WriteIndent(indent); Write('<'); Write(elemName); var hasGeneralQualifiers = false; var hasRdfResourceQual = false; for (var it = node.IterateQualifier(); it.HasNext();) { var qualifier = (XmpNode)it.Next(); if (!RdfAttrQualifier.Contains(qualifier.Name)) { hasGeneralQualifiers = true; } else { hasRdfResourceQual = qualifier.Name == "rdf:resource"; if (!emitAsRdfValue) { Write(' '); Write(qualifier.Name); Write("=\""); AppendNodeValue(qualifier.Value, true); Write('"'); } } } // Process the property according to the standard patterns. if (hasGeneralQualifiers && !emitAsRdfValue) { // This node has general, non-attribute, qualifiers. Emit using the // qualified property form. // ! The value is output by a recursive call ON THE SAME NODE with // emitAsRDFValue set. if (hasRdfResourceQual) { throw new XmpException("Can't mix rdf:resource and general qualifiers", XmpErrorCode.BadRdf); } // Change serialization to canonical format with inner rdf:Description-tag // depending on option if (useCanonicalRdf) { Write(">"); WriteNewline(); indent++; WriteIndent(indent); Write(RdfStructStart); Write(">"); } else { Write(" rdf:parseType=\"Resource\">"); } WriteNewline(); SerializeCanonicalRdfProperty(node, useCanonicalRdf, true, indent + 1); for (var it = node.IterateQualifier(); it.HasNext();) { var qualifier = (XmpNode)it.Next(); if (!RdfAttrQualifier.Contains(qualifier.Name)) { SerializeCanonicalRdfProperty(qualifier, useCanonicalRdf, false, indent + 1); } } if (useCanonicalRdf) { WriteIndent(indent); Write(RdfStructEnd); WriteNewline(); indent--; } } else { // This node has no general qualifiers. Emit using an unqualified form. if (!node.Options.IsCompositeProperty) { // This is a simple property. if (node.Options.IsUri) { Write(" rdf:resource=\""); AppendNodeValue(node.Value, true); Write("\"/>"); WriteNewline(); emitEndTag = false; } else if (string.IsNullOrEmpty(node.Value)) { Write("/>"); WriteNewline(); emitEndTag = false; } else { Write('>'); AppendNodeValue(node.Value, false); indentEndTag = false; } } else { if (node.Options.IsArray) { // This is an array. Write('>'); WriteNewline(); EmitRdfArrayTag(node, true, indent + 1); if (node.Options.IsArrayAltText) { XmpNodeUtils.NormalizeLangArray(node); } for (var it1 = node.IterateChildren(); it1.HasNext();) { var child = (XmpNode)it1.Next(); SerializeCanonicalRdfProperty(child, useCanonicalRdf, false, indent + 2); } EmitRdfArrayTag(node, false, indent + 1); } else if (!hasRdfResourceQual) { // This is a "normal" struct, use the rdf:parseType="Resource" form. if (!node.HasChildren) { // Change serialization to canonical format with inner rdf:Description-tag // if option is set if (useCanonicalRdf) { Write(">"); WriteNewline(); WriteIndent(indent + 1); Write(RdfEmptyStruct); } else { Write(" rdf:parseType=\"Resource\"/>"); emitEndTag = false; } WriteNewline(); } else { // Change serialization to canonical format with inner rdf:Description-tag // if option is set if (useCanonicalRdf) { Write(">"); WriteNewline(); indent++; WriteIndent(indent); Write(RdfStructStart); Write(">"); } else { Write(" rdf:parseType=\"Resource\">"); } WriteNewline(); for (var it = node.IterateChildren(); it.HasNext();) { var child = (XmpNode)it.Next(); SerializeCanonicalRdfProperty(child, useCanonicalRdf, false, indent + 1); } if (useCanonicalRdf) { WriteIndent(indent); Write(RdfStructEnd); WriteNewline(); indent--; } } } else { // This is a struct with an rdf:resource attribute, use the // "empty property element" form. for (var it1 = node.IterateChildren(); it1.HasNext();) { var child = (XmpNode)it1.Next(); if (!CanBeRdfAttrProp(child)) { throw new XmpException("Can't mix rdf:resource and complex fields", XmpErrorCode.BadRdf); } WriteNewline(); WriteIndent(indent + 1); Write(' '); Write(child.Name); Write("=\""); AppendNodeValue(child.Value, true); Write('"'); } Write("/>"); WriteNewline(); emitEndTag = false; } } } // Emit the property element end tag. if (emitEndTag) { if (indentEndTag) { WriteIndent(indent); } Write("</"); Write(elemName); Write('>'); WriteNewline(); } }
/// <summary>Visit all of the top level nodes looking for aliases.</summary> /// <remarks> /// 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. /// </remarks> /// <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; var strictAliasing = options.StrictAliasing; for (var schemaIt = tree.GetUnmodifiableChildren().Iterator(); schemaIt.HasNext();) { var currSchema = (XmpNode)schemaIt.Next(); if (!currSchema.HasAliases) { continue; } for (var propertyIt = currSchema.IterateChildren(); propertyIt.HasNext();) { var currProp = (XmpNode)propertyIt.Next(); if (!currProp.IsAlias) { continue; } currProp.IsAlias = false; // Find the base path, look for the base schema and root node. var info = XmpMetaFactory.SchemaRegistry.FindAlias(currProp.Name); if (info != null) { // find or create schema var baseSchema = XmpNodeUtils.FindSchemaNode(tree, info.Namespace, null, true); baseSchema.IsImplicit = false; var baseNode = XmpNodeUtils.FindChildNode(baseSchema, info.Prefix + info.PropName, false); if (baseNode == null) { if (info.AliasForm.IsSimple()) { // A top-to-top alias, transplant the property. // change the alias property name to the base name var qname = info.Prefix + info.PropName; currProp.Name = qname; baseSchema.AddChild(currProp); // remove the alias property propertyIt.Remove(); } 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(propertyIt, currProp, baseNode); } } else if (info.AliasForm.IsSimple()) { // 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); } propertyIt.Remove(); } 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.IsArrayAltText) { var xdIndex = XmpNodeUtils.LookupLanguageItem(baseNode, XmpConstants.XDefault); if (xdIndex != -1) { itemNode = baseNode.GetChild(xdIndex); } } else if (baseNode.HasChildren) { itemNode = baseNode.GetChild(1); } if (itemNode == null) { TransplantArrayItemAlias(propertyIt, currProp, baseNode); } else if (strictAliasing) { CompareAliasedSubtrees(currProp, itemNode, true); } propertyIt.Remove(); } } } currSchema.HasAliases = false; } }
/// <summary>Visit all schemas to do general fixes and handle special cases.</summary> /// <param name="xmp">the metadata object implementation</param> /// <exception cref="XmpException">Thrown if the normalisation fails.</exception> private static void TouchUpDataModel(XmpMeta xmp) { // make sure the DC schema is existing, because it might be needed within the normalization // if not touched it will be removed by removeEmptySchemas XmpNodeUtils.FindSchemaNode(xmp.GetRoot(), XmpConstants.NsDC, true); // Do the special case fixes within each schema. for (var it = xmp.GetRoot().IterateChildren(); it.HasNext();) { var currSchema = (XmpNode)it.Next(); switch (currSchema.Name) { case XmpConstants.NsDC: { NormalizeDcArrays(currSchema); break; } case XmpConstants.NsExif: { // Do a special case fix for exif:GPSTimeStamp. FixGpsTimeStamp(currSchema); /*var arrayNode = XmpNodeUtils.FindChildNode(currSchema, "exif:UserComment", false); * if (arrayNode != null) * { * RepairAltText(arrayNode); * }*/ var userComment = XmpNodeUtils.FindChildNode(currSchema, "exif:UserComment", false); if (userComment != null) { if (userComment.Options.IsSimple) { XmpNode newNode = new XmpNode(XmpConstants.ArrayItemName, userComment.Value, userComment.Options); newNode.Parent = userComment; int QualNo = userComment.GetQualifierLength(); while (QualNo > 0) { newNode.AddQualifier(userComment.GetQualifier(userComment.GetQualifierLength() - QualNo)); --QualNo; } userComment.RemoveQualifiers(); if (!newNode.Options.HasLanguage) { var po = new PropertyOptions(); po.SetOption(PropertyOptions.HasQualifiersFlag, true); XmpNode langQual = new XmpNode("xml:lang", "x-default", po); newNode.AddQualifier(langQual); newNode.Options.SetOption(PropertyOptions.HasQualifiersFlag, true); newNode.Options.SetOption(PropertyOptions.HasLanguageFlag, true); } userComment.AddChild(newNode); userComment.Options = new PropertyOptions(PropertyOptions.ArrayFlag | PropertyOptions.ArrayOrderedFlag | PropertyOptions.ArrayAltTextFlag | PropertyOptions.ArrayAlternateFlag); userComment.Value = ""; } RepairAltText(userComment); } break; } case XmpConstants.NsDm: { // Do a special case migration of xmpDM:copyright to // dc:rights['x-default']. var dmCopyright = XmpNodeUtils.FindChildNode(currSchema, "xmpDM:copyright", false); if (dmCopyright != null) { MigrateAudioCopyright(xmp, dmCopyright); } break; } case XmpConstants.NsXmpRights: { var arrayNode = XmpNodeUtils.FindChildNode(currSchema, "xmpRights:UsageTerms", false); if (arrayNode != null) { RepairAltText(arrayNode); } break; } } } }
/// <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); } } } } } } }
/// <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); }
/// <param name="xmp">The XMP object containing the array to be catenated.</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="separator"> /// The string to be used to separate the items in the catenated /// string. Defaults to "; ", ASCII semicolon and space /// (U+003B, U+0020). /// </param> /// <param name="quotes"> /// The characters to be used as quotes around array items that /// contain a separator. Defaults to '"' /// </param> /// <param name="allowCommas">Option flag to control the catenation.</param> /// <returns>Returns the string containing the catenated array items.</returns> /// <exception cref="XmpException">Forwards the Exceptions from the metadata processing</exception> public static string CatenateArrayItems(IXmpMeta xmp, string schemaNs, string arrayName, string separator, string quotes, bool allowCommas) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(arrayName); ParameterAsserts.AssertImplementation(xmp); if (string.IsNullOrEmpty(separator)) { separator = "; "; } if (string.IsNullOrEmpty(quotes)) { quotes = "\""; } var xmpImpl = (XmpMeta)xmp; // Return an empty result if the array does not exist, // hurl if it isn't the right form. var arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName); var arrayNode = XmpNodeUtils.FindNode(xmpImpl.GetRoot(), arrayPath, false, null); if (arrayNode == null) { return(string.Empty); } if (!arrayNode.Options.IsArray || arrayNode.Options.IsArrayAlternate) { throw new XmpException("Named property must be non-alternate array", XmpErrorCode.BadParam); } // Make sure the separator is OK. CheckSeparator(separator); // Make sure the open and close quotes are a legitimate pair. var openQuote = quotes[0]; var closeQuote = CheckQuotes(quotes, openQuote); // Build the result, quoting the array items, adding separators. // Hurl if any item isn't simple. var catenatedString = new StringBuilder(); for (var it = arrayNode.IterateChildren(); it.HasNext();) { var currItem = (XmpNode)it.Next(); if (currItem.Options.IsCompositeProperty) { throw new XmpException("Array items must be simple", XmpErrorCode.BadParam); } var str = ApplyQuotes(currItem.Value, openQuote, closeQuote, allowCommas); catenatedString.Append(str); if (it.HasNext()) { catenatedString.Append(separator); } } return(catenatedString.ToString()); }