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