1 /* Copyright (C) 2005-2011 Fabio Riccardi */ 2 3 package com.lightcrafts.image.metadata; 4 5 import java.io.IOException; 6 7 import org.w3c.dom.Document; 8 import org.w3c.dom.Element; 9 import org.w3c.dom.Node; 10 11 import com.lightcrafts.app.Application; 12 import com.lightcrafts.image.metadata.values.ByteMetaValue; 13 import com.lightcrafts.image.metadata.values.ImageMetaValue; 14 import com.lightcrafts.image.metadata.values.UndefinedMetaValue; 15 import com.lightcrafts.image.metadata.values.UnsignedByteMetaValue; 16 import com.lightcrafts.utils.xml.ElementFilter; 17 import com.lightcrafts.utils.xml.XMLUtil; 18 import com.lightcrafts.utils.xml.NodeTypeFilter; 19 20 import static com.lightcrafts.image.metadata.XMPConstants.*; 21 22 /** 23 * <code>XMPUtil</code> is a set of utility functions for dealing with XMP 24 * metadata. 25 * 26 * @author Paul J. Lucas [paul@lightcrafts.com] 27 */ 28 public final class XMPUtil { 29 30 ////////// public ///////////////////////////////////////////////////////// 31 32 /** 33 * Creates a new empty XMP document. 34 * 35 * @param includeXMPPacket If <code>true</code>, XMP packet processing 36 * instructions are included in the new document. 37 * @return Returns said document. 38 */ createEmptyXMPDocument( boolean includeXMPPacket )39 public static Document createEmptyXMPDocument( boolean includeXMPPacket ) { 40 final StringBuilder s = new StringBuilder(); 41 if ( includeXMPPacket ) 42 s.append( XMP_XPACKET_BEGIN ); 43 s.append( XMP_EMPTY_DOCUMENT_STRING ); 44 if ( includeXMPPacket ) 45 s.append( XMP_XPACKET_END ); 46 try { 47 return XMLUtil.readDocumentFrom( s.toString() ); 48 } 49 catch ( IOException e ) { 50 throw new Error( e ); 51 } 52 } 53 54 /** 55 * Create an empty <code>rdf:Description</code> element. 56 * 57 * @param xmpDoc The XMP XML {@link Document} to create the RDF element as 58 * part of. 59 * @param nsURI The XML namespace URI to use. 60 * @param prefix The XML prefix to use. 61 * @return Returns said <code>rdf:Description</code> element. 62 */ createRDFDescription( Document xmpDoc, String nsURI, String prefix )63 public static Element createRDFDescription( Document xmpDoc, 64 String nsURI, String prefix ) { 65 final Element rdfDescElement = 66 xmpDoc.createElementNS( XMP_RDF_NS, XMP_RDF_PREFIX + ":Description" ); 67 rdfDescElement.setAttribute( XMP_RDF_PREFIX + ":about", "" ); 68 rdfDescElement.setAttribute( "xmlns:" + prefix, nsURI ); 69 return rdfDescElement; 70 } 71 72 /** 73 * Gets the LightZone description element child of an <code>rdf:RDF</code> 74 * of an XMP packet document. 75 * 76 * @param rdfElement The <code>rdf:RDF</code> element of an XMP packet 77 * document. 78 * @param create If <code>true</code>, create an empty LightZone 79 * description element and append it as a child of the <code>rdf:RDF</code> 80 * element if no LightZone description element exists. 81 * @return Returns the LightZone description element or <code>null</code> 82 * if there is no such element and <code>create</code> is false. 83 * @see #getRDFElementOf(Document) 84 */ getLZNDescription( Element rdfElement, boolean create )85 public static Element getLZNDescription( Element rdfElement, 86 boolean create ) 87 throws IOException 88 { 89 final ElementFilter filter = 90 new ElementFilter( 91 XMP_RDF_PREFIX + ":Description", 92 "xmlns:lzn", Application.LznNamespace 93 ); 94 Element lznDescElement = 95 (Element)XMLUtil.getFirstChildOf( rdfElement, filter ); 96 if ( lznDescElement != null || !create ) 97 return lznDescElement; 98 99 final Document emptyLZNDoc = 100 XMLUtil.readDocumentFrom( XMP_EMPTY_LZN_DESCRIPTION_STRING ); 101 lznDescElement = (Element)rdfElement.getOwnerDocument().importNode( 102 emptyLZNDoc.getDocumentElement(), true 103 ); 104 rdfElement.appendChild( lznDescElement ); 105 return lznDescElement; 106 } 107 108 /** 109 * Gets the LZN document from the given XMP document. 110 * 111 * @param xmpDoc The XMP XML {@link Document} to get the LZN document from. 112 * @return Returns said {@link Document} or <code>null</code> if the XMP 113 * document doesn't contain an LZN document. 114 */ getLZNDocumentFrom( Document xmpDoc )115 public static Document getLZNDocumentFrom( Document xmpDoc ) 116 throws IOException 117 { 118 final Element rdfElement = getRDFElementOf( xmpDoc ); 119 final Element lznDescription = getLZNDescription( rdfElement, false ); 120 if ( lznDescription == null ) 121 return null; 122 final Element child = (Element)XMLUtil.getFirstChildOf( 123 lznDescription, new NodeTypeFilter( Node.ELEMENT_NODE ) 124 ); 125 if ( child == null ) 126 return null; 127 if ( child.getLocalName().equals( "xmpwrapper" ) ) { 128 final Node lznNode = child.getFirstChild(); 129 if ( lznNode == null ) 130 return null; 131 final String lznText = lznNode.getTextContent(); 132 if ( lznText == null ) 133 return null; 134 return XMLUtil.readDocumentFrom( lznText ); 135 } 136 final Document lznDoc = XMLUtil.createDocument(); 137 lznDoc.appendChild( lznDoc.importNode( child, true ) ); 138 return lznDoc; 139 } 140 141 /** 142 * Gets the <code>rdf:RDF</code> element of an XMP packet document. 143 * 144 * @param xmpDoc The XMP packet document to get the <code>rdf:RDF</code> 145 * element of. 146 * @return Returns said element or <code>null</code> if the given document 147 * doesn't contain an <code>rdf:RDF</code> element. 148 */ getRDFElementOf( Document xmpDoc )149 public static Element getRDFElementOf( Document xmpDoc ) { 150 return (Element)XMLUtil.getFirstChildOf( 151 xmpDoc.getDocumentElement(), 152 new ElementFilter( XMP_RDF_PREFIX + ":RDF" ) 153 ); 154 } 155 156 /** 157 * Gets the XMP data from an {@link ImageMetaValue}. The reason this 158 * function exists is because, despite the XMP specification clearly 159 * stating that the type of the XMP data is unsigned byte, it's been seen 160 * to be undefined in practice; hence, this functions tests for both cases. 161 * 162 * @param value The {@link ImageMetaValue} to get the XMP data from. It is 163 * expected to be either an instance of {@link UnsignedByteMetaValue} or 164 * {@link UndefinedMetaValue}. 165 * @return Returns the raw bytes of the XMP data or <code>null</code> if 166 * either the value doesn't appear to contain XMP data or is 167 * <code>null</code>. 168 */ getXMPDataFrom( ImageMetaValue value )169 public static byte[] getXMPDataFrom( ImageMetaValue value ) { 170 if ( value instanceof UnsignedByteMetaValue ) 171 return ((ByteMetaValue)value).getByteValues(); 172 if ( value instanceof UndefinedMetaValue ) 173 return ((UndefinedMetaValue)value).getUndefinedValue(); 174 return null; 175 } 176 177 /** 178 * Merge the metadata for the EXIF, IPTC, TIFF, and XAP directories from 179 * one XMP document into another. 180 * 181 * @param newXMPDoc The {@link Document} containing the new XMP metadata 182 * for a particular directory. This document is not modified. 183 * @param oldXMPDoc The {@link Document} containing the old XMP metadata 184 * for a particular directory. This document has the new XMP metadata 185 * merged into it. 186 * @return Returns <code>oldXMPDoc</code>. 187 * @see #mergeMetadata(Document,Document,String,String) 188 */ mergeMetadata( Document newXMPDoc, Document oldXMPDoc )189 public static Document mergeMetadata( Document newXMPDoc, 190 Document oldXMPDoc ) { 191 mergeMetadata( 192 newXMPDoc, oldXMPDoc, XMP_DC_NS, XMP_DC_PREFIX 193 ); 194 mergeMetadata( 195 newXMPDoc, oldXMPDoc, XMP_EXIF_NS, XMP_EXIF_PREFIX 196 ); 197 mergeMetadata( 198 newXMPDoc, oldXMPDoc, XMP_IPTC_NS, XMP_IPTC_PREFIX 199 ); 200 mergeMetadata( 201 newXMPDoc, oldXMPDoc, XMP_TIFF_NS, XMP_TIFF_PREFIX 202 ); 203 mergeMetadata( 204 newXMPDoc, oldXMPDoc, XMP_XAP_NS, XMP_XAP_PREFIX 205 ); 206 return oldXMPDoc; 207 } 208 209 /** 210 * Merge the metadata for a given directory from one XMP document into 211 * another. 212 * 213 * @param newXMPDoc The {@link Document} containing the new XMP metadata 214 * for a particular directory. This document is not modified. 215 * @param oldXMPDoc The {@link Document} containing the old XMP metadata 216 * for a particular directory. This document has the new XMP metadata 217 * merged into it. 218 * @param dirNS The directory's XML namespace. 219 * @param dirPrefix The directory's XML prefix. 220 * @see #mergeMetadata(Document,Document) 221 */ mergeMetadata( Document newXMPDoc, Document oldXMPDoc, String dirNS, String dirPrefix )222 public static void mergeMetadata( Document newXMPDoc, Document oldXMPDoc, 223 String dirNS, String dirPrefix ) { 224 final Element newRDFElement = getRDFElementOf( newXMPDoc ); 225 final Element oldRDFElement = getRDFElementOf( oldXMPDoc ); 226 // 227 // Find the rdf element containing the metadata for the given 228 // directory. 229 // 230 final ElementFilter dirFilter = new ElementFilter( 231 XMP_RDF_PREFIX + ":Description", "xmlns:" + dirPrefix, dirNS 232 ); 233 Node newRDFDirElement = 234 XMLUtil.getFirstChildOf( newRDFElement, dirFilter ); 235 final Element oldRDFDirElement = 236 (Element)XMLUtil.getFirstChildOf( oldRDFElement, dirFilter ); 237 if ( newRDFDirElement == null ) { 238 if ( oldRDFDirElement != null ) { 239 // 240 // The new document doesn't contain the RDF element of interest 241 // so remove it from the old document. 242 // 243 final Node parent = oldRDFDirElement.getParentNode(); 244 parent.removeChild( oldRDFDirElement ); 245 } 246 return; 247 } 248 249 final Document oldDocument = oldRDFElement.getOwnerDocument(); 250 newRDFDirElement = oldDocument.importNode( newRDFDirElement, true ); 251 252 // 253 // See if the existing metadata has metadata for the directory we're 254 // doing: if so, replace it; if not, append it. 255 // 256 if ( oldRDFDirElement != null ) 257 oldRDFElement.replaceChild( newRDFDirElement, oldRDFDirElement ); 258 else 259 oldRDFElement.appendChild( newRDFDirElement ); 260 261 /* 262 // This old code does a node-by-node merge. 263 264 final ElementPrefixFilter dirPrefixFilter = 265 new ElementPrefixFilter( dirPrefix ); 266 final Node[] newDirElements = 267 XMLUtil.getChildrenOf( newRDFDirElement, dirPrefixFilter ); 268 for ( int i = 0; i < newDirElements.length; ++i ) { 269 final Element newDirElement = 270 (Element)oldDocument.importNode( newDirElements[i], true ); 271 final Element oldDirElement = (Element)XMLUtil.getFirstChildOf( 272 oldRDFDirElement, 273 new ElementFilter( newDirElement.getTagName() ) 274 ); 275 if ( oldDirElement != null ) 276 oldRDFDirElement.replaceChild( newDirElement, oldDirElement ); 277 else 278 oldRDFDirElement.appendChild( newDirElement ); 279 } 280 */ 281 } 282 283 } 284 /* vim:set et sw=4 ts=4: */ 285