/// <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. } }
/// <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>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>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; } } } }
/// <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="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); } } } } } } }