/// <summary> /// Prepares the next node to return if not already done. /// </summary> /// <seealso cref= Iterator#hasNext() </seealso> public override bool MoveNext() { if (_outerInstance._skipSiblings) { return(false); } if (_childrenIterator.MoveNext()) { XmpNode child = (XmpNode)_childrenIterator.Current; if (child != null) { _index++; string path = null; if (child.Options.SchemaNode) { _outerInstance.BaseNs = child.Name; } else if (child.Parent != null) { // for all but the root node and schema nodes path = AccumulatePath(child, _parentPath, _index); } // report next property, skip not-leaf nodes in case options is set if (!_outerInstance.Options.JustLeafnodes || !child.HasChildren()) { ReturnProperty = CreatePropertyInfo(child, _outerInstance.BaseNs, path); return(true); } } return(MoveNext()); } return(false); }
/// <summary> /// Deletes the the given node and its children from its parent. /// Takes care about adjusting the flags. </summary> /// <param name="node"> the top-most node to delete. </param> internal static void DeleteNode(XmpNode node) { XmpNode parent = node.Parent; if (node.Options.Qualifier) { // root is qualifier parent.RemoveQualifier(node); } else { // root is NO qualifier parent.RemoveChild(node); } // delete empty Schema nodes if (!parent.HasChildren() && parent.Options.SchemaNode) { parent.Parent.RemoveChild(parent); } }
/// <summary> /// See if an array is an alt-text array. If so, make sure the x-default item /// is first. /// </summary> /// <param name="arrayNode"> /// the array node to check if its an alt-text array </param> internal static void DetectAltText(XmpNode arrayNode) { if (arrayNode.Options.ArrayAlternate && arrayNode.HasChildren()) { bool isAltText = false; for (IEnumerator it = arrayNode.IterateChildren(); it.MoveNext();) { XmpNode child = (XmpNode)it.Current; if (child != null && child.Options != null && child.Options.HasLanguage) { isAltText = true; break; } } if (isAltText) { arrayNode.Options.ArrayAltText = true; NormalizeLangArray(arrayNode); } } }
/// <summary> /// Remove all schema children according to the flag /// <code>doAllProperties</code>. Empty schemas are automatically remove /// by <code>XMPNode</code> /// </summary> /// <param name="schemaNode"> /// a schema node </param> /// <param name="doAllProperties"> /// flag if all properties or only externals shall be removed. </param> /// <returns> Returns true if the schema is empty after the operation. </returns> private static bool RemoveSchemaChildren(XmpNode schemaNode, bool doAllProperties) { ArrayList currPropsToRemove = new ArrayList(); for (IEnumerator it = schemaNode.IterateChildren(); it.MoveNext();) { XmpNode currProp = (XmpNode)it.Current; if (currProp == null) { continue; } if (doAllProperties || !Utils.IsInternalProperty(schemaNode.Name, currProp.Name)) { currPropsToRemove.Add(currProp); } } foreach (XmpNode xmpNode in currPropsToRemove) { schemaNode.Children.Remove(xmpNode); } currPropsToRemove.Clear(); return(!schemaNode.HasChildren()); }
/// <summary> /// 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. /// /// <blockquote> /// /// <pre> /// <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> /// </pre> /// /// </blockquote> /// </summary> /// <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="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) { bool emitEndTag = true; bool indentEndTag = true; // Determine the XML element name. Open the start tag with the name and // attribute qualifiers. string elemName = node.Name; if (emitAsRdfValue) { elemName = "rdf:value"; } else if (XmpConst.ARRAY_ITEM_NAME.Equals(elemName)) { elemName = "rdf:li"; } WriteIndent(indent); Write('<'); Write(elemName); bool hasGeneralQualifiers = false; bool hasRdfResourceQual = false; for (IEnumerator it = node.IterateQualifier(); it.MoveNext();) { XmpNode qualifier = (XmpNode) it.Current; if (qualifier != null) { if (!RDF_ATTR_QUALIFIER.Contains(qualifier.Name)) { hasGeneralQualifiers = true; } else { hasRdfResourceQual = "rdf:resource".Equals(qualifier.Name); 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", XmpError.BADRDF); } // Change serialization to canonical format with inner rdf:Description-tag // depending on option if (useCanonicalRdf) { Write(">"); WriteNewline(); indent++; WriteIndent(indent); Write(RDF_STRUCT_START); Write(">"); } else { Write(" rdf:parseType=\"Resource\">"); } WriteNewline(); SerializeCanonicalRdfProperty(node, useCanonicalRdf, true, indent + 1); for (IEnumerator it = node.IterateQualifier(); it.MoveNext();) { XmpNode qualifier = (XmpNode) it.Current; if (qualifier != null && !RDF_ATTR_QUALIFIER.Contains(qualifier.Name)) { SerializeCanonicalRdfProperty(qualifier, useCanonicalRdf, false, indent + 1); } } if (useCanonicalRdf) { WriteIndent(indent); Write(RDF_STRUCT_END); WriteNewline(); indent--; } } else { // This node has no general qualifiers. Emit using an unqualified form. if (!node.Options.CompositeProperty) { // This is a simple property. if (node.Options.Uri) { Write(" rdf:resource=\""); AppendNodeValue(node.Value, true); Write("\"/>"); WriteNewline(); emitEndTag = false; } else if (node.Value == null || "".Equals(node.Value)) { Write("/>"); WriteNewline(); emitEndTag = false; } else { Write('>'); AppendNodeValue(node.Value, false); indentEndTag = false; } } else if (node.Options.Array) { // This is an array. Write('>'); WriteNewline(); EmitRdfArrayTag(node, true, indent + 1); if (node.Options.ArrayAltText) { XmpNodeUtils.NormalizeLangArray(node); } for (IEnumerator it = node.IterateChildren(); it.MoveNext();) { XmpNode child = (XmpNode) it.Current; 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(RDF_EMPTY_STRUCT); } 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(RDF_STRUCT_START); Write(">"); } else { Write(" rdf:parseType=\"Resource\">"); } WriteNewline(); for (IEnumerator it = node.IterateChildren(); it.MoveNext();) { XmpNode child = (XmpNode) it.Current; SerializeCanonicalRdfProperty(child, useCanonicalRdf, false, indent + 1); } if (useCanonicalRdf) { WriteIndent(indent); Write(RDF_STRUCT_END); WriteNewline(); indent--; } } } else { // This is a struct with an rdf:resource attribute, use the // "empty property element" form. for (IEnumerator it = node.IterateChildren(); it.MoveNext();) { XmpNode child = (XmpNode) it.Current; if (child != null) { if (!canBeRDFAttrProp(child)) { throw new XmpException("Can't mix rdf:resource and complex fields", XmpError.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> /// Serializes a struct property. /// </summary> /// <param name="node"> an XMPNode </param> /// <param name="indent"> the current indent level </param> /// <param name="hasRdfResourceQual"> Flag if the element has resource qualifier </param> /// <returns> Returns true if an end flag shall be emitted. </returns> /// <exception cref="IOException"> Forwards the writer exceptions. </exception> /// <exception cref="XmpException"> If qualifier and element fields are mixed. </exception> private bool SerializeCompactRdfStructProp(XmpNode node, int indent, bool hasRdfResourceQual) { // This must be a struct. bool hasAttrFields = false; bool hasElemFields = false; bool emitEndTag = true; for (IEnumerator ic = node.IterateChildren(); ic.MoveNext();) { XmpNode field = (XmpNode) ic.Current; if (field == null) continue; if (canBeRDFAttrProp(field)) { hasAttrFields = true; } else { hasElemFields = true; } if (hasAttrFields && hasElemFields) { break; // No sense looking further. } } if (hasRdfResourceQual && hasElemFields) { throw new XmpException("Can't mix rdf:resource qualifier and element fields", XmpError.BADRDF); } if (!node.HasChildren()) { // Catch an empty struct as a special case. The case // below would emit an empty // XML element, which gets reparsed as a simple property // with an empty value. Write(" rdf:parseType=\"Resource\"/>"); WriteNewline(); emitEndTag = false; } else if (!hasElemFields) { // All fields can be attributes, use the // emptyPropertyElt form. SerializeCompactRdfAttrProps(node, indent + 1); Write("/>"); WriteNewline(); emitEndTag = false; } else if (!hasAttrFields) { // All fields must be elements, use the // parseTypeResourcePropertyElt form. Write(" rdf:parseType=\"Resource\">"); WriteNewline(); SerializeCompactRdfElementProps(node, indent + 1); } else { // Have a mix of attributes and elements, use an inner rdf:Description. Write('>'); WriteNewline(); WriteIndent(indent + 1); Write(RDF_STRUCT_START); SerializeCompactRdfAttrProps(node, indent + 2); Write(">"); WriteNewline(); SerializeCompactRdfElementProps(node, indent + 1); WriteIndent(indent + 1); Write(RDF_STRUCT_END); WriteNewline(); } return emitEndTag; }
/// <summary> /// Writes the array start and end tags. /// </summary> /// <param name="arrayNode"> an array node </param> /// <param name="isStartTag"> flag if its the start or end tag </param> /// <param name="indent"> the current indent level </param> /// <exception cref="IOException"> forwards writer exceptions </exception> private void EmitRdfArrayTag(XmpNode arrayNode, bool isStartTag, int indent) { if (isStartTag || arrayNode.HasChildren()) { WriteIndent(indent); Write(isStartTag ? "<rdf:" : "</rdf:"); if (arrayNode.Options.ArrayAlternate) { Write("Alt"); } else if (arrayNode.Options.ArrayOrdered) { Write("Seq"); } else { Write("Bag"); } if (isStartTag && !arrayNode.HasChildren()) { Write("/>"); } else { Write(">"); } WriteNewline(); } }
/// <seealso cref= XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean) </seealso> /// <param name="destXmp"> The destination XMP object. </param> /// <param name="sourceNode"> the source node </param> /// <param name="destParent"> the parent of the destination node </param> /// <param name="replaceOldValues"> Replace the values of existing properties. </param> /// <param name="deleteEmptyValues"> flag if properties with empty values should be deleted /// in the destination object. </param> /// <exception cref="XmpException"> </exception> private static void AppendSubtree(XmpMetaImpl destXmp, XmpNode sourceNode, XmpNode destParent, bool replaceOldValues, bool deleteEmptyValues) { XmpNode destNode = XmpNodeUtils.FindChildNode(destParent, sourceNode.Name, false); bool valueIsEmpty = false; if (deleteEmptyValues) { valueIsEmpty = sourceNode.Options.Simple ? string.IsNullOrEmpty(sourceNode.Value) : !sourceNode.HasChildren(); } if (deleteEmptyValues && valueIsEmpty) { if (destNode != null) { destParent.RemoveChild(destNode); } } else if (destNode == null) { // The one easy case, the destination does not exist. destParent.AddChild((XmpNode) sourceNode.Clone()); } else if (replaceOldValues) { // The destination exists and should be replaced. destXmp.SetNode(destNode, sourceNode.Value, sourceNode.Options, true); destParent.RemoveChild(destNode); destNode = (XmpNode) sourceNode.Clone(); destParent.AddChild(destNode); } else { // The destination exists and is not totally replaced. Structs and // arrays are merged. PropertyOptions sourceForm = sourceNode.Options; PropertyOptions destForm = destNode.Options; if (sourceForm != destForm) { return; } if (sourceForm.Struct) { // To merge a struct process the fields recursively. E.g. add simple missing fields. // The recursive call to AppendSubtree will handle deletion for fields with empty // values. for (IEnumerator it = sourceNode.IterateChildren(); it.MoveNext();) { XmpNode sourceField = (XmpNode) it.Current; if (sourceField == null) continue; AppendSubtree(destXmp, sourceField, destNode, replaceOldValues, deleteEmptyValues); if (deleteEmptyValues && !destNode.HasChildren()) { destParent.RemoveChild(destNode); } } } else if (sourceForm.ArrayAltText) { // Merge AltText arrays by the "xml:lang" qualifiers. Make sure x-default is first. // Make a special check for deletion of empty values. Meaningful in AltText arrays // because the "xml:lang" qualifier provides unambiguous source/dest correspondence. for (IEnumerator it = sourceNode.IterateChildren(); it.MoveNext();) { XmpNode sourceItem = (XmpNode) it.Current; if (sourceItem == null) continue; if (!sourceItem.HasQualifier() || !XML_LANG.Equals(sourceItem.GetQualifier(1).Name)) { continue; } int destIndex = XmpNodeUtils.LookupLanguageItem(destNode, sourceItem.GetQualifier(1).Value); if (deleteEmptyValues && (string.IsNullOrEmpty(sourceItem.Value))) { if (destIndex != -1) { destNode.RemoveChild(destIndex); if (!destNode.HasChildren()) { destParent.RemoveChild(destNode); } } } else if (destIndex == -1) { // Not replacing, keep the existing item. if (!X_DEFAULT.Equals(sourceItem.GetQualifier(1).Value) || !destNode.HasChildren()) { sourceItem.CloneSubtree(destNode); } else { XmpNode destItem = new XmpNode(sourceItem.Name, sourceItem.Value, sourceItem.Options); sourceItem.CloneSubtree(destItem); destNode.AddChild(1, destItem); } } } } else if (sourceForm.Array) { // Merge other arrays by item values. Don't worry about order or duplicates. Source // items with empty values do not cause deletion, that conflicts horribly with // merging. for (IEnumerator @is = sourceNode.IterateChildren(); @is.MoveNext();) { XmpNode sourceItem = (XmpNode) @is.Current; if (sourceItem == null) continue; bool match = false; for (IEnumerator id = destNode.IterateChildren(); id.MoveNext();) { XmpNode destItem = (XmpNode) id.Current; if (destItem == null) continue; if (ItemValuesMatch(sourceItem, destItem)) { match = true; } } if (!match) { destNode = (XmpNode) sourceItem.Clone(); destParent.AddChild(destNode); } } } } }
/// <summary> /// Remove all schema children according to the flag /// <code>doAllProperties</code>. Empty schemas are automatically remove /// by <code>XMPNode</code> /// </summary> /// <param name="schemaNode"> /// a schema node </param> /// <param name="doAllProperties"> /// flag if all properties or only externals shall be removed. </param> /// <returns> Returns true if the schema is empty after the operation. </returns> private static bool RemoveSchemaChildren(XmpNode schemaNode, bool doAllProperties) { ArrayList currPropsToRemove = new ArrayList(); for (IEnumerator it = schemaNode.IterateChildren(); it.MoveNext();) { XmpNode currProp = (XmpNode) it.Current; if (currProp == null) continue; if (doAllProperties || !Utils.IsInternalProperty(schemaNode.Name, currProp.Name)) { currPropsToRemove.Add(currProp); } } foreach (XmpNode xmpNode in currPropsToRemove) { schemaNode.Children.Remove(xmpNode); } currPropsToRemove.Clear(); return !schemaNode.HasChildren(); }
/// <summary> /// <ol> /// <li>Look for an exact match with the specific language. /// <li>If a generic language is given, look for partial matches. /// <li>Look for an "x-default"-item. /// <li>Choose the first item. /// </ol> /// </summary> /// <param name="arrayNode"> /// the alt text array node </param> /// <param name="genericLang"> /// the generic language </param> /// <param name="specificLang"> /// the specific language </param> /// <returns> Returns the kind of match as an Integer and the found node in an /// array. /// </returns> /// <exception cref="XmpException"> </exception> internal static object[] ChooseLocalizedText(XmpNode arrayNode, string genericLang, string specificLang) { // See if the array has the right form. Allow empty alt arrays, // that is what parsing returns. if (!arrayNode.Options.ArrayAltText) { throw new XmpException("Localized text array is not alt-text", XmpError.BADXPATH); } if (!arrayNode.HasChildren()) { return new object[] {CLT_NO_VALUES, null}; } int foundGenericMatches = 0; XmpNode resultNode = null; XmpNode xDefault = null; // Look for the first partial match with the generic language. for (IEnumerator it = arrayNode.IterateChildren(); it.MoveNext();) { XmpNode currItem = (XmpNode) it.Current; // perform some checks on the current item if (currItem == null || currItem.Options == null || currItem.Options.CompositeProperty) { throw new XmpException("Alt-text array item is not simple", XmpError.BADXPATH); } if (!currItem.HasQualifier() || !XML_LANG.Equals(currItem.GetQualifier(1).Name)) { throw new XmpException("Alt-text array item has no language qualifier", XmpError.BADXPATH); } string currLang = currItem.GetQualifier(1).Value; // Look for an exact match with the specific language. if (specificLang.Equals(currLang)) { return new object[] {CLT_SPECIFIC_MATCH, currItem}; } if (genericLang != null && currLang.StartsWith(genericLang)) { if (resultNode == null) { resultNode = currItem; } // ! Don't return/break, need to look for other matches. foundGenericMatches++; } else if (X_DEFAULT.Equals(currLang)) { xDefault = currItem; } } // evaluate loop if (foundGenericMatches == 1) { return new object[] {CLT_SINGLE_GENERIC, resultNode}; } if (foundGenericMatches > 1) { return new object[] {CLT_MULTIPLE_GENERIC, resultNode}; } if (xDefault != null) { return new object[] {CLT_XDEFAULT, xDefault}; } { // Everything failed, choose the first item. return new object[] {CLT_FIRST_ITEM, arrayNode.GetChild(1)}; } }
/// <seealso cref= XMPUtils#removeProperties(XMPMeta, String, String, boolean, boolean) /// </seealso> /// <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); XmpMetaImpl xmpImpl = (XmpMetaImpl)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", XmpError.BADPARAM); } XmpPath expPath = XmpPathParser.ExpandXPath(schemaNs, propName); XmpNode propNode = XmpNodeUtils.FindNode(xmpImpl.Root, expPath, false, null); if (propNode != null) { if (doAllProperties || !Utils.IsInternalProperty(expPath.GetSegment((int)XmpPath.STEP_SCHEMA).Name, expPath.GetSegment((int)XmpPath.STEP_ROOT_PROP).Name)) { XmpNode parent = propNode.Parent; parent.RemoveChild(propNode); if (parent.Options.SchemaNode && !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; XmpNode schemaNode = XmpNodeUtils.FindSchemaNode(xmpImpl.Root, schemaNs, false); if (schemaNode != null) { if (RemoveSchemaChildren(schemaNode, doAllProperties)) { xmpImpl.Root.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. IXmpAliasInfo[] aliases = XmpMetaFactory.SchemaRegistry.FindAliases(schemaNs); for (int i = 0; i < aliases.Length; i++) { IXmpAliasInfo info = aliases[i]; XmpPath path = XmpPathParser.ExpandXPath(info.Namespace, info.PropName); XmpNode actualProp = XmpNodeUtils.FindNode(xmpImpl.Root, path, false, null); if (actualProp != null) { XmpNode parent = actualProp.Parent; 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. ArrayList schemasToRemove = new ArrayList(); for (IEnumerator it = xmpImpl.Root.IterateChildren(); it.MoveNext();) { XmpNode schema = (XmpNode)it.Current; if (schema == null) { continue; } if (RemoveSchemaChildren(schema, doAllProperties)) { schemasToRemove.Add(schema); } } foreach (XmpNode xmpNode in schemasToRemove) { xmpImpl.Root.Children.Remove(xmpNode); } schemasToRemove.Clear(); } }
/// <summary> /// <ol> /// <li>Look for an exact match with the specific language. /// <li>If a generic language is given, look for partial matches. /// <li>Look for an "x-default"-item. /// <li>Choose the first item. /// </ol> /// </summary> /// <param name="arrayNode"> /// the alt text array node </param> /// <param name="genericLang"> /// the generic language </param> /// <param name="specificLang"> /// the specific language </param> /// <returns> Returns the kind of match as an Integer and the found node in an /// array. /// </returns> /// <exception cref="XmpException"> </exception> internal static object[] ChooseLocalizedText(XmpNode arrayNode, string genericLang, string specificLang) { // See if the array has the right form. Allow empty alt arrays, // that is what parsing returns. if (!arrayNode.Options.ArrayAltText) { throw new XmpException("Localized text array is not alt-text", XmpError.BADXPATH); } if (!arrayNode.HasChildren()) { return(new object[] { CLT_NO_VALUES, null }); } int foundGenericMatches = 0; XmpNode resultNode = null; XmpNode xDefault = null; // Look for the first partial match with the generic language. for (IEnumerator it = arrayNode.IterateChildren(); it.MoveNext();) { XmpNode currItem = (XmpNode)it.Current; // perform some checks on the current item if (currItem == null || currItem.Options == null || currItem.Options.CompositeProperty) { throw new XmpException("Alt-text array item is not simple", XmpError.BADXPATH); } if (!currItem.HasQualifier() || !XML_LANG.Equals(currItem.GetQualifier(1).Name)) { throw new XmpException("Alt-text array item has no language qualifier", XmpError.BADXPATH); } string currLang = currItem.GetQualifier(1).Value; // Look for an exact match with the specific language. if (specificLang.Equals(currLang)) { return(new object[] { CLT_SPECIFIC_MATCH, currItem }); } if (genericLang != null && currLang.StartsWith(genericLang)) { if (resultNode == null) { resultNode = currItem; } // ! Don't return/break, need to look for other matches. foundGenericMatches++; } else if (X_DEFAULT.Equals(currLang)) { xDefault = currItem; } } // evaluate loop if (foundGenericMatches == 1) { return(new object[] { CLT_SINGLE_GENERIC, resultNode }); } if (foundGenericMatches > 1) { return(new object[] { CLT_MULTIPLE_GENERIC, resultNode }); } if (xDefault != null) { return(new object[] { CLT_XDEFAULT, xDefault }); } { // Everything failed, choose the first item. return(new object[] { CLT_FIRST_ITEM, arrayNode.GetChild(1) }); } }
/// <seealso cref= XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean) </seealso> /// <param name="destXmp"> The destination XMP object. </param> /// <param name="sourceNode"> the source node </param> /// <param name="destParent"> the parent of the destination node </param> /// <param name="replaceOldValues"> Replace the values of existing properties. </param> /// <param name="deleteEmptyValues"> flag if properties with empty values should be deleted /// in the destination object. </param> /// <exception cref="XmpException"> </exception> private static void AppendSubtree(XmpMetaImpl destXmp, XmpNode sourceNode, XmpNode destParent, bool replaceOldValues, bool deleteEmptyValues) { XmpNode destNode = XmpNodeUtils.FindChildNode(destParent, sourceNode.Name, false); bool valueIsEmpty = false; if (deleteEmptyValues) { valueIsEmpty = sourceNode.Options.Simple ? string.IsNullOrEmpty(sourceNode.Value) : !sourceNode.HasChildren(); } if (deleteEmptyValues && valueIsEmpty) { if (destNode != null) { destParent.RemoveChild(destNode); } } else if (destNode == null) { // The one easy case, the destination does not exist. destParent.AddChild((XmpNode)sourceNode.Clone()); } else if (replaceOldValues) { // The destination exists and should be replaced. destXmp.SetNode(destNode, sourceNode.Value, sourceNode.Options, true); destParent.RemoveChild(destNode); destNode = (XmpNode)sourceNode.Clone(); destParent.AddChild(destNode); } else { // The destination exists and is not totally replaced. Structs and // arrays are merged. PropertyOptions sourceForm = sourceNode.Options; PropertyOptions destForm = destNode.Options; if (sourceForm != destForm) { return; } if (sourceForm.Struct) { // To merge a struct process the fields recursively. E.g. add simple missing fields. // The recursive call to AppendSubtree will handle deletion for fields with empty // values. for (IEnumerator it = sourceNode.IterateChildren(); it.MoveNext();) { XmpNode sourceField = (XmpNode)it.Current; if (sourceField == null) { continue; } AppendSubtree(destXmp, sourceField, destNode, replaceOldValues, deleteEmptyValues); if (deleteEmptyValues && !destNode.HasChildren()) { destParent.RemoveChild(destNode); } } } else if (sourceForm.ArrayAltText) { // Merge AltText arrays by the "xml:lang" qualifiers. Make sure x-default is first. // Make a special check for deletion of empty values. Meaningful in AltText arrays // because the "xml:lang" qualifier provides unambiguous source/dest correspondence. for (IEnumerator it = sourceNode.IterateChildren(); it.MoveNext();) { XmpNode sourceItem = (XmpNode)it.Current; if (sourceItem == null) { continue; } if (!sourceItem.HasQualifier() || !XML_LANG.Equals(sourceItem.GetQualifier(1).Name)) { continue; } int destIndex = XmpNodeUtils.LookupLanguageItem(destNode, sourceItem.GetQualifier(1).Value); if (deleteEmptyValues && (string.IsNullOrEmpty(sourceItem.Value))) { if (destIndex != -1) { destNode.RemoveChild(destIndex); if (!destNode.HasChildren()) { destParent.RemoveChild(destNode); } } } else if (destIndex == -1) { // Not replacing, keep the existing item. if (!X_DEFAULT.Equals(sourceItem.GetQualifier(1).Value) || !destNode.HasChildren()) { sourceItem.CloneSubtree(destNode); } else { XmpNode destItem = new XmpNode(sourceItem.Name, sourceItem.Value, sourceItem.Options); sourceItem.CloneSubtree(destItem); destNode.AddChild(1, destItem); } } } } else if (sourceForm.Array) { // Merge other arrays by item values. Don't worry about order or duplicates. Source // items with empty values do not cause deletion, that conflicts horribly with // merging. for (IEnumerator @is = sourceNode.IterateChildren(); @is.MoveNext();) { XmpNode sourceItem = (XmpNode)@is.Current; if (sourceItem == null) { continue; } bool match = false; for (IEnumerator id = destNode.IterateChildren(); id.MoveNext();) { XmpNode destItem = (XmpNode)id.Current; if (destItem == null) { continue; } if (ItemValuesMatch(sourceItem, destItem)) { match = true; } } if (!match) { destNode = (XmpNode)sourceItem.Clone(); destParent.AddChild(destNode); } } } } }
/// <seealso cref= XMPMeta#setLocalizedText(String, String, String, String, String, /// PropertyOptions) </seealso> public virtual void SetLocalizedText(string schemaNs, string altTextName, string genericLang, string specificLang, string itemValue, PropertyOptions options) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(altTextName); ParameterAsserts.AssertSpecificLang(specificLang); genericLang = genericLang != null?Utils.NormalizeLangValue(genericLang) : null; specificLang = Utils.NormalizeLangValue(specificLang); XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, altTextName); // Find the array node and set the options if it was just created. XmpNode arrayNode = XmpNodeUtils.FindNode(_tree, arrayPath, true, new PropertyOptions(PropertyOptions.ARRAY | PropertyOptions.ARRAY_ORDERED | PropertyOptions.ARRAY_ALTERNATE | PropertyOptions.ARRAY_ALT_TEXT)); if (arrayNode == null) { throw new XmpException("Failed to find or create array node", XmpError.BADXPATH); } if (!arrayNode.Options.ArrayAltText) { if (!arrayNode.HasChildren() && arrayNode.Options.ArrayAlternate) { arrayNode.Options.ArrayAltText = true; } else { throw new XmpException("Specified property is no alt-text array", XmpError.BADXPATH); } } // Make sure the x-default item, if any, is first. bool haveXDefault = false; XmpNode xdItem = null; foreach (XmpNode currItem in arrayNode.Children) { if (!currItem.HasQualifier() || !XML_LANG.Equals(currItem.GetQualifier(1).Name)) { throw new XmpException("Language qualifier must be first", XmpError.BADXPATH); } if (X_DEFAULT.Equals(currItem.GetQualifier(1).Value)) { xdItem = currItem; haveXDefault = true; break; } } // Moves x-default to the beginning of the array if (xdItem != null && arrayNode.ChildrenLength > 1) { arrayNode.RemoveChild(xdItem); arrayNode.AddChild(1, xdItem); } // Find the appropriate item. // chooseLocalizedText will make sure the array is a language // alternative. object[] result = XmpNodeUtils.ChooseLocalizedText(arrayNode, genericLang, specificLang); int match = (int)((int?)result[0]); XmpNode itemNode = (XmpNode)result[1]; bool specificXDefault = X_DEFAULT.Equals(specificLang); switch (match) { case XmpNodeUtils.CLT_NO_VALUES: // Create the array items for the specificLang and x-default, with // x-default first. XmpNodeUtils.AppendLangItem(arrayNode, X_DEFAULT, itemValue); haveXDefault = true; if (!specificXDefault) { XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); } break; case XmpNodeUtils.CLT_SPECIFIC_MATCH: if (!specificXDefault) { // Update the specific item, update x-default if it matches the // old value. if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.Value.Equals(itemNode.Value)) { xdItem.Value = itemValue; } // ! Do this after the x-default check! itemNode.Value = itemValue; } else { // Update all items whose values match the old x-default value. Debug.Assert(haveXDefault && xdItem == itemNode); foreach (XmpNode currItem in arrayNode.Children) { if (currItem == xdItem || !currItem.Value.Equals(xdItem != null ? xdItem.Value : null)) { continue; } currItem.Value = itemValue; } // And finally do the x-default item. if (xdItem != null) { xdItem.Value = itemValue; } } break; case XmpNodeUtils.CLT_SINGLE_GENERIC: // Update the generic item, update x-default if it matches the old // value. if (haveXDefault && xdItem != itemNode && xdItem != null && xdItem.Value.Equals(itemNode.Value)) { xdItem.Value = itemValue; } itemNode.Value = itemValue; // ! Do this after // the x-default // check! break; case XmpNodeUtils.CLT_MULTIPLE_GENERIC: // Create the specific language, ignore x-default. XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); if (specificXDefault) { haveXDefault = true; } break; case XmpNodeUtils.CLT_XDEFAULT: // Create the specific language, update x-default if it was the only // item. if (xdItem != null && arrayNode.ChildrenLength == 1) { xdItem.Value = itemValue; } XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); break; case XmpNodeUtils.CLT_FIRST_ITEM: // Create the specific language, don't add an x-default item. XmpNodeUtils.AppendLangItem(arrayNode, specificLang, itemValue); if (specificXDefault) { haveXDefault = true; } break; default: // does not happen under normal circumstances throw new XmpException("Unexpected result from ChooseLocalizedText", XmpError.INTERNALFAILURE); } // Add an x-default at the front if needed. if (!haveXDefault && arrayNode.ChildrenLength == 1) { XmpNodeUtils.AppendLangItem(arrayNode, X_DEFAULT, itemValue); } }
/// <summary> /// See if an array is an alt-text array. If so, make sure the x-default item /// is first. /// </summary> /// <param name="arrayNode"> /// the array node to check if its an alt-text array </param> internal static void DetectAltText(XmpNode arrayNode) { if (arrayNode.Options.ArrayAlternate && arrayNode.HasChildren()) { bool isAltText = false; for (IEnumerator it = arrayNode.IterateChildren(); it.MoveNext();) { XmpNode child = (XmpNode) it.Current; if (child != null && child.Options != null && child.Options.HasLanguage) { isAltText = true; break; } } if (isAltText) { arrayNode.Options.ArrayAltText = true; NormalizeLangArray(arrayNode); } } }
/// <summary> /// The parent is an RDF pseudo-struct containing an rdf:value field. Fix the /// XMP data model. The rdf:value node must be the first child, the other /// children are qualifiers. The form, value, and children of the rdf:value /// node are the real ones. The rdf:value node's qualifiers must be added to /// the others. /// </summary> /// <param name="xmpParent"> the parent xmp node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void FixupQualifiedNode(XmpNode xmpParent) { Debug.Assert(xmpParent.Options.Struct && xmpParent.HasChildren()); XmpNode valueNode = xmpParent.GetChild(1); Debug.Assert("rdf:value".Equals(valueNode.Name)); // Move the qualifiers on the value node to the parent. // Make sure an xml:lang qualifier stays at the front. // Check for duplicate names between the value node's qualifiers and the parent's children. // The parent's children are about to become qualifiers. Check here, between the groups. // Intra-group duplicates are caught by XMPNode#addChild(...). if (valueNode.Options.HasLanguage) { if (xmpParent.Options.HasLanguage) { throw new XmpException("Redundant xml:lang for rdf:value element", XmpError.BADXMP); } XmpNode langQual = valueNode.GetQualifier(1); valueNode.RemoveQualifier(langQual); xmpParent.AddQualifier(langQual); } // Start the remaining copy after the xml:lang qualifier. for (int i = 1; i <= valueNode.QualifierLength; i++) { XmpNode qualifier = valueNode.GetQualifier(i); xmpParent.AddQualifier(qualifier); } // Change the parent's other children into qualifiers. // This loop starts at 1, child 0 is the rdf:value node. for (int i = 2; i <= xmpParent.ChildrenLength; i++) { XmpNode qualifier = xmpParent.GetChild(i); xmpParent.AddQualifier(qualifier); } // Move the options and value last, other checks need the parent's original options. // Move the value node's children to be the parent's children. Debug.Assert(xmpParent.Options.Struct || xmpParent.HasValueChild); xmpParent.HasValueChild = false; xmpParent.Options.Struct = false; xmpParent.Options.MergeWith(valueNode.Options); xmpParent.Value = valueNode.Value; xmpParent.RemoveChildren(); for (IEnumerator it = valueNode.IterateChildren(); it.MoveNext();) { XmpNode child = (XmpNode) it.Current; xmpParent.AddChild(child); } }
/// <summary> /// 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> /// </summary> /// <param name="xmp"> the metadata object </param> /// <param name="dmCopyright"> the "dm:copyright"-property </param> private static void MigrateAudioCopyright(XMPMeta xmp, XmpNode dmCopyright) { try { XmpNode dcSchema = XmpNodeUtils.FindSchemaNode(((XmpMetaImpl)xmp).Root, XmpConst.NS_DC, true); string dmValue = dmCopyright.Value; const string doubleLf = "\n\n"; XmpNode 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(XmpConst.NS_DC, "rights", "", XmpConst.X_DEFAULT, dmValue, null); } else { int xdIndex = XmpNodeUtils.LookupLanguageItem(dcRightsArray, XmpConst.X_DEFAULT); if (xdIndex < 0) { // 2. No x-default item, create from the first item. string firstValue = dcRightsArray.GetChild(1).Value; xmp.SetLocalizedText(XmpConst.NS_DC, "rights", "", XmpConst.X_DEFAULT, firstValue, null); xdIndex = XmpNodeUtils.LookupLanguageItem(dcRightsArray, XmpConst.X_DEFAULT); } // 3. Look for a double linefeed in the x-default value. XmpNode defaultNode = dcRightsArray.GetChild(xdIndex); string defaultValue = defaultNode.Value; int 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) + dmValue; } } } // 4. Get rid of the xmpDM:copyright. dmCopyright.Parent.RemoveChild(dmCopyright); } catch (XmpException) { // Don't let failures (like a bad dc:rights form) stop other // cleanup. } }