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