package net.sourceforge.gpstools.xmp;
/* gpsdings
* Copyright (C) 2007 Moritz Ringler
* $Id: AbstractXMPReader.java 441 2010-12-13 20:04:20Z ringler $
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import java.io.File;
import java.io.Reader;
import java.io.IOException;
import java.util.Calendar;
import java.util.TimeZone;
import java.util.Date;
import java.nio.charset.Charset;
import java.nio.CharBuffer;
import java.nio.ByteBuffer;
import java.text.ParseException;
import org.xml.sax.SAXException;
import net.sourceforge.gpstools.gpx.Wpt;
import net.sourceforge.gpstools.exif.GpsExifReader;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
/** A class that reads GPS data from XMP metadata in a jpeg header.
It uses Java Advanced Image I/O to retrieve the metadata and
SAX to parse the XMP data. Therefore it has no external library dependencies.
<p>
Based on the 2005 Adobe XMP Specification. **/
public abstract class AbstractXMPReader implements GpsExifReader{
private transient XMPHandler xmphandler;
private final transient Object lockobj = new Object();
public final static String XMP_NAMESPACE = "http://ns.adobe.com/xap/1.0/";
/** Constructs a new AbstractXMPReader. */
public AbstractXMPReader(){
//sole constructor
}
protected abstract ByteBuffer getXMPBytes(File jpeg) throws IOException, XMPReadException;
/** Retrieves and decodes the XMP data in meta.
* @return an XMP document or <code>null</null> if no XMP node was found
**/
protected static CharBuffer getXMPChars(ByteBuffer xmpBytes){
CharBuffer result = null;
if(xmpBytes != null){
Charset utf8 = Charset.forName("utf-8");
result = utf8.decode(xmpBytes);
}
return(result);
}
/** Looks for XMP metadata in the specified jpeg file and if successfull
* extracts GPS information from it and returns it.
* @return a GPX waypoint if an XMP document containing at
* least latitude and longitude
* information was found, <code>null</code> otherwise
**/
@Override
public Wpt readGPSTag(File jpeg) throws IOException{
Wpt gps = null;
XMPProperties xmp = null;
try{
xmp = readXMP(jpeg);
} catch (XMPReadException xrx){
throw xrx.toIOException();
}
if(xmp != null){
gps = xmp.getWpt();
//System.out.println(gps.getLat());
//System.out.println(gps.getLon());
if (
gps != null &&
(gps.getLat() == null || gps.getLon() == null)
){
gps = null;
}
}
if(gps == null){
throw new IOException("No XMP GPS information found in " + jpeg.getPath());
}
return gps;
}
protected XMPProperties readXMP(File jpeg) throws IOException, XMPReadException{
ByteBuffer buff = getXMPBytes(jpeg);
CharBuffer xmpChars = getXMPChars(buff);
//System.out.println(xmpChars.toString());
XMPProperties result = null;
if(xmpChars != null){
result = parseXMP(xmpChars);
}
return result;
}
/** Returns the local dateTime that results from parsing <code>str</code>
and adding the utc offset of the timezone specified in <code>str</code>.
**/
@Override
public Date parseExifDate(String str) throws ParseException{
Calendar cal = XMPHandler.parseDate(str);
return (cal == null)? null : toLocalDate(cal);
}
private static Date toLocalDate(Calendar cal){
TimeZone tz = cal.getTimeZone();
long utcDate = cal.getTime().getTime();
return new Date(utcDate + tz.getOffset(utcDate));
}
/** Returns the local time that results from reading the
exif:DateTimeOriginal xmp property <em>ignoring</em>
the time zone specifier. This is useful when XMP must
be handled in the same way as Exif, which lacks time
zone information. **/
@Override
public Date readOriginalTime(File jpeg) throws IOException{
Calendar cal = null;
try{
cal = getOriginalTime(jpeg);
} catch (XMPReadException xrx){
throw xrx.toIOException();
}
return (cal == null)? null : toLocalDate(cal);
}
/** Returns the date and time that result from reading the
exif:DateTimeOriginal xmp property <em>including</em>
the time zone specifier. **/
public Calendar getOriginalTime(File jpeg) throws XMPReadException, IOException{
XMPProperties xmp = readXMP(jpeg);
return (xmp == null)? null : xmp.getTime();
}
/** Extracts information from a decoded XMP document and returns it
* as a GPX waypoint object.
* @return a GPX waypoint if at least latitude and longitude
* information was found in the XMP document, <code>null</code> otherwise
**/
synchronized XMPProperties parseXMP(CharBuffer text) throws XMPReadException, IOException{
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setFeature("http://xml.org/sax/features/namespaces", true);
reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
synchronized(lockobj){
if(xmphandler == null){
xmphandler = new XMPHandler();
}
}
reader.setContentHandler(xmphandler);
InputSource in = new InputSource(new CharBufferReader(text));
reader.parse(in);
} catch (SAXException ex){
throw new XMPReadException(ex);
}
return xmphandler.getParseResult();
}
/** Wraps a CharBuffer as a reader. Needed because you cannot construct
a SAX InputSource directly on a CharBuffer or Readable. **/
private static class CharBufferReader extends Reader{
private final CharBuffer buffer;
public CharBufferReader(CharBuffer buff){
buffer = buff;
}
@Override
public int read(char[] cbuf, int off, int len){
if(!buffer.hasRemaining()){
return -1;
}
int bytesToRead = Math.min(len, buffer.remaining());
buffer.get(cbuf, off, bytesToRead);
return bytesToRead;
}
@Override
public void close(){
// do nothing.
}
}
}