/// <summary> /// Serializes an <code>XMPMeta</code>-object as RDF into a byte buffer. /// </summary> /// <param name="xmp"> a metadata implementation object </param> /// <param name="options"> Options to control the serialization (see <seealso cref="SerializeOptions"/>). </param> /// <returns> Returns a byte buffer containing the serialized RDF. </returns> /// <exception cref="XmpException"> on serializsation errors. </exception> public static byte[] SerializeToBuffer(XmpMetaImpl xmp, SerializeOptions options) { MemoryStream @out = new MemoryStream(2048); Serialize(xmp, @out, options); return(@out.GetBuffer()); }
/// <summary> /// Parses the input source into an XMP metadata object, including /// de-aliasing and normalisation. /// </summary> /// <param name="input"> the input can be an <code>InputStream</code>, a <code>String</code> or /// a byte buffer containing the XMP packet. </param> /// <param name="options"> the parse options </param> /// <returns> Returns the resulting XMP metadata object </returns> /// <exception cref="XmpException"> Thrown if parsing or normalisation fails. </exception> public static XMPMeta Parse(object input, ParseOptions options) { ParameterAsserts.AssertNotNull(input); options = options ?? new ParseOptions(); XmlDocument document = ParseXml(input, options); bool xmpmetaRequired = options.RequireXmpMeta; object[] result = new object[3]; result = FindRootNode(document, xmpmetaRequired, result); if (result != null && result[1] == XmpRdf) { XmpMetaImpl xmp = ParseRdf.Parse((XmlNode)result[0]); xmp.PacketHeader = (string)result[2]; // Check if the XMP object shall be normalized if (!options.OmitNormalization) { return(XmpNormalizer.Process(xmp, options)); } return(xmp); } // no appropriate root node found, return empty metadata object return(new XmpMetaImpl()); }
/// <summary> /// Static method to Serialize the metadata object. For each serialisation, a new XMPSerializer /// instance is created, either XMPSerializerRDF or XMPSerializerPlain so thats its possible to /// serialialize the same XMPMeta objects in two threads. /// </summary> /// <param name="xmp"> a metadata implementation object </param> /// <param name="out"> the output stream to Serialize to </param> /// <param name="options"> serialization options, can be <code>null</code> for default. </param> /// <exception cref="XmpException"> </exception> public static void Serialize(XmpMetaImpl xmp, Stream @out, SerializeOptions options) { options = options ?? new SerializeOptions(); // sort the internal data model on demand if (options.Sort) { xmp.Sort(); } (new XmpSerializerRdf()).Serialize(xmp, @out, options); }
/// <summary> /// Constructor with optionsl initial values. If <code>propName</code> is provided, /// <code>schemaNs</code> has also be provided. </summary> /// <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 redurce to this property within the <code>schemaNs</code> </param> /// <param name="options"> advanced iteration options, see <seealso cref="IteratorOptions"/> </param> /// <exception cref="XmpException"> If the node defined by the paramters is not existing. </exception> public XmpIteratorImpl(XmpMetaImpl 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; string initialPath = null; bool baseSchema = !String.IsNullOrEmpty(schemaNs); bool baseProperty = !String.IsNullOrEmpty(propPath); if (!baseSchema && !baseProperty) { // complete tree will be iterated startNode = xmp.Root; } else if (baseSchema && baseProperty) { // Schema and property node provided XmpPath path = XmpPathParser.ExpandXPath(schemaNs, propPath); // base path is the prop path without the property leaf XmpPath basePath = new XmpPath(); for (int i = 0; i < path.Size() - 1; i++) { basePath.Add(path.GetSegment(i)); } startNode = XmpNodeUtils.FindNode(xmp.Root, path, false, null); _baseNs = schemaNs; initialPath = basePath.ToString(); } else if (baseSchema && !baseProperty) { // Only Schema provided startNode = XmpNodeUtils.FindSchemaNode(xmp.Root, schemaNs, false); } else // !baseSchema && baseProperty { // No schema but property provided -> error throw new XmpException("Schema namespace URI is required", XmpError.BADSCHEMA); } // create iterator if (startNode != null) { _nodeIterator = (!_options.JustChildren) ? new NodeIterator(this, startNode, initialPath, 1) : new NodeIteratorChildren(this, startNode, initialPath); } else { // create null iterator _nodeIterator = EmptyList.GetEnumerator(); } }
/// <summary> /// Normalizes a raw parsed XMPMeta-Object </summary> /// <param name="xmp"> the raw metadata object </param> /// <param name="options"> the parsing options </param> /// <returns> Returns the normalized metadata object </returns> /// <exception cref="XmpException"> Collects all severe processing errors. </exception> internal static XMPMeta Process(XmpMetaImpl xmp, ParseOptions options) { XmpNode tree = xmp.Root; TouchUpDataModel(xmp); MoveExplicitAliases(tree, options); TweakOldXmp(tree); DeleteEmptySchemas(tree); return xmp; }
/// <summary> /// Normalizes a raw parsed XMPMeta-Object </summary> /// <param name="xmp"> the raw metadata object </param> /// <param name="options"> the parsing options </param> /// <returns> Returns the normalized metadata object </returns> /// <exception cref="XmpException"> Collects all severe processing errors. </exception> internal static XMPMeta Process(XmpMetaImpl xmp, ParseOptions options) { XmpNode tree = xmp.Root; TouchUpDataModel(xmp); MoveExplicitAliases(tree, options); TweakOldXmp(tree); DeleteEmptySchemas(tree); return(xmp); }
/// <seealso cref= XMPUtils#appendProperties(XMPMeta, XMPMeta, boolean, boolean) </seealso> /// <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); XmpMetaImpl src = (XmpMetaImpl)source; XmpMetaImpl dest = (XmpMetaImpl)destination; for (IEnumerator it = src.Root.IterateChildren(); it.MoveNext();) { XmpNode sourceSchema = (XmpNode)it.Current; if (sourceSchema == null) { continue; } // Make sure we have a destination schema node XmpNode destSchema = XmpNodeUtils.FindSchemaNode(dest.Root, sourceSchema.Name, false); bool createdSchema = false; if (destSchema == null) { PropertyOptions propertyOptions = new PropertyOptions(); propertyOptions.SchemaNode = true; destSchema = new XmpNode(sourceSchema.Name, sourceSchema.Value, propertyOptions); dest.Root.AddChild(destSchema); createdSchema = true; } // Process the source schema's children. for (IEnumerator ic = sourceSchema.IterateChildren(); ic.MoveNext();) { XmpNode sourceProp = (XmpNode)ic.Current; if (sourceProp == null) { continue; } 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.Root.RemoveChild(destSchema); } } }
/// <summary> /// Serializes an <code>XMPMeta</code>-object as RDF into a string. /// <em>Note:</em> Encoding is forced to UTF-16 when serializing to a /// string to ensure the correctness of "exact packet size". /// </summary> /// <param name="xmp"> a metadata implementation object </param> /// <param name="options"> Options to control the serialization (see /// <seealso cref="SerializeOptions"/>). </param> /// <returns> Returns a string containing the serialized RDF. </returns> /// <exception cref="XmpException"> on serializsation errors. </exception> public static string SerializeToString(XmpMetaImpl xmp, SerializeOptions options) { // forces the encoding to be UTF-16 to get the correct string length options = options ?? new SerializeOptions(); options.EncodeUtf16Be = true; MemoryStream @out = new MemoryStream(2048); Serialize(xmp, @out, options); try { return Encoding.GetEncoding(options.Encoding).GetString(@out.GetBuffer()); } catch (Exception) { // cannot happen as UTF-8/16LE/BE is required to be implemented in // Java return GetString(@out.GetBuffer()); } }
/// <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(XmpMetaImpl 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.Root, XmpConst.NS_DC, true); // Do the special case fixes within each schema. IEnumerator it = xmp.Root.IterateChildren(); while (it.MoveNext()) { XmpNode currSchema = (XmpNode)it.Current; if (currSchema != null && XmpConst.NS_DC.Equals(currSchema.Name)) { NormalizeDcArrays(currSchema); } else if (currSchema != null && XmpConst.NS_EXIF.Equals(currSchema.Name)) { // Do a special case fix for exif:GPSTimeStamp. FixGpsTimeStamp(currSchema); XmpNode arrayNode = XmpNodeUtils.FindChildNode(currSchema, "exif:UserComment", false); if (arrayNode != null) { RepairAltText(arrayNode); } } else if (currSchema != null && XmpConst.NS_DM.Equals(currSchema.Name)) { // Do a special case migration of xmpDM:copyright to // dc:rights['x-default']. XmpNode dmCopyright = XmpNodeUtils.FindChildNode(currSchema, "xmpDM:copyright", false); if (dmCopyright != null) { MigrateAudioCopyright(xmp, dmCopyright); } } else if (currSchema != null && XmpConst.NS_XMP_RIGHTS.Equals(currSchema.Name)) { XmpNode arrayNode = XmpNodeUtils.FindChildNode(currSchema, "xmpRights:UsageTerms", false); if (arrayNode != null) { RepairAltText(arrayNode); } } } }
/// <summary> /// Serializes an <code>XMPMeta</code>-object as RDF into a string. /// <em>Note:</em> Encoding is forced to UTF-16 when serializing to a /// string to ensure the correctness of "exact packet size". /// </summary> /// <param name="xmp"> a metadata implementation object </param> /// <param name="options"> Options to control the serialization (see /// <seealso cref="SerializeOptions"/>). </param> /// <returns> Returns a string containing the serialized RDF. </returns> /// <exception cref="XmpException"> on serializsation errors. </exception> public static string SerializeToString(XmpMetaImpl xmp, SerializeOptions options) { // forces the encoding to be UTF-16 to get the correct string length options = options ?? new SerializeOptions(); options.EncodeUtf16Be = true; MemoryStream @out = new MemoryStream(2048); Serialize(xmp, @out, options); try { return(Encoding.GetEncoding(options.Encoding).GetString(@out.GetBuffer())); } catch (Exception) { // cannot happen as UTF-8/16LE/BE is required to be implemented in // Java return(GetString(@out.GetBuffer())); } }
/// <summary> /// Utility to find or create the array used by <code>separateArrayItems()</code>. </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, XmpMetaImpl xmp) { arrayOptions = XmpNodeUtils.VerifySetOptions(arrayOptions, null); if (!arrayOptions.OnlyArrayOptions) { throw new XmpException("Options can only provide array form", XmpError.BADOPTIONS); } // Find the array node, make sure it is OK. Move the current children // aside, to be readded later if kept. XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName); XmpNode arrayNode = XmpNodeUtils.FindNode(xmp.Root, arrayPath, false, null); if (arrayNode != null) { // The array exists, make sure the form is compatible. Zero // arrayForm means take what exists. PropertyOptions arrayForm = arrayNode.Options; if (!arrayForm.Array || arrayForm.ArrayAlternate) { throw new XmpException("Named property must be non-alternate array", XmpError.BADXPATH); } if (arrayOptions.EqualArrayTypes(arrayForm)) { throw new XmpException("Mismatch of specified and existing array form", XmpError.BADXPATH); // *** Right error? } } else { // The array does not exist, try to create it. // don't modify the options handed into the method arrayOptions.Array = true; arrayNode = XmpNodeUtils.FindNode(xmp.Root, arrayPath, true, arrayOptions); if (arrayNode == null) { throw new XmpException("Failed to create named array", XmpError.BADXPATH); } } return(arrayNode); }
/// <summary> /// 7.2.16 literalPropertyElt /// start-element ( URI == propertyElementURIs, /// attributes == set ( idAttr?, datatypeAttr?) ) /// text() /// end-element() /// /// Add a leaf node with the text value and qualifiers for the attributes. </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfLiteralPropertyElement(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, bool isTopLevel) { XmpNode newChild = AddChildNode(xmp, xmpParent, xmlNode, null, isTopLevel); if (xmlNode.Attributes != null) { for (int i = 0; i < xmlNode.Attributes.Count; i++) { XmlNode attribute = xmlNode.Attributes[i]; if ("xmlns".Equals(attribute.Prefix) || (attribute.Prefix == null && "xmlns".Equals(attribute.Name))) { continue; } string attrNs = attribute.NamespaceURI; string attrLocal = attribute.LocalName; if (XML_LANG.Equals(attribute.Name)) { AddQualifierNode(newChild, XML_LANG, attribute.Value); } else if (NS_RDF.Equals(attrNs) && ("ID".Equals(attrLocal) || "datatype".Equals(attrLocal))) { continue; // Ignore all rdf:ID and rdf:datatype attributes. } else throw new XmpException("Invalid attribute for literal property element", XmpError.BADRDF); } } string textValue = ""; for (int i = 0; i < xmlNode.ChildNodes.Count; i++) { XmlNode child = xmlNode.ChildNodes[i]; if (child.NodeType == XmlNodeType.Text) { textValue += child.Value; } else { throw new XmpException("Invalid child of literal property element", XmpError.BADRDF); } } newChild.Value = textValue; }
/// <summary> /// 7.2.15 resourcePropertyElt /// start-element ( URI == propertyElementURIs, attributes == set ( idAttr? ) ) /// ws* nodeElement ws* /// end-element() /// /// This handles structs using an rdf:Description node, /// arrays using rdf:Bag/Seq/Alt, and typedNodes. It also catches and cleans up qualified /// properties written with rdf:Description and rdf:value. /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfResourcePropertyElement(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, bool isTopLevel) { if (isTopLevel && "iX:changes".Equals(xmlNode.Name)) { // Strip old "punchcard" chaff which has on the prefix "iX:". return; } XmpNode newCompound = AddChildNode(xmp, xmpParent, xmlNode, "", isTopLevel); // walk through the attributes if (xmlNode.Attributes != null) { for (int i = 0; i < xmlNode.Attributes.Count; i++) { XmlNode attribute = xmlNode.Attributes[i]; if ("xmlns".Equals(attribute.Prefix) || (attribute.Prefix == null && "xmlns".Equals(attribute.Name))) { continue; } string attrLocal = attribute.LocalName; string attrNs = attribute.NamespaceURI; if (XML_LANG.Equals(attribute.Name)) { AddQualifierNode(newCompound, XML_LANG, attribute.Value); } else if ("ID".Equals(attrLocal) && NS_RDF.Equals(attrNs)) { continue; // Ignore all rdf:ID attributes. } throw new XmpException("Invalid attribute for resource property element", XmpError.BADRDF); } } // walk through the children bool found = false; for (int i = 0; i < xmlNode.ChildNodes.Count; i++) { XmlNode currChild = xmlNode.ChildNodes[i]; if (!IsWhitespaceNode(currChild)) { if (currChild.NodeType == XmlNodeType.Element && !found) { bool isRdf = NS_RDF.Equals(currChild.NamespaceURI); string childLocal = currChild.LocalName; if (isRdf && "Bag".Equals(childLocal)) { newCompound.Options.Array = true; } else if (isRdf && "Seq".Equals(childLocal)) { newCompound.Options.Array = true; newCompound.Options.ArrayOrdered = true; } else if (isRdf && "Alt".Equals(childLocal)) { newCompound.Options.Array = true; newCompound.Options.ArrayOrdered = true; newCompound.Options.ArrayAlternate = true; } else { newCompound.Options.Struct = true; if (!isRdf && !"Description".Equals(childLocal)) { string typeName = currChild.NamespaceURI; if (typeName == null) { throw new XmpException("All XML elements must be in a namespace", XmpError.BADXMP); } typeName += ':' + childLocal; AddQualifierNode(newCompound, "rdf:type", typeName); } } RdfNodeElement(xmp, newCompound, currChild, false); if (newCompound.HasValueChild) { FixupQualifiedNode(newCompound); } else if (newCompound.Options.ArrayAlternate) { XmpNodeUtils.DetectAltText(newCompound); } found = true; } else if (found) { // found second child element throw new XmpException("Invalid child of resource property element", XmpError.BADRDF); } else { throw new XmpException("Children of resource property element must be XML elements", XmpError.BADRDF); } } } if (!found) { // didn't found any child elements throw new XmpException("Missing child of resource property element", XmpError.BADRDF); } }
/// <summary> /// 7.2.14 propertyElt /// /// resourcePropertyElt | literalPropertyElt | parseTypeLiteralPropertyElt | /// parseTypeResourcePropertyElt | parseTypeCollectionPropertyElt | /// parseTypeOtherPropertyElt | emptyPropertyElt /// /// 7.2.15 resourcePropertyElt /// start-element ( URI == propertyElementURIs, attributes == set ( idAttr? ) ) /// ws* nodeElement ws* /// end-element() /// /// 7.2.16 literalPropertyElt /// start-element ( /// URI == propertyElementURIs, attributes == set ( idAttr?, datatypeAttr?) ) /// text() /// end-element() /// /// 7.2.17 parseTypeLiteralPropertyElt /// start-element ( /// URI == propertyElementURIs, attributes == set ( idAttr?, parseLiteral ) ) /// literal /// end-element() /// /// 7.2.18 parseTypeResourcePropertyElt /// start-element ( /// URI == propertyElementURIs, attributes == set ( idAttr?, parseResource ) ) /// propertyEltList /// end-element() /// /// 7.2.19 parseTypeCollectionPropertyElt /// start-element ( /// URI == propertyElementURIs, attributes == set ( idAttr?, parseCollection ) ) /// nodeElementList /// end-element() /// /// 7.2.20 parseTypeOtherPropertyElt /// start-element ( URI == propertyElementURIs, attributes == set ( idAttr?, parseOther ) ) /// propertyEltList /// end-element() /// /// 7.2.21 emptyPropertyElt /// start-element ( URI == propertyElementURIs, /// attributes == set ( idAttr?, ( resourceAttr | nodeIdAttr )?, propertyAttr* ) ) /// end-element() /// /// The various property element forms are not distinguished by the XML element name, /// but by their attributes for the most part. The exceptions are resourcePropertyElt and /// literalPropertyElt. They are distinguished by their XML element content. /// /// NOTE: The RDF syntax does not explicitly include the xml:lang attribute although it can /// appear in many of these. We have to allow for it in the attibute counts below. /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfPropertyElement(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, bool isTopLevel) { int nodeTerm = GetRdfTermKind(xmlNode); if (!IsPropertyElementName(nodeTerm)) { throw new XmpException("Invalid property element name", XmpError.BADRDF); } // remove the namespace-definitions from the list XmlAttributeCollection attributes = xmlNode.Attributes; if (attributes == null) return; IList nsAttrs = null; for (int i = 0; i < attributes.Count; i++) { XmlNode attribute = attributes[i]; if ("xmlns".Equals(attribute.Prefix) || (attribute.Prefix == null && "xmlns".Equals(attribute.Name))) { if (nsAttrs == null) { nsAttrs = new ArrayList(); } nsAttrs.Add(attribute.Name); } } if (nsAttrs != null) { for (IEnumerator it = nsAttrs.GetEnumerator(); it.MoveNext();) { string ns = (string) it.Current; attributes.RemoveNamedItem(ns); } } if (attributes.Count > 3) { // Only an emptyPropertyElt can have more than 3 attributes. RdfEmptyPropertyElement(xmp, xmpParent, xmlNode, isTopLevel); } else { // Look through the attributes for one that isn't rdf:ID or xml:lang, // it will usually tell what we should be dealing with. // The called routines must verify their specific syntax! for (int i = 0; i < attributes.Count; i++) { XmlNode attribute = attributes[i]; string attrLocal = attribute.LocalName; string attrNs = attribute.NamespaceURI; string attrValue = attribute.Value; if (!(XML_LANG.Equals(attribute.Name) && !("ID".Equals(attrLocal) && NS_RDF.Equals(attrNs)))) { if ("datatype".Equals(attrLocal) && NS_RDF.Equals(attrNs)) { RdfLiteralPropertyElement(xmp, xmpParent, xmlNode, isTopLevel); } else if (!("parseType".Equals(attrLocal) && NS_RDF.Equals(attrNs))) { RdfEmptyPropertyElement(xmp, xmpParent, xmlNode, isTopLevel); } else if ("Literal".Equals(attrValue)) { RdfParseTypeLiteralPropertyElement(); } else if ("Resource".Equals(attrValue)) { RdfParseTypeResourcePropertyElement(xmp, xmpParent, xmlNode, isTopLevel); } else if ("Collection".Equals(attrValue)) { RdfParseTypeCollectionPropertyElement(); } else { RdfParseTypeOtherPropertyElement(); } return; } } // Only rdf:ID and xml:lang, could be a resourcePropertyElt, a literalPropertyElt, // or an emptyPropertyElt. Look at the child XML nodes to decide which. if (xmlNode.HasChildNodes) { for (int i = 0; i < xmlNode.ChildNodes.Count; i++) { XmlNode currChild = xmlNode.ChildNodes[i]; if (currChild.NodeType != XmlNodeType.Text) { RdfResourcePropertyElement(xmp, xmpParent, xmlNode, isTopLevel); return; } } RdfLiteralPropertyElement(xmp, xmpParent, xmlNode, isTopLevel); } else { RdfEmptyPropertyElement(xmp, xmpParent, xmlNode, isTopLevel); } } }
/// <summary> /// 7.2.13 propertyEltList /// ws* ( propertyElt ws* )* /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlParent"> the currently processed XML node </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfPropertyElementList(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlParent, bool isTopLevel) { for (int i = 0; i < xmlParent.ChildNodes.Count; i++) { XmlNode currChild = xmlParent.ChildNodes[i]; if (IsWhitespaceNode(currChild)) { continue; } if (currChild.NodeType != XmlNodeType.Element) { throw new XmpException("Expected property element node not found", XmpError.BADRDF); } RdfPropertyElement(xmp, xmpParent, currChild, isTopLevel); } }
/// <seealso cref= XMPUtils#catenateArrayItems(XMPMeta, String, String, String, String, /// boolean) /// </seealso> /// <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 = "\""; } XmpMetaImpl xmpImpl = (XmpMetaImpl)xmp; // Return an empty result if the array does not exist, // hurl if it isn't the right form. XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName); XmpNode arrayNode = XmpNodeUtils.FindNode(xmpImpl.Root, arrayPath, false, null); if (arrayNode == null) { return(""); } if (!arrayNode.Options.Array || arrayNode.Options.ArrayAlternate) { throw new XmpException("Named property must be non-alternate array", XmpError.BADPARAM); } // Make sure the separator is OK. CheckSeparator(separator); // Make sure the open and close quotes are a legitimate pair. char openQuote = quotes[0]; char closeQuote = CheckQuotes(quotes, openQuote); // Build the result, quoting the array items, adding separators. // Hurl if any item isn't simple. StringBuilder catinatedString = new StringBuilder(); for (IEnumerator it = arrayNode.IterateChildren(); it.MoveNext();) { XmpNode currItem = (XmpNode)it.Current; if (currItem == null) { continue; } if (currItem.Options.CompositeProperty) { throw new XmpException("Array items must be simple", XmpError.BADPARAM); } string str = ApplyQuotes(currItem.Value, openQuote, closeQuote, allowCommas); catinatedString.Append(str); if (it.MoveNext()) { catinatedString.Append(separator); } } return(catinatedString.ToString()); }
/// <summary> /// Adds a child node. /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="value"> Node value </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <returns> Returns the newly created child node. </returns> /// <exception cref="XmpException"> thown on parsing errors </exception> private static XmpNode AddChildNode(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, string value, bool isTopLevel) { IXmpSchemaRegistry registry = XmpMetaFactory.SchemaRegistry; string @namespace = xmlNode.NamespaceURI; string childName; if (@namespace != null) { if (NS_DC_DEPRECATED.Equals(@namespace)) { // Fix a legacy DC namespace @namespace = NS_DC; } string prefix = registry.GetNamespacePrefix(@namespace); if (prefix == null) { prefix = xmlNode.Prefix ?? DEFAULT_PREFIX; prefix = registry.RegisterNamespace(@namespace, prefix); } childName = prefix + xmlNode.LocalName; } else { throw new XmpException("XML namespace required for all elements and attributes", XmpError.BADRDF); } // create schema node if not already there PropertyOptions childOptions = new PropertyOptions(); bool isAlias = false; if (isTopLevel) { // Lookup the schema node, adjust the XMP parent pointer. // Incoming parent must be the tree root. XmpNode schemaNode = XmpNodeUtils.FindSchemaNode(xmp.Root, @namespace, DEFAULT_PREFIX, true); schemaNode.Implicit = false; // Clear the implicit node bit. // need runtime check for proper 32 bit code. xmpParent = schemaNode; // If this is an alias set the alias flag in the node // and the hasAliases flag in the tree. if (registry.FindAlias(childName) != null) { isAlias = true; xmp.Root.HasAliases = true; schemaNode.HasAliases = true; } } // Make sure that this is not a duplicate of a named node. bool isArrayItem = "rdf:li".Equals(childName); bool isValueNode = "rdf:value".Equals(childName); // Create XMP node and so some checks XmpNode newChild = new XmpNode(childName, value, childOptions); newChild.Alias = isAlias; // Add the new child to the XMP parent node, a value node first. if (!isValueNode) { xmpParent.AddChild(newChild); } else { xmpParent.AddChild(1, newChild); } if (isValueNode) { if (isTopLevel || !xmpParent.Options.Struct) { throw new XmpException("Misplaced rdf:value element", XmpError.BADRDF); } xmpParent.HasValueChild = true; } if (isArrayItem) { if (!xmpParent.Options.Array) { throw new XmpException("Misplaced rdf:li element", XmpError.BADRDF); } newChild.Name = ARRAY_ITEM_NAME; } return newChild; }
/// <summary> /// Each of these parsing methods is responsible for recognizing an RDF /// syntax production and adding the appropriate structure to the XMP tree. /// They simply return for success, failures will throw an exception. /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="rdfRdfNode"> the top-level xml node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> internal static void RdfRdf(XmpMetaImpl xmp, XmlNode rdfRdfNode) { if (rdfRdfNode.Attributes != null && rdfRdfNode.Attributes.Count > 0) { RdfNodeElementList(xmp, xmp.Root, rdfRdfNode); } else { throw new XmpException("Invalid attributes of rdf:RDF element", XmpError.BADRDF); } }
/// <summary> /// see {@link XMPUtils#separateArrayItems(XMPMeta, String, String, String, /// PropertyOptions, boolean)} /// </summary> /// <param name="xmp"> /// The XMP object containing the array to be updated. </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="catedStr"> /// The string to be separated into the array items. </param> /// <param name="arrayOptions"> /// Option flags to control the separation. </param> /// <param name="preserveCommas"> /// Flag if commas shall be preserved /// </param> /// <exception cref="XmpException"> /// Forwards the Exceptions from the metadata processing </exception> public static void SeparateArrayItems(IXmpMeta xmp, string schemaNs, string arrayName, string catedStr, PropertyOptions arrayOptions, bool preserveCommas) { ParameterAsserts.AssertSchemaNs(schemaNs); ParameterAsserts.AssertArrayName(arrayName); if (catedStr == null) { throw new XmpException("Parameter must not be null", XmpError.BADPARAM); } ParameterAsserts.AssertImplementation(xmp); XmpMetaImpl xmpImpl = (XmpMetaImpl)xmp; // Keep a zero value, has special meaning below. XmpNode arrayNode = SeparateFindCreateArray(schemaNs, arrayName, arrayOptions, xmpImpl); // Extract the item values one at a time, until the whole input string is done. int charKind = UCK_NORMAL; char ch = (char)0; int itemEnd = 0; int endPos = catedStr.Length; while (itemEnd < endPos) { string itemValue; int itemStart; // Skip any leading spaces and separation characters. Always skip commas here. // They can be kept when within a value, but not when alone between values. for (itemStart = itemEnd; itemStart < endPos; itemStart++) { ch = catedStr[itemStart]; charKind = ClassifyCharacter(ch); if (charKind == UCK_NORMAL || charKind == UCK_QUOTE) { break; } } if (itemStart >= endPos) { break; } int nextKind; if (charKind != UCK_QUOTE) { // This is not a quoted value. Scan for the end, create an array // item from the substring. for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) { ch = catedStr[itemEnd]; charKind = ClassifyCharacter(ch); if (charKind == UCK_NORMAL || charKind == UCK_QUOTE || (charKind == UCK_COMMA && preserveCommas)) { continue; } if (charKind != UCK_SPACE) { break; } if ((itemEnd + 1) < endPos) { ch = catedStr[itemEnd + 1]; nextKind = ClassifyCharacter(ch); if (nextKind == UCK_NORMAL || nextKind == UCK_QUOTE || (nextKind == UCK_COMMA && preserveCommas)) { continue; } } // Anything left? break; // Have multiple spaces, or a space followed by a // separator. } itemValue = catedStr.Substring(itemStart, itemEnd - itemStart); } else { // Accumulate quoted values into a local string, undoubling // internal quotes that // match the surrounding quotes. Do not undouble "unmatching" // quotes. char openQuote = ch; char closeQuote = GetClosingQuote(openQuote); itemStart++; // Skip the opening quote; itemValue = ""; for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) { ch = catedStr[itemEnd]; charKind = ClassifyCharacter(ch); if (charKind != UCK_QUOTE || !IsSurroundingQuote(ch, openQuote, closeQuote)) { // This is not a matching quote, just append it to the // item value. itemValue += ch; } else { // This is a "matching" quote. Is it doubled, or the // final closing quote? // Tolerate various edge cases like undoubled opening // (non-closing) quotes, // or end of input. char nextChar; if ((itemEnd + 1) < endPos) { nextChar = catedStr[itemEnd + 1]; nextKind = ClassifyCharacter(nextChar); } else { nextKind = UCK_SEMICOLON; nextChar = (char)0x3B; } if (ch == nextChar) { // This is doubled, copy it and skip the double. itemValue += ch; // Loop will add in charSize. itemEnd++; } else if (!IsClosingingQuote(ch, openQuote, closeQuote)) { // This is an undoubled, non-closing quote, copy it. itemValue += ch; } else { // This is an undoubled closing quote, skip it and // exit the loop. itemEnd++; break; } } } } // Add the separated item to the array. // Keep a matching old value in case it had separators. int foundIndex = -1; for (int oldChild = 1; oldChild <= arrayNode.ChildrenLength; oldChild++) { if (itemValue.Equals(arrayNode.GetChild(oldChild).Value)) { foundIndex = oldChild; break; } } if (foundIndex < 0) { XmpNode newItem = new XmpNode(ARRAY_ITEM_NAME, itemValue, null); arrayNode.AddChild(newItem); } } }
/// <summary> /// The actual serialization. /// </summary> /// <param name="xmp"> the metadata object to be serialized </param> /// <param name="out"> outputStream the output stream to Serialize to </param> /// <param name="options"> the serialization options /// </param> /// <exception cref="XmpException"> If case of wrong options or any other serialization error. </exception> public virtual void Serialize(IXmpMeta xmp, Stream @out, SerializeOptions options) { try { _outputStream = new CountOutputStream(@out); _writer = new StreamWriter(_outputStream, Encoding.GetEncoding(options.Encoding)); _xmp = (XmpMetaImpl) xmp; _options = options; _padding = options.Padding; _writer = new StreamWriter(_outputStream, Encoding.GetEncoding(options.Encoding)); CheckOptionsConsistence(); // serializes the whole packet, but don't write the tail yet // and flush to make sure that the written bytes are calculated correctly string tailStr = SerializeAsRdf(); _writer.Flush(); // adds padding AddPadding(tailStr.Length); // writes the tail Write(tailStr); _writer.Flush(); _outputStream.Close(); } catch (IOException) { throw new XmpException("Error writing to the OutputStream", XmpError.UNKNOWN); } }
/// <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> /// Utility to find or create the array used by <code>separateArrayItems()</code>. </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, XmpMetaImpl xmp) { arrayOptions = XmpNodeUtils.VerifySetOptions(arrayOptions, null); if (!arrayOptions.OnlyArrayOptions) { throw new XmpException("Options can only provide array form", XmpError.BADOPTIONS); } // Find the array node, make sure it is OK. Move the current children // aside, to be readded later if kept. XmpPath arrayPath = XmpPathParser.ExpandXPath(schemaNs, arrayName); XmpNode arrayNode = XmpNodeUtils.FindNode(xmp.Root, arrayPath, false, null); if (arrayNode != null) { // The array exists, make sure the form is compatible. Zero // arrayForm means take what exists. PropertyOptions arrayForm = arrayNode.Options; if (!arrayForm.Array || arrayForm.ArrayAlternate) { throw new XmpException("Named property must be non-alternate array", XmpError.BADXPATH); } if (arrayOptions.EqualArrayTypes(arrayForm)) { throw new XmpException("Mismatch of specified and existing array form", XmpError.BADXPATH); // *** Right error? } } else { // The array does not exist, try to create it. // don't modify the options handed into the method arrayOptions.Array = true; arrayNode = XmpNodeUtils.FindNode(xmp.Root, arrayPath, true, arrayOptions); if (arrayNode == null) { throw new XmpException("Failed to create named array", XmpError.BADXPATH); } } return arrayNode; }
/// <summary> /// 7.2.18 parseTypeResourcePropertyElt /// start-element ( URI == propertyElementURIs, /// attributes == set ( idAttr?, parseResource ) ) /// propertyEltList /// end-element() /// /// Add a new struct node with a qualifier for the possible rdf:ID attribute. /// Then process the XML child nodes to get the struct fields. /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfParseTypeResourcePropertyElement(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, bool isTopLevel) { XmpNode newStruct = AddChildNode(xmp, xmpParent, xmlNode, "", isTopLevel); newStruct.Options.Struct = true; if (xmlNode.Attributes != null) { for (int i = 0; i < xmlNode.Attributes.Count; i++) { XmlNode attribute = xmlNode.Attributes[i]; if ("xmlns".Equals(attribute.Prefix) || (attribute.Prefix == null && "xmlns".Equals(attribute.Name))) { continue; } string attrLocal = attribute.LocalName; string attrNs = attribute.NamespaceURI; if (XML_LANG.Equals(attribute.Name)) { AddQualifierNode(newStruct, XML_LANG, attribute.Value); } else if (NS_RDF.Equals(attrNs) && ("ID".Equals(attrLocal) || "parseType".Equals(attrLocal))) { continue; // The caller ensured the value is "Resource". // Ignore all rdf:ID attributes. } throw new XmpException("Invalid attribute for ParseTypeResource property element", XmpError.BADRDF); } } RdfPropertyElementList(xmp, newStruct, xmlNode, false); if (newStruct.HasValueChild) { FixupQualifiedNode(newStruct); } }
/// <summary> /// 7.2.10 nodeElementList<br> /// ws* ( nodeElement ws* )* /// /// Note: this method is only called from the rdf:RDF-node (top level) </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="rdfRdfNode"> the top-level xml node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfNodeElementList(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode rdfRdfNode) { for (int i = 0; i < rdfRdfNode.ChildNodes.Count; i++) { XmlNode child = rdfRdfNode.ChildNodes[i]; // filter whitespaces (and all text nodes) if (!IsWhitespaceNode(child)) { RdfNodeElement(xmp, xmpParent, child, true); } } }
/// <summary> /// 7.2.21 emptyPropertyElt /// start-element ( URI == propertyElementURIs, /// attributes == set ( /// idAttr?, ( resourceAttr | nodeIdAttr )?, propertyAttr* ) ) /// end-element() /// /// <ns:Prop1/> <!-- a simple property with an empty value --> /// <ns:Prop2 rdf:resource="http: *www.adobe.com/"/> <!-- a URI value --> /// <ns:Prop3 rdf:value="..." ns:Qual="..."/> <!-- a simple qualified property --> /// <ns:Prop4 ns:Field1="..." ns:Field2="..."/> <!-- a struct with simple fields --> /// /// An emptyPropertyElt is an element with no contained content, just a possibly empty set of /// attributes. An emptyPropertyElt can represent three special cases of simple XMP properties: a /// simple property with an empty value (ns:Prop1), a simple property whose value is a URI /// (ns:Prop2), or a simple property with simple qualifiers (ns:Prop3). /// An emptyPropertyElt can also represent an XMP struct whose fields are all simple and /// unqualified (ns:Prop4). /// /// It is an error to use both rdf:value and rdf:resource - that can lead to invalid RDF in the /// verbose form written using a literalPropertyElt. /// /// The XMP mapping for an emptyPropertyElt is a bit different from generic RDF, partly for /// design reasons and partly for historical reasons. The XMP mapping rules are: /// <ol> /// <li> If there is an rdf:value attribute then this is a simple property /// with a text value. /// All other attributes are qualifiers. /// <li> If there is an rdf:resource attribute then this is a simple property /// with a URI value. /// All other attributes are qualifiers. /// <li> If there are no attributes other than xml:lang, rdf:ID, or rdf:nodeID /// then this is a simple /// property with an empty value. /// <li> Otherwise this is a struct, the attributes other than xml:lang, rdf:ID, /// or rdf:nodeID are fields. /// </ol> /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfEmptyPropertyElement(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, bool isTopLevel) { bool hasPropertyAttrs = false; bool hasResourceAttr = false; bool hasNodeIdAttr = false; bool hasValueAttr = false; XmlNode valueNode = null; // ! Can come from rdf:value or rdf:resource. if (xmlNode.HasChildNodes) { throw new XmpException("Nested content not allowed with rdf:resource or property attributes", XmpError.BADRDF); } // First figure out what XMP this maps to and remember the XML node for a simple value. if (xmlNode.Attributes != null) { for (int i = 0; i < xmlNode.Attributes.Count; i++) { XmlNode attribute = xmlNode.Attributes[i]; if ("xmlns".Equals(attribute.Prefix) || (attribute.Prefix == null && "xmlns".Equals(attribute.Name))) { continue; } int attrTerm = GetRdfTermKind(attribute); switch (attrTerm) { case RDFTERM_ID: // Nothing to do. break; case RDFTERM_RESOURCE: if (hasNodeIdAttr) { throw new XmpException( "Empty property element can't have both rdf:resource and rdf:nodeID", XmpError.BADRDF); } if (hasValueAttr) { throw new XmpException( "Empty property element can't have both rdf:value and rdf:resource", XmpError.BADXMP); } hasResourceAttr = true; if (!hasValueAttr) { valueNode = attribute; } break; case RDFTERM_NODE_ID: if (hasResourceAttr) { throw new XmpException( "Empty property element can't have both rdf:resource and rdf:nodeID", XmpError.BADRDF); } hasNodeIdAttr = true; break; case RDFTERM_OTHER: if ("value".Equals(attribute.LocalName) && NS_RDF.Equals(attribute.NamespaceURI)) { if (hasResourceAttr) { throw new XmpException( "Empty property element can't have both rdf:value and rdf:resource", XmpError.BADXMP); } hasValueAttr = true; valueNode = attribute; } else if (!XML_LANG.Equals(attribute.Name)) { hasPropertyAttrs = true; } break; default: throw new XmpException("Unrecognized attribute of empty property element", XmpError.BADRDF); } } } // Create the right kind of child node and visit the attributes again // to add the fields or qualifiers. // ! Because of implementation vagaries, // the xmpParent is the tree root for top level properties. // ! The schema is found, created if necessary, by addChildNode. XmpNode childNode = AddChildNode(xmp, xmpParent, xmlNode, "", isTopLevel); bool childIsStruct = false; if (hasValueAttr || hasResourceAttr) { childNode.Value = valueNode != null ? valueNode.Value : ""; if (!hasValueAttr) { // ! Might have both rdf:value and rdf:resource. childNode.Options.Uri = true; } } else if (hasPropertyAttrs) { childNode.Options.Struct = true; childIsStruct = true; } if (xmlNode.Attributes != null) { for (int i = 0; i < xmlNode.Attributes.Count; i++) { XmlNode attribute = xmlNode.Attributes[i]; if (attribute == valueNode || "xmlns".Equals(attribute.Prefix) || (attribute.Prefix == null && "xmlns".Equals(attribute.Name))) { continue; // Skip the rdf:value or rdf:resource attribute holding the value. } int attrTerm = GetRdfTermKind(attribute); switch (attrTerm) { case RDFTERM_ID: case RDFTERM_NODE_ID: break; // Ignore all rdf:ID and rdf:nodeID attributes. case RDFTERM_RESOURCE: AddQualifierNode(childNode, "rdf:resource", attribute.Value); break; case RDFTERM_OTHER: if (!childIsStruct) { AddQualifierNode(childNode, attribute.Name, attribute.Value); } else if (XML_LANG.Equals(attribute.Name)) { AddQualifierNode(childNode, XML_LANG, attribute.Value); } else { AddChildNode(xmp, childNode, attribute, attribute.Value, false); } break; default: throw new XmpException("Unrecognized attribute of empty property element", XmpError.BADRDF); } } } }
/// <summary> /// Serializes an <code>XMPMeta</code>-object as RDF into a byte buffer. /// </summary> /// <param name="xmp"> a metadata implementation object </param> /// <param name="options"> Options to control the serialization (see <seealso cref="SerializeOptions"/>). </param> /// <returns> Returns a byte buffer containing the serialized RDF. </returns> /// <exception cref="XmpException"> on serializsation errors. </exception> public static byte[] SerializeToBuffer(XmpMetaImpl xmp, SerializeOptions options) { MemoryStream @out = new MemoryStream(2048); Serialize(xmp, @out, options); return @out.GetBuffer(); }
/// <summary> /// The main parsing method. The XML tree is walked through from the root node and and XMP tree /// is created. This is a raw parse, the normalisation of the XMP tree happens outside. /// </summary> /// <param name="xmlRoot"> the XML root node </param> /// <returns> Returns an XMP metadata object (not normalized) </returns> /// <exception cref="XmpException"> Occurs if the parsing fails for any reason. </exception> internal static XmpMetaImpl Parse(XmlNode xmlRoot) { XmpMetaImpl xmp = new XmpMetaImpl(); RdfRdf(xmp, xmlRoot); return xmp; }
/// <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> /// 7.2.5 nodeElementURIs /// anyURI - ( coreSyntaxTerms | rdf:li | oldTerms ) /// /// 7.2.11 nodeElement /// start-element ( URI == nodeElementURIs, /// attributes == set ( ( idAttr | nodeIdAttr | aboutAttr )?, propertyAttr* ) ) /// propertyEltList /// end-element() /// /// A node element URI is rdf:Description or anything else that is not an RDF /// term. /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfNodeElement(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, bool isTopLevel) { int nodeTerm = GetRdfTermKind(xmlNode); if (nodeTerm != RDFTERM_DESCRIPTION && nodeTerm != RDFTERM_OTHER) { throw new XmpException("Node element must be rdf:Description or typed node", XmpError.BADRDF); } if (isTopLevel && nodeTerm == RDFTERM_OTHER) { throw new XmpException("Top level typed node not allowed", XmpError.BADXMP); } RdfNodeElementAttrs(xmp, xmpParent, xmlNode, isTopLevel); RdfPropertyElementList(xmp, xmpParent, xmlNode, isTopLevel); }
/// <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> /// 7.2.7 propertyAttributeURIs /// anyURI - ( coreSyntaxTerms | rdf:Description | rdf:li | oldTerms ) /// /// 7.2.11 nodeElement /// start-element ( URI == nodeElementURIs, /// attributes == set ( ( idAttr | nodeIdAttr | aboutAttr )?, propertyAttr* ) ) /// propertyEltList /// end-element() /// /// Process the attribute list for an RDF node element. A property attribute URI is /// anything other than an RDF term. The rdf:ID and rdf:nodeID attributes are simply ignored, /// as are rdf:about attributes on inner nodes. /// </summary> /// <param name="xmp"> the xmp metadata object that is generated </param> /// <param name="xmpParent"> the parent xmp node </param> /// <param name="xmlNode"> the currently processed XML node </param> /// <param name="isTopLevel"> Flag if the node is a top-level node </param> /// <exception cref="XmpException"> thown on parsing errors </exception> private static void RdfNodeElementAttrs(XmpMetaImpl xmp, XmpNode xmpParent, XmlNode xmlNode, bool isTopLevel) { // Used to detect attributes that are mutually exclusive. int exclusiveAttrs = 0; if (xmlNode == null || xmlNode.Attributes == null) return; for (int i = 0; i < xmlNode.Attributes.Count; i++) { XmlNode attribute = xmlNode.Attributes[i]; // quick hack, ns declarations do not appear in C++ // ignore "ID" without namespace if ("xmlns".Equals(attribute.Prefix) || (attribute.Prefix == null && "xmlns".Equals(attribute.Name))) { continue; } int attrTerm = GetRdfTermKind(attribute); switch (attrTerm) { case RDFTERM_ID: case RDFTERM_NODE_ID: case RDFTERM_ABOUT: if (exclusiveAttrs > 0) { throw new XmpException("Mutally exclusive about, ID, nodeID attributes", XmpError.BADRDF); } exclusiveAttrs++; if (isTopLevel && (attrTerm == RDFTERM_ABOUT)) { // This is the rdf:about attribute on a top level node. Set // the XMP tree name if // it doesn't have a name yet. Make sure this name matches // the XMP tree name. if (!string.IsNullOrEmpty(xmpParent.Name)) { if (!xmpParent.Name.Equals(attribute.Value)) { throw new XmpException("Mismatched top level rdf:about values", XmpError.BADXMP); } } else { xmpParent.Name = attribute.Value; } } break; case RDFTERM_OTHER: AddChildNode(xmp, xmpParent, attribute, attribute.Value, isTopLevel); break; default: throw new XmpException("Invalid nodeElement attribute", XmpError.BADRDF); } } }
/// <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(XmpMetaImpl 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.Root, XmpConst.NS_DC, true); // Do the special case fixes within each schema. IEnumerator it = xmp.Root.IterateChildren(); while (it.MoveNext()) { XmpNode currSchema = (XmpNode) it.Current; if (currSchema != null && XmpConst.NS_DC.Equals(currSchema.Name)) { NormalizeDcArrays(currSchema); } else if (currSchema != null && XmpConst.NS_EXIF.Equals(currSchema.Name)) { // Do a special case fix for exif:GPSTimeStamp. FixGpsTimeStamp(currSchema); XmpNode arrayNode = XmpNodeUtils.FindChildNode(currSchema, "exif:UserComment", false); if (arrayNode != null) { RepairAltText(arrayNode); } } else if (currSchema != null && XmpConst.NS_DM.Equals(currSchema.Name)) { // Do a special case migration of xmpDM:copyright to // dc:rights['x-default']. XmpNode dmCopyright = XmpNodeUtils.FindChildNode(currSchema, "xmpDM:copyright", false); if (dmCopyright != null) { MigrateAudioCopyright(xmp, dmCopyright); } } else if (currSchema != null && XmpConst.NS_XMP_RIGHTS.Equals(currSchema.Name)) { XmpNode arrayNode = XmpNodeUtils.FindChildNode(currSchema, "xmpRights:UsageTerms", false); if (arrayNode != null) { RepairAltText(arrayNode); } } } }