001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.io.File;
005import java.io.IOException;
006import java.text.ParseException;
007import java.util.Date;
008
009import org.openstreetmap.josm.data.coor.LatLon;
010
011import com.drew.imaging.jpeg.JpegMetadataReader;
012import com.drew.imaging.jpeg.JpegProcessingException;
013import com.drew.lang.Rational;
014import com.drew.metadata.Directory;
015import com.drew.metadata.Metadata;
016import com.drew.metadata.MetadataException;
017import com.drew.metadata.Tag;
018import com.drew.metadata.exif.ExifIFD0Directory;
019import com.drew.metadata.exif.ExifSubIFDDirectory;
020import com.drew.metadata.exif.GpsDirectory;
021
022/**
023 * Read out EXIF information from a JPEG file
024 * @author Imi
025 * @since 99
026 */
027public final class ExifReader {
028
029    private ExifReader() {
030        // Hide default constructor for utils classes
031    }
032    
033    /**
034     * Returns the date/time from the given JPEG file.
035     * @param filename The JPEG file to read
036     * @return The date/time read in the EXIF section, or {@code null} if not found
037     * @throws ParseException if {@link DateParser#parse} fails to parse date/time
038     */
039    public static Date readTime(File filename) throws ParseException {
040        try {
041            Metadata metadata = JpegMetadataReader.readMetadata(filename);
042            String dateStr = null;
043            OUTER:
044            for (Directory dirIt : metadata.getDirectories()) {
045                for (Tag tag : dirIt.getTags()) {
046                    if (tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL /* 0x9003 */) {
047                        dateStr = tag.getDescription();
048                        break OUTER; // prefer this tag
049                    }
050                    if (tag.getTagType() == ExifIFD0Directory.TAG_DATETIME /* 0x0132 */ ||
051                        tag.getTagType() == ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED /* 0x9004 */) {
052                        dateStr = tag.getDescription();
053                    }
054                }
055            }
056            if (dateStr != null) {
057                dateStr = dateStr.replace('/', ':'); // workaround for HTC Sensation bug, see #7228
058                return DateParser.parse(dateStr);
059            }
060        } catch (ParseException e) {
061            throw e;
062        } catch (Exception e) {
063            e.printStackTrace();
064        }
065        return null;
066    }
067
068    /**
069     * Returns the image orientation of the given JPEG file.
070     * @param filename The JPEG file to read
071     * @return The image orientation as an {@code int}. Default value is 1. Possible values are listed in EXIF spec as follows:<br>
072     * <ul>1. The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.</ul>
073     * <ul>2. The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.</ul>
074     * <ul>3. The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.</ul>
075     * <ul>4. The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.</ul>
076     * <ul>5. The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.</ul>
077     * <ul>6. The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.</ul>
078     * <ul>7. The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.</ul>
079     * <ul>8. The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.</ul>
080     * @see <a href="http://www.impulseadventure.com/photo/exif-orientation.html">http://www.impulseadventure.com/photo/exif-orientation.html</a>
081     * @see <a href="http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto">http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto</a>
082     */
083    public static Integer readOrientation(File filename) {
084        try {
085            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
086            final Directory dir = metadata.getDirectory(ExifIFD0Directory.class);
087            return dir.getInt(ExifIFD0Directory.TAG_ORIENTATION);
088        } catch (JpegProcessingException e) {
089            e.printStackTrace();
090        } catch (MetadataException e) {
091            e.printStackTrace();
092        } catch (IOException e) {
093            e.printStackTrace();
094        }
095        return null;
096    }
097
098    /**
099     * Returns the geolocation of the given JPEG file.
100     * @param filename The JPEG file to read
101     * @return The lat/lon read in the EXIF section, or {@code null} if not found
102     * @since 6209
103     */
104    public static LatLon readLatLon(File filename) {
105        try {
106            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
107            final GpsDirectory dirGps = metadata.getDirectory(GpsDirectory.class);
108            return readLatLon(dirGps);
109        } catch (JpegProcessingException e) {
110            e.printStackTrace();
111        } catch (IOException e) {
112            e.printStackTrace();
113        } catch (MetadataException e) {
114            e.printStackTrace();
115        }
116        return null;
117    }
118
119    /**
120     * Returns the geolocation of the given EXIF GPS directory.
121     * @param dirGps The EXIF GPS directory
122     * @return The lat/lon read in the EXIF section, or {@code null} if {@code dirGps} is null
123     * @throws MetadataException 
124     * @since 6209
125     */
126    public static LatLon readLatLon(GpsDirectory dirGps) throws MetadataException {
127        if (dirGps != null) {
128            double lat = readAxis(dirGps, GpsDirectory.TAG_GPS_LATITUDE, GpsDirectory.TAG_GPS_LATITUDE_REF, 'S');
129            double lon = readAxis(dirGps, GpsDirectory.TAG_GPS_LONGITUDE, GpsDirectory.TAG_GPS_LONGITUDE_REF, 'W');
130            return new LatLon(lat, lon);
131        }
132        return null;
133    }
134    
135    /**
136     * Returns the direction of the given JPEG file.
137     * @param filename The JPEG file to read
138     * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), or {@code null} if missing or if {@code dirGps} is null
139     * @since 6209
140     */
141    public static Double readDirection(File filename) {
142        try {
143            final Metadata metadata = JpegMetadataReader.readMetadata(filename);
144            final GpsDirectory dirGps = metadata.getDirectory(GpsDirectory.class);
145            return readDirection(dirGps);
146        } catch (JpegProcessingException e) {
147            e.printStackTrace();
148        } catch (IOException e) {
149            e.printStackTrace();
150        }
151        return null;
152    }
153    
154    /**
155     * Returns the direction of the given EXIF GPS directory.
156     * @param dirGps The EXIF GPS directory
157     * @return The direction of the image when it was captures (in degrees between 0.0 and 359.99), or {@code null} if missing or if {@code dirGps} is null
158     * @since 6209
159     */
160    public static Double readDirection(GpsDirectory dirGps) {
161        if (dirGps != null) {
162            Rational direction = dirGps.getRational(GpsDirectory.TAG_GPS_IMG_DIRECTION);
163            if (direction != null) {
164                return direction.doubleValue();
165            }
166        }
167        return null;
168    }
169
170    private static double readAxis(GpsDirectory dirGps, int gpsTag, int gpsTagRef, char cRef) throws MetadataException  {
171        double value;
172        Rational[] components = dirGps.getRationalArray(gpsTag);
173        if (components != null) {
174            double deg = components[0].doubleValue();
175            double min = components[1].doubleValue();
176            double sec = components[2].doubleValue();
177   
178            if (Double.isNaN(deg) && Double.isNaN(min) && Double.isNaN(sec))
179                throw new IllegalArgumentException();
180   
181            value = (Double.isNaN(deg) ? 0 : deg + (Double.isNaN(min) ? 0 : (min / 60)) + (Double.isNaN(sec) ? 0 : (sec / 3600)));
182   
183            if (dirGps.getString(gpsTagRef).charAt(0) == cRef) {
184                value = -value;
185            }
186        } else {
187            // Try to read lon/lat as double value (Nonstandard, created by some cameras -> #5220)
188            value = dirGps.getDouble(gpsTag);
189        }
190        return value;
191    }
192}